mirror of
https://github.com/go-gitea/gitea.git
synced 2026-11-02 13:44:16 +09:00
fix(auth): only auto-link the account the external identity points to (#40121)
With `[oauth2_client].ACCOUNT_LINKING = auto`, submitting the link account sign-up form with a user name or email that already exists linked the external identity to that existing account and signed in as it, without checking that the external identity belongs to that account. Automatic linking now only happens when the existing user matches the external identity: its email equals the provider email, or its name equals the user name derived from the provider (per `[oauth2_client].USERNAME`, same as auto registration). Otherwise the linking login page is shown, which requires the existing account's password. Auto registration is unchanged, since the name and email there already come from the provider. --------- Co-authored-by: silverwind <me@silverwind.io> Co-authored-by: Giteabot <teabot@gitea.io>
This commit is contained in:
2 files changed
+54
No files matched your search
@@ -449,6 +449,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()
|
||||
@@ -649,6 +657,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
|
||||
|
||||
@@ -115,6 +115,49 @@ func TestOAuth2AvatarFromPicture(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
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{
|
||||
|
||||
Reference in new issue
Block a user