diff --git a/modules/setting/oauth2.go b/modules/setting/oauth2.go index 8598b7b24d..4d9bf73bfd 100644 --- a/modules/setting/oauth2.go +++ b/modules/setting/oauth2.go @@ -22,11 +22,13 @@ const ( OAuth2UsernameNickname OAuth2UsernameType = "nickname" // OAuth2UsernameEmail username of oauth2 email field will be used as gitea name OAuth2UsernameEmail OAuth2UsernameType = "email" + // @OAuth2UsernamePreferredUsername oauth2 preferred_username field will be used as gitea name + OAuth2UsernamePreferredUsername OAuth2UsernameType = "preferred_username" ) func (username OAuth2UsernameType) isValid() bool { switch username { - case OAuth2UsernameUserid, OAuth2UsernameNickname, OAuth2UsernameEmail: + case OAuth2UsernameUserid, OAuth2UsernameNickname, OAuth2UsernameEmail, OAuth2UsernamePreferredUsername: return true } return false diff --git a/routers/web/auth/auth.go b/routers/web/auth/auth.go index 6950f7ff04..2f502871fb 100644 --- a/routers/web/auth/auth.go +++ b/routers/web/auth/auth.go @@ -401,6 +401,12 @@ func handleSignInFull(ctx *context.Context, u *user_model.User, remember, obeyRe func getUserName(gothUser *goth.User) (string, error) { switch setting.OAuth2Client.Username { + case setting.OAuth2UsernamePreferredUsername: + username := gothUser.RawData["preferred_username"].(string) + if strings.Contains(username, "@") { + return user_model.NormalizeUserName(strings.Split(username, "@")[0]) + } + return user_model.NormalizeUserName(username) case setting.OAuth2UsernameEmail: return user_model.NormalizeUserName(strings.Split(gothUser.Email, "@")[0]) case setting.OAuth2UsernameNickname: diff --git a/routers/web/auth/oauth.go b/routers/web/auth/oauth.go index e6d9c18b18..9efdc1e561 100644 --- a/routers/web/auth/oauth.go +++ b/routers/web/auth/oauth.go @@ -1117,6 +1117,8 @@ func SignInOAuthCallback(ctx *context.Context) { } if setting.OAuth2Client.Username == setting.OAuth2UsernameNickname && gothUser.NickName == "" { missingFields = append(missingFields, "nickname") + } else if setting.OAuth2Client.Username == setting.OAuth2UsernamePreferredUsername && (gothUser.RawData["preferred_username"] == nil || gothUser.RawData["preferred_username"].(string) == "") { + missingFields = append(missingFields, "preferred_username") } if len(missingFields) > 0 { // we don't have enough information to create an account automatically, diff --git a/tests/integration/oauth_test.go b/tests/integration/oauth_test.go index 9dace5def6..effb8ea11b 100644 --- a/tests/integration/oauth_test.go +++ b/tests/integration/oauth_test.go @@ -2127,3 +2127,205 @@ func TestSignInOAuthCallbackSignInRetrieveError(t *testing.T) { assert.NotNil(t, flashCookie) assert.Equal(t, "error%3DOAuth2%2BRetrieveError%253A%2Boauth2%253A%2Bcannot%2Bfetch%2Btoken%253A%2B404%2BNot%2BFound%250AResponse%253A%2Bcooked", flashCookie.Value) } + +func TestSignUpViaOAuthWithMissingNickname(t *testing.T) { + defer tests.PrepareTestEnv(t)() + // enable auto-creation of accounts via OAuth2 with "nickname" username + defer test.MockVariableValue(&setting.OAuth2Client.EnableAutoRegistration, true)() + defer test.MockVariableValue(&setting.OAuth2Client.Username, "nickname")() + + // OAuth2 authentication source GitLab + gitlabName := "gitlab" + addAuthSource(t, authSourcePayloadGitLabCustom(gitlabName)) + userGitLabUserID := "5678" + + // The Goth User doesn't contain a nickname, redirected to link account + defer mockCompleteUserAuth(func(res http.ResponseWriter, req *http.Request) (goth.User, error) { + return goth.User{ + Provider: gitlabName, + UserID: userGitLabUserID, + Email: "gitlabuser@example.com", + RawData: map[string]any{ + "preferred_username": "preferred_username", + }, + }, nil + })() + req := NewRequest(t, "GET", fmt.Sprintf("/user/oauth2/%s/callback?code=XYZ&state=XYZ", gitlabName)) + resp := MakeRequest(t, req, http.StatusSeeOther) + assert.Equal(t, "/user/link_account", test.RedirectURL(resp)) +} + +func TestSignUpViaOAuthWithMissingUsername(t *testing.T) { + defer tests.PrepareTestEnv(t)() + // enable auto-creation of accounts via OAuth2 with "preferred_username" username + defer test.MockVariableValue(&setting.OAuth2Client.EnableAutoRegistration, true)() + defer test.MockVariableValue(&setting.OAuth2Client.Username, "preferred_username")() + + // OAuth2 authentication source GitLab + gitlabName := "gitlab" + addAuthSource(t, authSourcePayloadGitLabCustom(gitlabName)) + userGitLabUserID := "5678" + + // The Goth User doesn't contain preferred_username, redirected to link account + defer mockCompleteUserAuth(func(res http.ResponseWriter, req *http.Request) (goth.User, error) { + return goth.User{ + Provider: gitlabName, + UserID: userGitLabUserID, + Email: "gitlabuser@example.com", + NickName: "nickname", + }, nil + })() + req := NewRequest(t, "GET", fmt.Sprintf("/user/oauth2/%s/callback?code=XYZ&state=XYZ", gitlabName)) + resp := MakeRequest(t, req, http.StatusSeeOther) + assert.Equal(t, "/user/link_account", test.RedirectURL(resp)) +} + +func TestSignUpViaOAuthWithNicknameAsUsername(t *testing.T) { + defer tests.PrepareTestEnv(t)() + // enable auto-creation of accounts via OAuth2 with "nickname" username + defer test.MockVariableValue(&setting.OAuth2Client.EnableAutoRegistration, true)() + defer test.MockVariableValue(&setting.OAuth2Client.Username, "nickname")() + + // OAuth2 authentication source GitLab + gitlabName := "gitlab" + addAuthSource(t, authSourcePayloadGitLabCustom(gitlabName)) + userGitLabUserID := "5678" + + // The Goth User contains the nickname that'll be used as the Forgejo user's username + defer mockCompleteUserAuth(func(res http.ResponseWriter, req *http.Request) (goth.User, error) { + return goth.User{ + Provider: gitlabName, + UserID: userGitLabUserID, + Email: "gitlabuser@example.com", + NickName: "nickname", + RawData: map[string]any{ + "preferred_username": "preferred_username", + }, + }, nil + })() + req := NewRequest(t, "GET", fmt.Sprintf("/user/oauth2/%s/callback?code=XYZ&state=XYZ", gitlabName)) + resp := MakeRequest(t, req, http.StatusSeeOther) + assert.Equal(t, "/", test.RedirectURL(resp)) + userAfterLogin := unittest.AssertExistsAndLoadBean(t, &user_model.User{LoginName: userGitLabUserID}) + assert.Equal(t, "nickname", userAfterLogin.Name) +} + +func TestSignUpViaOAuthWithUserIdAsUsername(t *testing.T) { + defer tests.PrepareTestEnv(t)() + // enable auto-creation of accounts via OAuth2 with "userid" username + defer test.MockVariableValue(&setting.OAuth2Client.EnableAutoRegistration, true)() + defer test.MockVariableValue(&setting.OAuth2Client.Username, "userid")() + + // OAuth2 authentication source GitLab + gitlabName := "gitlab" + addAuthSource(t, authSourcePayloadGitLabCustom(gitlabName)) + userGitLabUserID := "5678" + + // The Goth User's UserID will be used as the Forgejo user's username + defer mockCompleteUserAuth(func(res http.ResponseWriter, req *http.Request) (goth.User, error) { + return goth.User{ + Provider: gitlabName, + UserID: userGitLabUserID, + Email: "gitlabuser@example.com", + NickName: "nickname", + RawData: map[string]any{ + "preferred_username": "preferred_username", + }, + }, nil + })() + req := NewRequest(t, "GET", fmt.Sprintf("/user/oauth2/%s/callback?code=XYZ&state=XYZ", gitlabName)) + resp := MakeRequest(t, req, http.StatusSeeOther) + assert.Equal(t, "/", test.RedirectURL(resp)) + userAfterLogin := unittest.AssertExistsAndLoadBean(t, &user_model.User{LoginName: userGitLabUserID}) + assert.Equal(t, userGitLabUserID, userAfterLogin.Name) +} + +func TestSignUpViaOAuthWithEmailAsUsername(t *testing.T) { + defer tests.PrepareTestEnv(t)() + // enable auto-creation of accounts via OAuth2 with "email" username + defer test.MockVariableValue(&setting.OAuth2Client.EnableAutoRegistration, true)() + defer test.MockVariableValue(&setting.OAuth2Client.Username, "email")() + + // OAuth2 authentication source GitLab + gitlabName := "gitlab" + addAuthSource(t, authSourcePayloadGitLabCustom(gitlabName)) + userGitLabUserID := "5678" + + // The Goth User's email local part will be used as the Forgejo user's username + defer mockCompleteUserAuth(func(res http.ResponseWriter, req *http.Request) (goth.User, error) { + return goth.User{ + Provider: gitlabName, + UserID: userGitLabUserID, + Email: "gitlabuser@example.com", + NickName: "nickname", + RawData: map[string]any{ + "preferred_username": "preferred_username", + }, + }, nil + })() + req := NewRequest(t, "GET", fmt.Sprintf("/user/oauth2/%s/callback?code=XYZ&state=XYZ", gitlabName)) + resp := MakeRequest(t, req, http.StatusSeeOther) + assert.Equal(t, "/", test.RedirectURL(resp)) + userAfterLogin := unittest.AssertExistsAndLoadBean(t, &user_model.User{LoginName: userGitLabUserID}) + assert.Equal(t, "gitlabuser", userAfterLogin.Name) +} + +func TestSignUpViaOAuthWithPreferredUsernameAsUsername(t *testing.T) { + defer tests.PrepareTestEnv(t)() + // enable auto-creation of accounts via OAuth2 with "preferred_username" username + defer test.MockVariableValue(&setting.OAuth2Client.EnableAutoRegistration, true)() + defer test.MockVariableValue(&setting.OAuth2Client.Username, "preferred_username")() + + // OAuth2 authentication source GitLab + gitlabName := "gitlab" + addAuthSource(t, authSourcePayloadGitLabCustom(gitlabName)) + userGitLabUserID := "5678" + + // The Goth User's preferred_username claim will be used as the Forgejo user's username + defer mockCompleteUserAuth(func(res http.ResponseWriter, req *http.Request) (goth.User, error) { + return goth.User{ + Provider: gitlabName, + UserID: userGitLabUserID, + Email: "gitlabuser@example.com", + NickName: "nickname", + RawData: map[string]any{ + "preferred_username": "preferred_username", + }, + }, nil + })() + req := NewRequest(t, "GET", fmt.Sprintf("/user/oauth2/%s/callback?code=XYZ&state=XYZ", gitlabName)) + resp := MakeRequest(t, req, http.StatusSeeOther) + assert.Equal(t, "/", test.RedirectURL(resp)) + userAfterLogin := unittest.AssertExistsAndLoadBean(t, &user_model.User{LoginName: userGitLabUserID}) + assert.Equal(t, "preferred_username", userAfterLogin.Name) +} + +func TestSignUpViaOAuthWithPreferredUsernameProcessesEmail(t *testing.T) { + defer tests.PrepareTestEnv(t)() + // enable auto-creation of accounts via OAuth2 with "preferred_username" username + defer test.MockVariableValue(&setting.OAuth2Client.EnableAutoRegistration, true)() + defer test.MockVariableValue(&setting.OAuth2Client.Username, "preferred_username")() + + // OAuth2 authentication source GitLab + gitlabName := "gitlab" + addAuthSource(t, authSourcePayloadGitLabCustom(gitlabName)) + userGitLabUserID := "5678" + + // The Goth User's preferred_username claim contains an "@" suffix that will be stripped + defer mockCompleteUserAuth(func(res http.ResponseWriter, req *http.Request) (goth.User, error) { + return goth.User{ + Provider: gitlabName, + UserID: userGitLabUserID, + Email: "gitlabuser@example.com", + NickName: "nickname", + RawData: map[string]any{ + "preferred_username": "someotheremail@gmail.com", + }, + }, nil + })() + req := NewRequest(t, "GET", fmt.Sprintf("/user/oauth2/%s/callback?code=XYZ&state=XYZ", gitlabName)) + resp := MakeRequest(t, req, http.StatusSeeOther) + assert.Equal(t, "/", test.RedirectURL(resp)) + userAfterLogin := unittest.AssertExistsAndLoadBean(t, &user_model.User{LoginName: userGitLabUserID}) + assert.Equal(t, "someotheremail", userAfterLogin.Name) +}