0
0
mirror of https://github.com/go-gitea/gitea.git synced 2026-06-09 21:20:42 +02:00
gitea/services/auth/source/oauth2/source_sync.go
bircni c43eb7c33a
fix(auth): do not auto-reactivate disabled users on OAuth2 callback (#38009)
The OAuth2 sign-in callback unconditionally set IsActive=true on the
local user row whenever the IdP authenticated them, silently undoing an
administrator's "Disable Account" action and granting the user a fresh
session in the same response. Treat the local IsActive flag as an
authoritative admin override: inactive users get a session and are
routed through the existing activate / prohibit-login pages by
verifyAuthWithOptions, matching the local-credentials sign-in path.

Adds an integration regression test that disables a linked local user
and asserts the row stays IsActive=false after a full OIDC callback.

---------

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-06-06 22:07:47 +00:00

115 lines
3.2 KiB
Go

// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package oauth2
import (
"context"
"time"
"gitea.dev/models/auth"
"gitea.dev/models/db"
user_model "gitea.dev/models/user"
"gitea.dev/modules/log"
"github.com/markbates/goth"
"golang.org/x/oauth2"
)
// Sync causes this OAuth2 source to synchronize its users with the db.
func (source *Source) Sync(ctx context.Context, updateExisting bool) error {
log.Trace("Doing: SyncExternalUsers[%s] %d", source.AuthSource.Name, source.AuthSource.ID)
if !updateExisting {
log.Info("SyncExternalUsers[%s] not running since updateExisting is false", source.AuthSource.Name)
return nil
}
provider, err := createProvider(source.AuthSource.Name, source)
if err != nil {
return err
}
if !provider.RefreshTokenAvailable() {
log.Trace("SyncExternalUsers[%s] provider doesn't support refresh tokens, can't synchronize", source.AuthSource.Name)
return nil
}
opts := user_model.FindExternalUserOptions{
HasRefreshToken: true,
Expired: true,
LoginSourceID: source.AuthSource.ID,
}
return user_model.IterateExternalLogin(ctx, opts, func(ctx context.Context, u *user_model.ExternalLoginUser) error {
return source.refresh(ctx, provider, u)
})
}
func (source *Source) refresh(ctx context.Context, provider goth.Provider, u *user_model.ExternalLoginUser) error {
log.Trace("Syncing login_source_id=%d external_id=%s expiration=%s", u.LoginSourceID, u.ExternalID, u.ExpiresAt)
shouldDisable := false
token, err := provider.RefreshToken(u.RefreshToken)
if err != nil {
if err, ok := err.(*oauth2.RetrieveError); ok && err.ErrorCode == "invalid_grant" {
// this signals that the token is not valid and the user should be disabled
shouldDisable = true
} else {
return err
}
}
user := &user_model.User{
LoginName: u.ExternalID,
LoginType: auth.OAuth2,
LoginSource: u.LoginSourceID,
}
hasUser, err := user_model.GetIndividualUser(ctx, user)
if err != nil {
return err
}
// If the grant is no longer valid, disable the user and
// delete local tokens. If the OAuth2 provider still
// recognizes them as a valid user, they will be able to login
// via their provider and reactivate their account.
if shouldDisable {
log.Info("SyncExternalUsers[%s] disabling user %d", source.AuthSource.Name, user.ID)
return db.WithTx(ctx, func(ctx context.Context) error {
if hasUser {
user.IsActive = false
err := user_model.UpdateUserCols(ctx, user, "is_active")
if err != nil {
return err
}
}
// HINT: OAUTH-AUTO-SYNC-USER-ACTIVATION
// Delete stored tokens, since they are invalid. This also prevents us from checking this in subsequent runs.
u.AccessToken = ""
u.RefreshToken = ""
u.ExpiresAt = time.Time{}
return user_model.UpdateExternalUserByExternalID(ctx, u)
})
}
// Otherwise, update the tokens
u.AccessToken = token.AccessToken
u.ExpiresAt = token.Expiry
// Some providers only update access tokens provide a new
// refresh token, so avoid updating it if it's empty
if token.RefreshToken != "" {
u.RefreshToken = token.RefreshToken
}
err = user_model.UpdateExternalUserByExternalID(ctx, u)
return err
}