Skip to content

Commit

Permalink
Merge pull request #3090 from gravitl/NET-1576
Browse files Browse the repository at this point in the history
NET-1576: Use User Email as Primary ID for all Oauth logins
  • Loading branch information
abhishek9686 authored Sep 2, 2024
2 parents bbca20e + c4bfae7 commit 148efab
Show file tree
Hide file tree
Showing 5 changed files with 113 additions and 38 deletions.
21 changes: 11 additions & 10 deletions models/user_mgmt.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
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 {
Expand Down
40 changes: 28 additions & 12 deletions pro/auth/azure-ad.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package auth
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
Expand Down Expand Up @@ -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
}
Expand All @@ -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.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)
}
}
_, err = logic.GetUser(content.Email)
if err != nil {
if database.IsEmptyRecord(err) { // user must not exist, so try to make one
if inviteExists {
Expand All @@ -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.ExternalIdentityProviderID = 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)
Expand All @@ -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
Expand All @@ -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,
}

Expand All @@ -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) {
Expand Down Expand Up @@ -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
}

Expand Down
84 changes: 71 additions & 13 deletions pro/auth/github.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package auth
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
Expand Down Expand Up @@ -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
}
Expand All @@ -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.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)
}

}
_, err = logic.GetUser(content.Email)
if err != nil {
if database.IsEmptyRecord(err) { // user must not exist, so try to make one
if inviteExists {
Expand All @@ -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.ExternalIdentityProviderID = 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)
Expand All @@ -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
Expand All @@ -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,
}

Expand All @@ -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")
Expand Down Expand Up @@ -187,11 +202,54 @@ 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(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())
}
}
if userInfo.Email == "" {
err = errors.New("failed to fetch user email from SSO state")
return userInfo, err
}
return userInfo, nil
}

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")
}
2 changes: 1 addition & 1 deletion pro/auth/google.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions pro/auth/oidc.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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
Expand Down

0 comments on commit 148efab

Please sign in to comment.