From ebce98448c76c19d0d7159743692dcd6b9ab359d Mon Sep 17 00:00:00 2001 From: abhishek9686 Date: Mon, 2 Sep 2024 09:23:28 +0530 Subject: [PATCH 1/3] use github apis to fetch user email --- models/user_mgmt.go | 21 +++++------ pro/auth/azure-ad.go | 40 ++++++++++++++------- pro/auth/github.go | 84 +++++++++++++++++++++++++++++++++++++------- 3 files changed, 110 insertions(+), 35 deletions(-) diff --git a/models/user_mgmt.go b/models/user_mgmt.go index 3efa81bf1..a928f5282 100644 --- a/models/user_mgmt.go +++ b/models/user_mgmt.go @@ -138,16 +138,17 @@ type UserGroup struct { // User struct - struct for Users type User struct { - UserName string `json:"username" bson:"username" validate:"min=3,max=40,in_charset|email"` - Password string `json:"password" bson:"password" validate:"required,min=5"` - IsAdmin bool `json:"isadmin" bson:"isadmin"` // deprecated - IsSuperAdmin bool `json:"issuperadmin"` // deprecated - RemoteGwIDs map[string]struct{} `json:"remote_gw_ids"` // deprecated - AuthType AuthType `json:"auth_type"` - UserGroups map[UserGroupID]struct{} `json:"user_group_ids"` - PlatformRoleID UserRoleID `json:"platform_role_id"` - NetworkRoles map[NetworkID]map[UserRoleID]struct{} `json:"network_roles"` - LastLoginTime time.Time `json:"last_login_time"` + UserName string `json:"username" bson:"username" validate:"min=3,max=40,in_charset|email"` + ExternalProviderID string `json:"external_provider_id"` + Password string `json:"password" bson:"password" validate:"required,min=5"` + IsAdmin bool `json:"isadmin" bson:"isadmin"` // deprecated + IsSuperAdmin bool `json:"issuperadmin"` // deprecated + RemoteGwIDs map[string]struct{} `json:"remote_gw_ids"` // deprecated + AuthType AuthType `json:"auth_type"` + UserGroups map[UserGroupID]struct{} `json:"user_group_ids"` + PlatformRoleID UserRoleID `json:"platform_role_id"` + NetworkRoles map[NetworkID]map[UserRoleID]struct{} `json:"network_roles"` + LastLoginTime time.Time `json:"last_login_time"` } type ReturnUserWithRolesAndGroups struct { diff --git a/pro/auth/azure-ad.go b/pro/auth/azure-ad.go index c41a96d81..fbe588ad2 100644 --- a/pro/auth/azure-ad.go +++ b/pro/auth/azure-ad.go @@ -3,6 +3,7 @@ package auth import ( "context" "encoding/json" + "errors" "fmt" "io" "net/http" @@ -60,7 +61,7 @@ func handleAzureCallback(w http.ResponseWriter, r *http.Request) { var content, err = getAzureUserInfo(rState, rCode) if err != nil { logger.Log(1, "error when getting user info from azure:", err.Error()) - if strings.Contains(err.Error(), "invalid oauth state") { + if strings.Contains(err.Error(), "invalid oauth state") || strings.Contains(err.Error(), "failed to fetch user email from SSO state") { handleOauthNotValid(w) return } @@ -74,12 +75,23 @@ func handleAzureCallback(w http.ResponseWriter, r *http.Request) { inviteExists = true } // check if user approval is already pending - if !inviteExists && logic.IsPendingUser(content.UserPrincipalName) { + if !inviteExists && logic.IsPendingUser(content.Email) { handleOauthUserSignUpApprovalPending(w) return } - - _, err = logic.GetUser(content.UserPrincipalName) + // if user exists with provider ID, convert them into email ID + user, err := logic.GetUser(content.UserPrincipalName) + if err == nil { + _, err := logic.GetUser(content.Email) + if err != nil { + user.UserName = content.Email + user.ExternalProviderID = content.UserPrincipalName + database.DeleteRecord(database.USERS_TABLE_NAME, content.UserPrincipalName) + d, _ := json.Marshal(user) + database.Insert(user.UserName, string(d), database.USERS_TABLE_NAME) + } + } + _, err = logic.GetUser(content.Email) if err != nil { if database.IsEmptyRecord(err) { // user must not exist, so try to make one if inviteExists { @@ -89,20 +101,20 @@ func handleAzureCallback(w http.ResponseWriter, r *http.Request) { logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal")) return } - user.UserName = content.UserPrincipalName // override username with azure id + user.ExternalProviderID = content.UserPrincipalName if err = logic.CreateUser(&user); err != nil { handleSomethingWentWrong(w) return } logic.DeleteUserInvite(content.Email) - logic.DeletePendingUser(content.UserPrincipalName) + logic.DeletePendingUser(content.Email) } else { - if !isEmailAllowed(content.UserPrincipalName) { + if !isEmailAllowed(content.Email) { handleOauthUserNotAllowedToSignUp(w) return } err = logic.InsertPendingUser(&models.User{ - UserName: content.UserPrincipalName, + UserName: content.Email, }) if err != nil { handleSomethingWentWrong(w) @@ -116,7 +128,7 @@ func handleAzureCallback(w http.ResponseWriter, r *http.Request) { return } } - user, err := logic.GetUser(content.UserPrincipalName) + user, err = logic.GetUser(content.Email) if err != nil { handleOauthUserNotFound(w) return @@ -136,7 +148,7 @@ func handleAzureCallback(w http.ResponseWriter, r *http.Request) { } // send a netmaker jwt token var authRequest = models.UserAuthParams{ - UserName: content.UserPrincipalName, + UserName: content.Email, Password: newPass, } @@ -146,8 +158,8 @@ func handleAzureCallback(w http.ResponseWriter, r *http.Request) { return } - logger.Log(1, "completed azure OAuth sigin in for", content.UserPrincipalName) - http.Redirect(w, r, servercfg.GetFrontendURL()+"/login?login="+jwt+"&user="+content.UserPrincipalName, http.StatusPermanentRedirect) + logger.Log(1, "completed azure OAuth sigin in for", content.Email) + http.Redirect(w, r, servercfg.GetFrontendURL()+"/login?login="+jwt+"&user="+content.Email, http.StatusPermanentRedirect) } func getAzureUserInfo(state string, code string) (*OAuthUser, error) { @@ -187,6 +199,10 @@ func getAzureUserInfo(state string, code string) (*OAuthUser, error) { if userInfo.Email == "" { userInfo.Email = getUserEmailFromClaims(token.AccessToken) } + if userInfo.Email == "" { + err = errors.New("failed to fetch user email from SSO state") + return userInfo, err + } return userInfo, nil } diff --git a/pro/auth/github.go b/pro/auth/github.go index 2b65ee2fb..1cb52cf92 100644 --- a/pro/auth/github.go +++ b/pro/auth/github.go @@ -3,6 +3,7 @@ package auth import ( "context" "encoding/json" + "errors" "fmt" "io" "net/http" @@ -60,7 +61,7 @@ func handleGithubCallback(w http.ResponseWriter, r *http.Request) { var content, err = getGithubUserInfo(rState, rCode) if err != nil { logger.Log(1, "error when getting user info from github:", err.Error()) - if strings.Contains(err.Error(), "invalid oauth state") { + if strings.Contains(err.Error(), "invalid oauth state") || strings.Contains(err.Error(), "failed to fetch user email from SSO state") { handleOauthNotValid(w) return } @@ -74,11 +75,25 @@ func handleGithubCallback(w http.ResponseWriter, r *http.Request) { inviteExists = true } // check if user approval is already pending - if !inviteExists && logic.IsPendingUser(content.Login) { + if !inviteExists && logic.IsPendingUser(content.Email) { handleOauthUserSignUpApprovalPending(w) return } - _, err = logic.GetUser(content.Login) + // if user exists with provider ID, convert them into email ID + user, err := logic.GetUser(content.Login) + if err == nil { + // checks if user exists with email + _, err := logic.GetUser(content.Email) + if err != nil { + user.UserName = content.Email + user.ExternalProviderID = content.Login + database.DeleteRecord(database.USERS_TABLE_NAME, content.Login) + d, _ := json.Marshal(user) + database.Insert(user.UserName, string(d), database.USERS_TABLE_NAME) + } + + } + _, err = logic.GetUser(content.Email) if err != nil { if database.IsEmptyRecord(err) { // user must not exist, so try to make one if inviteExists { @@ -88,20 +103,20 @@ func handleGithubCallback(w http.ResponseWriter, r *http.Request) { logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal")) return } - user.UserName = content.Login // overrides email with github id + user.ExternalProviderID = content.Login if err = logic.CreateUser(&user); err != nil { handleSomethingWentWrong(w) return } logic.DeleteUserInvite(content.Email) - logic.DeletePendingUser(content.Login) + logic.DeletePendingUser(content.Email) } else { - if !isEmailAllowed(content.Login) { + if !isEmailAllowed(content.Email) { handleOauthUserNotAllowedToSignUp(w) return } err = logic.InsertPendingUser(&models.User{ - UserName: content.Login, + UserName: content.Email, }) if err != nil { handleSomethingWentWrong(w) @@ -115,7 +130,7 @@ func handleGithubCallback(w http.ResponseWriter, r *http.Request) { return } } - user, err := logic.GetUser(content.Login) + user, err = logic.GetUser(content.Email) if err != nil { handleOauthUserNotFound(w) return @@ -135,7 +150,7 @@ func handleGithubCallback(w http.ResponseWriter, r *http.Request) { } // send a netmaker jwt token var authRequest = models.UserAuthParams{ - UserName: content.Login, + UserName: content.Email, Password: newPass, } @@ -145,11 +160,11 @@ func handleGithubCallback(w http.ResponseWriter, r *http.Request) { return } - logger.Log(1, "completed github OAuth sigin in for", content.Login) - http.Redirect(w, r, servercfg.GetFrontendURL()+"/login?login="+jwt+"&user="+content.Login, http.StatusPermanentRedirect) + logger.Log(1, "completed github OAuth sigin in for", content.Email) + http.Redirect(w, r, servercfg.GetFrontendURL()+"/login?login="+jwt+"&user="+content.Email, http.StatusPermanentRedirect) } -func getGithubUserInfo(state string, code string) (*OAuthUser, error) { +func getGithubUserInfo(state, code string) (*OAuthUser, error) { oauth_state_string, isValid := logic.IsStateValid(state) if (!isValid || state != oauth_state_string) && !isStateCached(state) { return nil, fmt.Errorf("invalid oauth state") @@ -187,7 +202,16 @@ func getGithubUserInfo(state string, code string) (*OAuthUser, error) { } userInfo.AccessToken = string(data) if userInfo.Email == "" { - userInfo.Email = getUserEmailFromClaims(token.AccessToken) + // if user's email is not made public, get the info from the github emails api + logger.Log(0, "=======> fetching user email from github api") + userInfo.Email, err = getGithubEmailsInfo(token.AccessToken) + if err != nil { + logger.Log(0, "failed to fetch user's email from github: ", err.Error()) + } + } + if userInfo.Email == "" { + err = errors.New("failed to fetch user email from SSO state") + return userInfo, err } return userInfo, nil } @@ -195,3 +219,37 @@ func getGithubUserInfo(state string, code string) (*OAuthUser, error) { func verifyGithubUser(token *oauth2.Token) bool { return token.Valid() } + +func getGithubEmailsInfo(accessToken string) (string, error) { + + var httpClient = &http.Client{} + var httpReq, reqErr = http.NewRequest("GET", "https://api.github.com/user/emails", nil) + if reqErr != nil { + return "", fmt.Errorf("failed to create request to GitHub") + } + httpReq.Header.Add("Accept", "application/vnd.github.v3+json") + httpReq.Header.Set("Authorization", "token "+accessToken) + response, err := httpClient.Do(httpReq) + if err != nil { + return "", fmt.Errorf("failed getting user info: %s", err.Error()) + } + defer response.Body.Close() + contents, err := io.ReadAll(response.Body) + if err != nil { + return "", fmt.Errorf("failed reading response body: %s", err.Error()) + } + + emailsInfo := []interface{}{} + err = json.Unmarshal(contents, &emailsInfo) + if err != nil { + return "", err + } + for _, info := range emailsInfo { + emailInfoMap := info.(map[string]interface{}) + if emailInfoMap["primary"].(bool) { + return emailInfoMap["email"].(string), nil + } + + } + return "", errors.New("email not found") +} From ed2a0a0a01e351d1fb8d722d2be6421385bffff3 Mon Sep 17 00:00:00 2001 From: abhishek9686 Date: Mon, 2 Sep 2024 10:57:10 +0530 Subject: [PATCH 2/3] fix oidc invite flow --- models/user_mgmt.go | 22 +++++++++++----------- pro/auth/azure-ad.go | 4 ++-- pro/auth/github.go | 4 ++-- pro/auth/google.go | 2 +- pro/auth/oidc.go | 4 ++-- 5 files changed, 18 insertions(+), 18 deletions(-) diff --git a/models/user_mgmt.go b/models/user_mgmt.go index a928f5282..a87a0f4b8 100644 --- a/models/user_mgmt.go +++ b/models/user_mgmt.go @@ -138,17 +138,17 @@ type UserGroup struct { // User struct - struct for Users type User struct { - UserName string `json:"username" bson:"username" validate:"min=3,max=40,in_charset|email"` - ExternalProviderID string `json:"external_provider_id"` - Password string `json:"password" bson:"password" validate:"required,min=5"` - IsAdmin bool `json:"isadmin" bson:"isadmin"` // deprecated - IsSuperAdmin bool `json:"issuperadmin"` // deprecated - RemoteGwIDs map[string]struct{} `json:"remote_gw_ids"` // deprecated - AuthType AuthType `json:"auth_type"` - UserGroups map[UserGroupID]struct{} `json:"user_group_ids"` - PlatformRoleID UserRoleID `json:"platform_role_id"` - NetworkRoles map[NetworkID]map[UserRoleID]struct{} `json:"network_roles"` - LastLoginTime time.Time `json:"last_login_time"` + UserName string `json:"username" bson:"username" validate:"min=3,max=40,in_charset|email"` + ExternalIdentityProviderID string `json:"external_identity_provider_id"` + Password string `json:"password" bson:"password" validate:"required,min=5"` + IsAdmin bool `json:"isadmin" bson:"isadmin"` // deprecated + IsSuperAdmin bool `json:"issuperadmin"` // deprecated + RemoteGwIDs map[string]struct{} `json:"remote_gw_ids"` // deprecated + AuthType AuthType `json:"auth_type"` + UserGroups map[UserGroupID]struct{} `json:"user_group_ids"` + PlatformRoleID UserRoleID `json:"platform_role_id"` + NetworkRoles map[NetworkID]map[UserRoleID]struct{} `json:"network_roles"` + LastLoginTime time.Time `json:"last_login_time"` } type ReturnUserWithRolesAndGroups struct { diff --git a/pro/auth/azure-ad.go b/pro/auth/azure-ad.go index fbe588ad2..7aa349536 100644 --- a/pro/auth/azure-ad.go +++ b/pro/auth/azure-ad.go @@ -85,7 +85,7 @@ func handleAzureCallback(w http.ResponseWriter, r *http.Request) { _, err := logic.GetUser(content.Email) if err != nil { user.UserName = content.Email - user.ExternalProviderID = content.UserPrincipalName + user.ExternalIdentityProviderID = content.UserPrincipalName database.DeleteRecord(database.USERS_TABLE_NAME, content.UserPrincipalName) d, _ := json.Marshal(user) database.Insert(user.UserName, string(d), database.USERS_TABLE_NAME) @@ -101,7 +101,7 @@ func handleAzureCallback(w http.ResponseWriter, r *http.Request) { logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal")) return } - user.ExternalProviderID = content.UserPrincipalName + user.ExternalIdentityProviderID = content.UserPrincipalName if err = logic.CreateUser(&user); err != nil { handleSomethingWentWrong(w) return diff --git a/pro/auth/github.go b/pro/auth/github.go index 1cb52cf92..5d2db5941 100644 --- a/pro/auth/github.go +++ b/pro/auth/github.go @@ -86,7 +86,7 @@ func handleGithubCallback(w http.ResponseWriter, r *http.Request) { _, err := logic.GetUser(content.Email) if err != nil { user.UserName = content.Email - user.ExternalProviderID = content.Login + user.ExternalIdentityProviderID = content.Login database.DeleteRecord(database.USERS_TABLE_NAME, content.Login) d, _ := json.Marshal(user) database.Insert(user.UserName, string(d), database.USERS_TABLE_NAME) @@ -103,7 +103,7 @@ func handleGithubCallback(w http.ResponseWriter, r *http.Request) { logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal")) return } - user.ExternalProviderID = content.Login + user.ExternalIdentityProviderID = content.Login if err = logic.CreateUser(&user); err != nil { handleSomethingWentWrong(w) return diff --git a/pro/auth/google.go b/pro/auth/google.go index 94db3a7c9..9ba9772c5 100644 --- a/pro/auth/google.go +++ b/pro/auth/google.go @@ -90,7 +90,7 @@ func handleGoogleCallback(w http.ResponseWriter, r *http.Request) { logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal")) return } - + user.ExternalIdentityProviderID = content.Email if err = logic.CreateUser(&user); err != nil { handleSomethingWentWrong(w) return diff --git a/pro/auth/oidc.go b/pro/auth/oidc.go index 72dc2b957..2fc71f665 100644 --- a/pro/auth/oidc.go +++ b/pro/auth/oidc.go @@ -80,10 +80,9 @@ func handleOIDCCallback(w http.ResponseWriter, r *http.Request) { handleOauthNotConfigured(w) return } - var inviteExists bool // check if invite exists for User - in, err := logic.GetUserInvite(content.Login) + in, err := logic.GetUserInvite(content.Email) if err == nil { inviteExists = true } @@ -102,6 +101,7 @@ func handleOIDCCallback(w http.ResponseWriter, r *http.Request) { logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal")) return } + user.ExternalIdentityProviderID = content.Email if err = logic.CreateUser(&user); err != nil { handleSomethingWentWrong(w) return From c4bfae77df6f4f861f989d768c3d70ac528d4a63 Mon Sep 17 00:00:00 2001 From: abhishek9686 Date: Mon, 2 Sep 2024 14:15:04 +0530 Subject: [PATCH 3/3] increase log verbose --- pro/auth/github.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pro/auth/github.go b/pro/auth/github.go index 5d2db5941..e72dc439d 100644 --- a/pro/auth/github.go +++ b/pro/auth/github.go @@ -203,7 +203,7 @@ func getGithubUserInfo(state, code string) (*OAuthUser, error) { userInfo.AccessToken = string(data) if userInfo.Email == "" { // if user's email is not made public, get the info from the github emails api - logger.Log(0, "=======> fetching user email from github api") + logger.Log(2, "fetching user email from github api") userInfo.Email, err = getGithubEmailsInfo(token.AccessToken) if err != nil { logger.Log(0, "failed to fetch user's email from github: ", err.Error())