-
-
Notifications
You must be signed in to change notification settings - Fork 6.4k
Description
Description
Thank you for maintaining Gitea — we rely on it daily and really appreciate the work that goes into the project.
Description
We encountered an issue where, when a user has previously authorized an OAuth2 application and the OAuth2 client later requests a different scope (e.g. the client application updated its scope configuration), the authorization flow enters an infinite redirect loop. The user has no way to recover from this through the UI — the only workaround we found is manually deleting the grant record from the database.
Gitea Version
main branch (commit b9d323c, 2026-02-18), also confirmed on v1.25.4
Steps to Reproduce
- Register a public OAuth2 application in Gitea
- An OAuth2 client requests authorization with
scope=repository - User clicks "Authorize Application" → a grant is created with scope
repository - The OAuth2 client's configuration changes to request
scope=read:repository,write:repository,read:user(e.g. the client application is updated to require additional permissions) - The client initiates a new authorization flow with the updated scope
- User sees the authorization page and clicks "Authorize Application" again
Note: The scope is specified by the OAuth2 client in the authorization request URL parameter (?scope=...), not configured in Gitea's application settings. This is standard OAuth2 behavior per RFC 6749 — clients may request different scopes on different authorization requests.
Expected Behavior
The authorization should succeed. Gitea should either:
- Update the existing grant's scope to match the newly authorized scope (the user has explicitly consented by clicking "Authorize" on a page showing the new permissions), or
- Delete the old grant and create a new one with the updated scope, or
- At minimum, show a clear error message to the user explaining the problem
Actual Behavior
The authorization enters an infinite redirect loop:
GET /login/oauth/authorize→ shows the authorization page (200 OK)- User clicks "Authorize Application"
POST /login/oauth/grant→ scope mismatch detected → error redirected toredirect_uri(303)- OAuth2 client receives the error and retries →
GET /login/oauth/authorize(200 OK) - Steps 2-4 repeat indefinitely
No error message is displayed to the user. The user cannot authorize the application and has no way to resolve this through the UI.
Server Logs
The loop is clearly visible in the access log. Below is an actual example — the user clicked "Authorize" once, triggering a rapid loop:
14:12:22 router: completed GET /login/oauth/authorize?client_id=...&scope=read:repository,write:repository,read:user&... 200 OK @ auth/oauth2_provider.go:177(auth.AuthorizeOAuth)
14:12:24 router: completed POST /login/oauth/grant 303 @ auth/oauth2_provider.go:353(auth.GrantApplicationOAuth)
14:12:24 router: completed GET /login/oauth/authorize?client_id=...&scope=read:repository,write:repository,read:user&... 200 OK @ auth/oauth2_provider.go:177(auth.AuthorizeOAuth)
14:12:24 router: completed POST /login/oauth/grant 303 @ auth/oauth2_provider.go:353(auth.GrantApplicationOAuth)
14:12:25 router: completed GET /login/oauth/authorize?client_id=...&scope=read:repository,write:repository,read:user&... 200 OK @ auth/oauth2_provider.go:177(auth.AuthorizeOAuth)
14:12:25 router: completed POST /login/oauth/grant 303 @ auth/oauth2_provider.go:353(auth.GrantApplicationOAuth)
14:12:25 router: completed GET /login/oauth/authorize?... 200 OK @ auth/oauth2_provider.go:177(auth.AuthorizeOAuth)
14:12:25 router: completed POST /login/oauth/grant 303 @ auth/oauth2_provider.go:353(auth.GrantApplicationOAuth)
14:12:26 router: completed GET /login/oauth/authorize?... 200 OK @ auth/oauth2_provider.go:177(auth.AuthorizeOAuth)
14:12:26 router: completed POST /login/oauth/grant 303 @ auth/oauth2_provider.go:353(auth.GrantApplicationOAuth)
14:12:26 router: completed GET /login/oauth/authorize?... 200 OK @ auth/oauth2_provider.go:177(auth.AuthorizeOAuth)
For comparison, a successful authorization (user with no pre-existing grant):
14:02:36 router: completed GET /login/oauth/authorize?... 200 OK @ auth/oauth2_provider.go:177(auth.AuthorizeOAuth)
14:02:39 router: completed POST /login/oauth/grant 303 @ auth/oauth2_provider.go:353(auth.GrantApplicationOAuth)
14:02:39 router: completed POST /login/oauth/access_token 200 OK @ auth/oauth2_provider.go:461(auth.AccessTokenOAuth)
Root Cause (our analysis)
We traced this to routers/web/auth/oauth2_provider.go. The GrantApplicationOAuth() function (line ~389) checks if the existing grant's scope matches the requested scope:
} else if grant.Scope != form.Scope {
handleAuthorizeError(ctx, AuthorizeError{
State: form.State,
ErrorDescription: "a grant exists with different scope",
ErrorCode: ErrorCodeServerError,
}, form.RedirectURI)
return
}When the scopes don't match, it returns an error to the redirect_uri. The OAuth2 client typically retries the authorization, creating the infinite loop.
The grant is never updated or replaced, so the user is permanently stuck until the grant record is manually deleted from the oauth2_grant table.
Additional Information
- This affects all user types (local, LDAP, etc.) — it is not authentication-source-specific
- This is a common real-world scenario: OAuth2 clients update their scope requirements as they evolve
- For confidential clients or applications with
skip_secondary_authorizationenabled, the issue manifests differently:AuthorizeOAuth()(line ~287) auto-redirects using the old grant, which may cause the client to receive insufficient permissions
Possible Fix
This is just our suggestion — we'd love to hear the maintainers' thoughts on the best approach.
One option would be to update the existing grant's scope in GrantApplicationOAuth() when a mismatch is detected. Since the user has already consented to the new scope by clicking "Authorize" on the authorization page, this seems safe:
} else if grant.Scope != form.Scope {
grant.Scope = form.Scope
if err = grant.UpdateScope(ctx); err != nil {
handleServerError(ctx, form.State, form.RedirectURI)
return
}
}Another option would be to delete the old grant and create a new one with the updated scope.
Gitea Version
v1.25.4
Can you reproduce the bug on the Gitea demo site?
No
Log Gist
No response
Screenshots
No response
Git Version
v1.25.4
Operating System
No response
How are you running Gitea?
Official binary downloaded from the Gitea releases page, managed by supervisor on Linux.
Database
MySQL/MariaDB