From 076844c47d74789e87314ab3b8af5a9703c12c35 Mon Sep 17 00:00:00 2001 From: easonysliu Date: Sat, 14 Mar 2026 14:40:14 +0800 Subject: [PATCH] Fix OAuth2 authorization loop when grant scope changes When an OAuth2 client requests a different scope than what was previously granted, the GrantApplicationOAuth handler returned a server_error to the redirect URI. Since the client typically retries the authorization, this caused an infinite redirect loop with no way for the user to recover except by manually deleting the grant from the database. Update the existing grant's scope instead of returning an error, since the user has already explicitly consented to the new scope by clicking "Authorize" on the authorization page. Also update the scope for confidential/trusted clients in the AuthorizeOAuth auto-redirect path so they receive tokens with the correct (current) scope. Fixes go-gitea/gitea#36762 Co-Authored-By: Claude (claude-opus-4-6) --- models/auth/oauth2.go | 10 ++++++++++ routers/web/auth/oauth2_provider.go | 22 ++++++++++++++++------ 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/models/auth/oauth2.go b/models/auth/oauth2.go index e2bb72b722..d0d5d1e52f 100644 --- a/models/auth/oauth2.go +++ b/models/auth/oauth2.go @@ -537,6 +537,16 @@ func (grant *OAuth2Grant) SetNonce(ctx context.Context, nonce string) error { return nil } +// SetScope updates the scope of a grant +func (grant *OAuth2Grant) SetScope(ctx context.Context, scope string) error { + grant.Scope = scope + _, err := db.GetEngine(ctx).ID(grant.ID).Cols("scope").Update(grant) + if err != nil { + return err + } + return nil +} + // GetOAuth2GrantByID returns the grant with the given ID func GetOAuth2GrantByID(ctx context.Context, id int64) (grant *OAuth2Grant, err error) { grant = new(OAuth2Grant) diff --git a/routers/web/auth/oauth2_provider.go b/routers/web/auth/oauth2_provider.go index 05931d8f59..f75186cf3c 100644 --- a/routers/web/auth/oauth2_provider.go +++ b/routers/web/auth/oauth2_provider.go @@ -286,6 +286,14 @@ func AuthorizeOAuth(ctx *context.Context) { // Redirect if user already granted access and the application is confidential or trusted otherwise // I.e. always require authorization for untrusted public clients as recommended by RFC 6749 Section 10.2 if (app.ConfidentialClient || app.SkipSecondaryAuthorization) && grant != nil { + // Update the grant scope if the requested scope has changed, so that + // confidential/trusted clients always get a token with the current scope. + if grant.Scope != form.Scope { + if err := grant.SetScope(ctx, form.Scope); err != nil { + handleServerError(ctx, form.State, form.RedirectURI) + return + } + } code, err := grant.GenerateNewAuthorizationCode(ctx, form.RedirectURI, form.CodeChallenge, form.CodeChallengeMethod) if err != nil { handleServerError(ctx, form.State, form.RedirectURI) @@ -388,12 +396,14 @@ func GrantApplicationOAuth(ctx *context.Context) { return } } else if grant.Scope != form.Scope { - handleAuthorizeError(ctx, AuthorizeError{ - State: form.State, - ErrorDescription: "a grant exists with different scope", - ErrorCode: ErrorCodeServerError, - }, form.RedirectURI) - return + // The user has re-authorized with a different scope. Update the + // existing grant to match the newly consented scope. This avoids + // an infinite redirect loop when the OAuth2 client changes its + // requested scope after the initial authorization. + if err := grant.SetScope(ctx, form.Scope); err != nil { + handleServerError(ctx, form.State, form.RedirectURI) + return + } } if len(form.Nonce) > 0 {