Skip to content

Commit

Permalink
github oauth2 working [#523]
Browse files Browse the repository at this point in the history
  • Loading branch information
roberlander2 committed Aug 22, 2022
1 parent eb9f30b commit 107e79a
Show file tree
Hide file tree
Showing 9 changed files with 254 additions and 14 deletions.
13 changes: 11 additions & 2 deletions .secrets.baseline
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@
"filename": "core/auth/auth.go",
"hashed_secret": "4d55af37dbbb6a42088d917caa1ca25428ec42c9",
"is_verified": false,
"line_number": 2389
"line_number": 2393
}
],
"core/auth/auth_type_email.go": [
Expand Down Expand Up @@ -187,6 +187,15 @@
"line_number": 391
}
],
"core/auth/auth_type_oauth2.go": [
{
"type": "Secret Keyword",
"filename": "core/auth/auth_type_oauth2.go",
"hashed_secret": "15a46c63d80cdc62bb7e988a24b5839ecb624e25",
"is_verified": false,
"line_number": 236
}
],
"core/auth/auth_type_oidc.go": [
{
"type": "Secret Keyword",
Expand Down Expand Up @@ -279,5 +288,5 @@
}
]
},
"generated_at": "2022-08-15T22:35:05Z"
"generated_at": "2022-08-22T22:23:14Z"
}
4 changes: 4 additions & 0 deletions core/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -2074,6 +2074,10 @@ func (a *Auth) getExternalAuthTypeImpl(authType model.AuthType) (externalAuthTyp
if strings.HasSuffix(authType.Code, "_oidc") {
key = "oidc"
}
//github_oauth2, other_oauth2
if strings.HasSuffix(authType.Code, "_oauth2") {
key = "oauth2"
}

if auth, ok := a.externalAuthTypes[key]; ok {
return auth, nil
Expand Down
233 changes: 221 additions & 12 deletions core/auth/auth_type_oauth2.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,12 @@ import (
"core-building-block/core/model"
"core-building-block/utils"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strconv"
"strings"

"github.com/rokwire/logging-library-go/errors"
"github.com/rokwire/logging-library-go/logs"
Expand All @@ -44,15 +48,16 @@ type oauth2AuthImpl struct {
}

type oauth2AuthConfig struct {
Host string `json:"host" validate:"required"`
AuthorizeURL string `json:"authorize_url"`
TokenURL string `json:"token_url"`
UserInfoURL string `json:"userinfo_url"`
Scopes string `json:"scopes"`
AllowSignUp bool `json:"allow_signup"`
UseState bool `json:"use_state"`
ClientID string `json:"client_id" validate:"required"`
ClientSecret string `json:"client_secret" validate:"required"`
Host string `json:"host" validate:"required"`
AuthorizeURL string `json:"authorize_url"`
TokenURL string `json:"token_url"`
UserInfoURL string `json:"userinfo_url"`
Scopes string `json:"scopes"`
AllowSignUp bool `json:"allow_signup"`
UseState bool `json:"use_state"`
ClientID string `json:"client_id" validate:"required"`
ClientSecret string `json:"client_secret" validate:"required"`
TokenTypes map[string]string `json:"token_types" validaate:"required"`
}

type oauth2Token struct {
Expand All @@ -66,7 +71,22 @@ type oauth2RefreshParams struct {
}

func (a *oauth2AuthImpl) externalLogin(authType model.AuthType, appType model.ApplicationType, appOrg model.ApplicationOrganization, creds string, params string, l *logs.Log) (*model.ExternalSystemUser, map[string]interface{}, error) {
return nil, nil, errors.New(logutils.Unimplemented)
oauth2Config, err := a.getOAuth2AuthConfig(authType, appType)
if err != nil {
return nil, nil, errors.WrapErrorAction(logutils.ActionGet, typeOAuth2AuthConfig, nil, err)
}

parsedCreds, err := url.Parse(strings.ReplaceAll(creds, `"`, ""))
if err != nil {
return nil, nil, errors.WrapErrorAction(logutils.ActionParse, "oauth2 login creds", nil, err)
}

externalUser, parameters, err := a.newToken(parsedCreds.Query().Get("code"), authType, appType, appOrg, oauth2Config, l)
if err != nil {
return nil, nil, err
}

return externalUser, parameters, nil
}

func (a *oauth2AuthImpl) refresh(params map[string]interface{}, authType model.AuthType, appType model.ApplicationType, appOrg model.ApplicationOrganization, l *logs.Log) (*model.ExternalSystemUser, map[string]interface{}, error) {
Expand All @@ -88,8 +108,8 @@ func (a *oauth2AuthImpl) getLoginURL(authType model.AuthType, appType model.Appl
"client_id": oauth2Config.ClientID,
"redirect_uri": redirectURI,
"scope": oauth2Config.Scopes,
"login": "", //TODO: propt user with specific account to use (optional)
"allow_aignup": strconv.FormatBool(oauth2Config.AllowSignUp),
// "login": "", //TODO: prompt user with specific account to use (optional)
"allow_signup": strconv.FormatBool(oauth2Config.AllowSignUp),
}

//TODO: will need to store state variable to make use of this
Expand All @@ -113,6 +133,195 @@ func (a *oauth2AuthImpl) getLoginURL(authType model.AuthType, appType model.Appl
return authURL + "?" + query.Encode(), responseParams, nil
}

func (a *oauth2AuthImpl) newToken(code string, authType model.AuthType, appType model.ApplicationType, appOrg model.ApplicationOrganization, oauth2Config *oauth2AuthConfig, l *logs.Log) (*model.ExternalSystemUser, map[string]interface{}, error) {
bodyData := map[string]string{
"client_id": oauth2Config.ClientID,
"code": code,
}

return a.loadOAuth2TokensAndInfo(bodyData, oauth2Config, authType, appType, appOrg, l)
}

func (a *oauth2AuthImpl) loadOAuth2TokensAndInfo(bodyData map[string]string, oauth2Config *oauth2AuthConfig, authType model.AuthType, appType model.ApplicationType,
appOrg model.ApplicationOrganization, l *logs.Log) (*model.ExternalSystemUser, map[string]interface{}, error) {
token, err := a.loadOAuth2TokenWithParams(bodyData, oauth2Config)
if err != nil {
return nil, nil, errors.WrapErrorAction(logutils.ActionGet, typeOAuth2Token, nil, err)
}

// sub, err := a.checkToken(token.IDToken, authType, appType, oauth2Config, l)
// if err != nil {
// return nil, nil, errors.WrapErrorAction(logutils.ActionValidate, typeOAuth2Token, nil, err)
// }

userInfoURL := oauth2Config.Host + "/login/oauth/user"
if len(oauth2Config.UserInfoURL) > 0 {
userInfoURL = oauth2Config.UserInfoURL
}
if oauth2Config.TokenTypes[token.TokenType] != "" {
token.TokenType = oauth2Config.TokenTypes[token.TokenType]
}
userInfo, err := a.loadOAuth2UserInfo(token, userInfoURL)
if err != nil {
return nil, nil, errors.WrapErrorAction(logutils.ActionGet, "user info", nil, err)
}

var userClaims map[string]interface{}
err = json.Unmarshal(userInfo, &userClaims)
if err != nil {
return nil, nil, errors.WrapErrorAction(logutils.ActionUnmarshal, "user info", nil, err)
}

// userClaimsSub, _ := userClaims["sub"].(string)
// if userClaimsSub != sub {
// return nil, nil, errors.Newf("mismatching user info sub %s and id token sub %s", userClaimsSub, sub)
// }

identityProviderID, _ := authType.Params["identity_provider"].(string)
identityProviderSetting := appOrg.FindIdentityProviderSetting(identityProviderID)
if identityProviderSetting == nil {
return nil, nil, errors.ErrorData(logutils.StatusMissing, model.TypeIdentityProviderConfig, &logutils.FieldArgs{"app_org": appOrg.ID, "identity_provider_id": identityProviderID})
}

//identifier
identifier, _ := userClaims[identityProviderSetting.UserIdentifierField].(string)
//name
name, _ := userClaims[identityProviderSetting.NameField].(string)
names := strings.Split(name, "")
//email
email, _ := userClaims[identityProviderSetting.EmailField].(string)
//system specific
systemSpecific := map[string]interface{}{}
userSpecificFields := identityProviderSetting.UserSpecificFields
if len(userSpecificFields) > 0 {
for _, field := range userSpecificFields {
fieldValue, _ := userClaims[field].(string)
systemSpecific[field] = fieldValue
}
}
//external ids
externalIDs := make(map[string]string)
for k, v := range identityProviderSetting.ExternalIDFields {
externalID, ok := userClaims[v].(string)
if !ok {
a.auth.logger.ErrorWithFields("failed to parse external id", logutils.Fields{k: userClaims[v]})
continue
}
externalIDs[k] = externalID
}

externalUser := model.ExternalSystemUser{Identifier: identifier, ExternalIDs: externalIDs, FirstName: names[0],
LastName: names[1], Email: email, SystemSpecific: systemSpecific}

oauth2Params := map[string]interface{}{}
oauth2Params["access_token"] = token.AccessToken
oauth2Params["token_type"] = token.TokenType

params := map[string]interface{}{}
params["oauth2_token"] = oauth2Params
return &externalUser, params, nil
}

func (a *oauth2AuthImpl) loadOAuth2TokenWithParams(params map[string]string, oauth2Config *oauth2AuthConfig) (*oauth2Token, error) {
tokenURI := ""
oauth2TokenURL := oauth2Config.Host + "/login/oauth/access_token"
if len(oauth2Config.TokenURL) > 0 {
oauth2TokenURL = oauth2Config.TokenURL
}
if strings.Contains(oauth2TokenURL, "{client_id}") {
tokenURI = strings.ReplaceAll(oauth2TokenURL, "{client_id}", oauth2Config.ClientID)
tokenURI = strings.ReplaceAll(tokenURI, "{client_secret}", oauth2Config.ClientSecret)
} else if len(oauth2Config.ClientSecret) > 0 {
tokenURI = oauth2TokenURL
params["client_secret"] = oauth2Config.ClientSecret
} else {
tokenURI = oauth2TokenURL
}

data := url.Values{}
for k, v := range params {
data.Set(k, v)
}
headers := map[string]string{
"Accept": "application/json",
"Content-Type": "application/x-www-form-urlencoded",
"Content-Length": strconv.Itoa(len(data.Encode())),
}

client := &http.Client{}
req, err := http.NewRequest(http.MethodPost, tokenURI, strings.NewReader(data.Encode()))
if err != nil {
return nil, errors.WrapErrorAction(logutils.ActionCreate, logutils.TypeRequest, nil, err)
}
for k, v := range headers {
req.Header.Set(k, v)
}
resp, err := client.Do(req)
if err != nil {
return nil, errors.WrapErrorAction(logutils.ActionSend, logutils.TypeRequest, nil, err)
}

defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, errors.WrapErrorAction(logutils.ActionRead, logutils.TypeRequestBody, nil, err)
}
if resp.StatusCode != 200 {
return nil, errors.ErrorData(logutils.StatusInvalid, logutils.TypeResponse, &logutils.FieldArgs{"status_code": resp.StatusCode, "error": string(body)})
}

var authToken oauth2Token
err = json.Unmarshal(body, &authToken)
if err != nil {
return nil, errors.WrapErrorAction(logutils.ActionUnmarshal, logutils.TypeToken, nil, err)
}
validate := validator.New()
err = validate.Struct(authToken)
if err != nil {
return nil, errors.WrapErrorAction(logutils.ActionValidate, logutils.TypeToken, nil, err)
}

return &authToken, nil
}

func (a *oauth2AuthImpl) loadOAuth2UserInfo(token *oauth2Token, url string) ([]byte, error) {
if len(token.AccessToken) == 0 {
return nil, errors.ErrorData(logutils.StatusMissing, "access token", nil)
}
if len(token.TokenType) == 0 {
return nil, errors.ErrorData(logutils.StatusMissing, "token type", nil)
}
if len(url) == 0 {
return nil, errors.ErrorData(logutils.StatusMissing, "user info url", nil)
}

client := &http.Client{}
req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
return nil, errors.WrapErrorAction(logutils.ActionCreate, logutils.TypeRequest, nil, err)
}
req.Header.Set("Authorization", fmt.Sprintf("%s %s", token.TokenType, token.AccessToken))

resp, err := client.Do(req)
if err != nil {
return nil, errors.WrapErrorAction(logutils.ActionSend, logutils.TypeRequest, nil, err)
}

defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, errors.WrapErrorAction(logutils.ActionRead, logutils.TypeResponse, nil, err)
}
if resp.StatusCode != 200 {
return nil, errors.ErrorData(logutils.StatusInvalid, logutils.TypeResponse, &logutils.FieldArgs{"status_code": resp.StatusCode, "error": string(body)})
}
if len(body) == 0 {
return nil, errors.ErrorData(logutils.StatusMissing, logutils.TypeResponseBody, nil)
}

return body, nil
}

func (a *oauth2AuthImpl) getOAuth2AuthConfig(authType model.AuthType, appType model.ApplicationType) (*oauth2AuthConfig, error) {
errFields := &logutils.FieldArgs{"auth_type_id": authType.ID, "app_type_id": appType}

Expand Down
1 change: 1 addition & 0 deletions core/model/application.go
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,7 @@ type IdentityProviderSetting struct {
UserIdentifierField string `bson:"user_identifier_field"`
ExternalIDFields map[string]string `bson:"external_id_fields"`

NameField string `bson:"name_field"`
FirstNameField string `bson:"first_name_field"`
MiddleNameField string `bson:"middle_name_field"`
LastNameField string `bson:"last_name_field"`
Expand Down
6 changes: 6 additions & 0 deletions driver/web/docs/gen/def.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5302,6 +5302,7 @@ components:
- email
- twilio_phone
- illinois_oidc
- github_oauth2
- anonymous
app_type_identifier:
type: string
Expand All @@ -5314,6 +5315,10 @@ components:
- $ref: '#/components/schemas/_shared_req_CredsEmail'
- $ref: '#/components/schemas/_shared_req_CredsTwilioPhone'
- $ref: '#/components/schemas/_shared_req_CredsOIDC'
- type: string
description: |
Auth login creds for auth_type="oauth2" (or variants)
- full redirect URI received from OAuth2 provider
- $ref: '#/components/schemas/_shared_req_CredsAPIKey'
params:
type: object
Expand Down Expand Up @@ -5388,6 +5393,7 @@ components:
type: string
enum:
- illinois_oidc
- github_oauth2
app_type_identifier:
type: string
org_id:
Expand Down
4 changes: 4 additions & 0 deletions driver/web/docs/gen/gen_types.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions driver/web/docs/schemas/apis/shared/requests/CredsOAuth2.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
type: string
description: |
Auth login creds for auth_type="oauth2" (or variants)
- full redirect URI received from OAuth2 provider
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ properties:
type: string
enum:
- illinois_oidc
- github_oauth2
app_type_identifier:
type: string
org_id:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ properties:
- email
- twilio_phone
- illinois_oidc
- github_oauth2
- anonymous
app_type_identifier:
type: string
Expand All @@ -24,6 +25,7 @@ properties:
- $ref: "../CredsEmail.yaml"
- $ref: "../CredsTwilioPhone.yaml"
- $ref: "../CredsOIDC.yaml"
- $ref: "../CredsOAuth2.yaml"
- $ref: "../CredsAPIKey.yaml"
params:
type: object
Expand Down

0 comments on commit 107e79a

Please sign in to comment.