diff --git a/routers/web/auth/auth.go b/routers/web/auth/auth.go index 0fd6bf4967d..4b10b8b0916 100644 --- a/routers/web/auth/auth.go +++ b/routers/web/auth/auth.go @@ -447,6 +447,14 @@ func extractUserNameFromOAuth2(gothUser *goth.User) (string, error) { } } +func oauth2IdentityMatchesUser(gothUser *goth.User, u *user_model.User) bool { + if gothUser.Email != "" && strings.EqualFold(gothUser.Email, u.Email) { + return true + } + uname, err := extractUserNameFromOAuth2(gothUser) + return err == nil && uname != "" && strings.EqualFold(uname, u.Name) +} + // HandleSignOut resets the session and sets the cookies func HandleSignOut(ctx *context.Context) { _ = ctx.Session.Flush() @@ -646,6 +654,9 @@ func createUserInContext(ctx *context.Context, tpl templates.TplName, form any, return false } } + if !oauth2IdentityMatchesUser(&possibleLinkAccountData.GothUser, user) { + break // name or email came from the sign-up form, report the conflict instead of linking + } // TODO: probably we should respect 'remember' user's choice... oauth2LinkAccount(ctx, user, possibleLinkAccountData, true) return false // user is already created here, all redirects are handled diff --git a/tests/integration/oauth_test.go b/tests/integration/oauth_test.go index a28a9f6696e..cda0527b3b1 100644 --- a/tests/integration/oauth_test.go +++ b/tests/integration/oauth_test.go @@ -29,7 +29,10 @@ import ( "gitea.dev/modules/test" "gitea.dev/modules/timeutil" "gitea.dev/modules/util" + "gitea.dev/modules/web" + "gitea.dev/routers/web/auth" "gitea.dev/services/auth/source/oauth2" + "gitea.dev/services/context" "gitea.dev/services/oauth2_provider" "gitea.dev/tests" @@ -41,6 +44,49 @@ import ( "github.com/stretchr/testify/require" ) +func TestOAuth2LinkAccountSignupAutoLinking(t *testing.T) { + defer tests.PrepareTestEnv(t)() + defer test.MockVariableValue(&setting.OAuth2Client.AccountLinking, setting.OAuth2AccountLinkingAuto)() + defer test.MockVariableValue(&setting.Service.RequireExternalRegistrationPassword, false)() + + addOAuth2Source(t, "test-link-signup", oauth2.Source{}) + authSource, err := auth_model.GetActiveOAuth2SourceByAuthName(t.Context(), "test-link-signup") + require.NoError(t, err) + + signUp := func(t *testing.T, gothUser goth.User, userName, email string, expectedStatus int) (*TestSession, *httptest.ResponseRecorder) { + defer web.RouteMockReset() + web.RouteMock(web.MockAfterMiddlewares, func(ctx *context.Context) { + require.NoError(t, auth.Oauth2SetLinkAccountData(ctx, auth.LinkAccountData{AuthSourceID: authSource.ID, GothUser: gothUser})) + }) + session := emptyTestSession(t) + req := NewRequestWithValues(t, "POST", "/user/link_account_signup", map[string]string{ + "user_name": userName, + "email": email, + }) + return session, session.MakeRequest(t, req, expectedStatus) + } + + user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + for _, form := range []struct{ name, userName, email, errMsg string }{ + {"FormNamesOtherUser", user2.Name, "new-user@example.com", "The username is already taken."}, + {"FormUsesOtherUsersEmail", "new-user", user2.Email, "The email address is already used."}, + } { + t.Run(form.name, func(t *testing.T) { + gothUser := goth.User{UserID: form.name, Email: "external@example.com"} + _, resp := signUp(t, gothUser, form.userName, form.email, http.StatusOK) + assert.Contains(t, resp.Body.String(), form.errMsg) + unittest.AssertNotExistsBean(t, &user_model.ExternalLoginUser{ExternalID: gothUser.UserID}, unittest.OrderBy("external_id")) + }) + } + + t.Run("ExternalIdentityMatchesUser", func(t *testing.T) { + gothUser := goth.User{UserID: "user2-sub", Email: user2.Email} + session, _ := signUp(t, gothUser, user2.Name, user2.Email, http.StatusSeeOther) + unittest.AssertExistsAndLoadBean(t, &user_model.ExternalLoginUser{ExternalID: gothUser.UserID, UserID: user2.ID}, unittest.OrderBy("external_id")) + session.MakeRequest(t, NewRequest(t, "GET", "/user/settings"), http.StatusOK) + }) +} + func testOAuth2PrepareTestCode(t *testing.T) { require.NoError(t, db.TruncateBeans(t.Context(), &auth_model.OAuth2AuthorizationCode{})) err := db.Insert(t.Context(), &auth_model.OAuth2AuthorizationCode{