fix(auth): ignore stale OIDC external login links to organizations (#37875)

## Summary

This fixes an OIDC sign-in edge case where a stale `external_login_user`
record can still point to an organization or a deleted user.

In that situation, Gitea may keep resolving the external login to the
wrong account during sign-in. For affected instances, this matches the
behavior reported in #36439 and #37812, where a user signing in with
OIDC/Entra ID could appear as an organization, or hit a 404 after that
organization was removed.

## What changed

- validate the user resolved from `external_login_user` during
OAuth2/OIDC login
- ignore stale links when the linked user no longer exists
- ignore stale links when the linked user is not an individual user
- remove the stale external login row so the sign-in flow can relink the
external account to the correct user

## Related

- Fixes #37812
- Related to #36439

---------

Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: Claude (Opus 4.8) <noreply@anthropic.com>
This commit is contained in:
Lunny Xiao
2026-05-30 13:37:09 -07:00
committed by GitHub
parent 28096162fa
commit 4e5f43896e
4 changed files with 103 additions and 23 deletions
+23 -7
View File
@@ -514,17 +514,33 @@ func oAuth2UserLoginCallback(ctx *context.Context, authSource *auth.Source, requ
}
// search in external linked users
externalLoginUser := &user_model.ExternalLoginUser{
ExternalID: gothUser.UserID,
LoginSourceID: authSource.ID,
}
hasUser, err = user_model.GetExternalLogin(request.Context(), externalLoginUser)
externalLoginUser, hasUser, err := user_model.GetExternalLogin(ctx, authSource.ID, gothUser.UserID)
if err != nil {
return nil, goth.User{}, err
}
if hasUser {
user, err = user_model.GetUserByID(request.Context(), externalLoginUser.UserID)
return user, gothUser, err
user, err = user_model.GetUserByID(ctx, externalLoginUser.UserID)
if err != nil && !user_model.IsErrUserNotExist(err) {
return nil, goth.User{}, err
}
if err == nil && user.IsIndividual() {
return user, gothUser, nil
}
// The external login record is stale: the linked user no longer exists, or it exists but is
// not an individual user (only individual users can sign in, so a link pointing at an
// organization, bot or remote user can never resolve). Remove it so the next sign-in can
// relink the external account to the correct user. Nothing is lost, because the link is
// recreated automatically on the next sign-in.
reason := "linked user does not exist"
if err == nil {
reason = fmt.Sprintf("linked user type is %d", user.Type)
}
log.Warn("Ignoring stale external login link [external-id=%s login-source-id=%d user-id=%d]: %s", externalLoginUser.ExternalID, externalLoginUser.LoginSourceID, externalLoginUser.UserID, reason)
if err := user_model.RemoveExternalLoginByExternalID(ctx, externalLoginUser.LoginSourceID, externalLoginUser.ExternalID); err != nil {
return nil, goth.User{}, err
}
}
// no user found to login