fix(auth): only auto-link the account the external identity points to (#40121) (#40137)

Backport https://github.com/go-gitea/gitea/pull/40121

With `[oauth2_client].ACCOUNT_LINKING = auto`, the link account sign-up
form linked the external identity to an existing account matching the
submitted user name or email and signed in as it. Automatic linking now
only happens when the existing user matches the external identity,
otherwise the linking login page asks for the account's password.

`release/v28` lacks `TestOAuth2AvatarFromPicture`, so only the new test
is added.

Co-authored-by: bircni <bircni@icloud.com>
Co-authored-by: Giteabot <teabot@gitea.io>
This commit is contained in:
authored and GitHub committed 2026-10-31 22:33:10 +00:00
1 parent 7868ade8f7
commit dfdb0438ad
2 files changed
+57

No files matched your search

+11
View File
@@ -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
+46
View File
@@ -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{