mirror of
https://github.com/go-gitea/gitea.git
synced 2026-09-08 22:13:26 +09:00
fix(oauth): restrict introspection to the token's client (#38042)
Bind OAuth token introspection responses to the authenticated client. Return an inactive response when the token grant belongs to a different OAuth application to avoid leaking token metadata across clients. Add integration coverage for cross-client introspection attempts against both access tokens and refresh tokens. Assisted-by: GPT-5.4
This commit is contained in:
@@ -128,7 +128,7 @@ func InfoOAuth(ctx *context.Context) {
|
||||
|
||||
// IntrospectOAuth introspects an oauth token
|
||||
func IntrospectOAuth(ctx *context.Context) {
|
||||
clientIDValid := false
|
||||
var introspectingApp *auth.OAuth2Application
|
||||
authHeader := ctx.Req.Header.Get("Authorization")
|
||||
if parsed, ok := httpauth.ParseAuthorizationHeader(authHeader); ok && parsed.BasicAuth != nil {
|
||||
clientID, clientSecret := parsed.BasicAuth.Username, parsed.BasicAuth.Password
|
||||
@@ -139,9 +139,14 @@ func IntrospectOAuth(ctx *context.Context) {
|
||||
ctx.HTTPError(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
clientIDValid = err == nil && app.ValidateClientSecret([]byte(clientSecret))
|
||||
clientIDValid := err == nil && app.ValidateClientSecret([]byte(clientSecret))
|
||||
if clientIDValid {
|
||||
introspectingApp = app
|
||||
}
|
||||
}
|
||||
if !clientIDValid {
|
||||
if introspectingApp == nil {
|
||||
// RFC 7662 requires the caller to authenticate to the introspection endpoint.
|
||||
// https://www.rfc-editor.org/rfc/rfc7662.html#section-2.1
|
||||
ctx.Resp.Header().Set("WWW-Authenticate", `Basic realm="Gitea OAuth2"`)
|
||||
ctx.PlainText(http.StatusUnauthorized, "no valid authorization")
|
||||
return
|
||||
@@ -156,21 +161,36 @@ func IntrospectOAuth(ctx *context.Context) {
|
||||
|
||||
form := web.GetForm(ctx).(*forms.IntrospectTokenForm)
|
||||
token, err := oauth2_provider.ParseToken(form.Token, oauth2_provider.DefaultSigningKey)
|
||||
if err == nil {
|
||||
grant, err := auth.GetOAuth2GrantByID(ctx, token.GrantID)
|
||||
if err == nil && grant != nil {
|
||||
app, err := auth.GetOAuth2ApplicationByID(ctx, grant.ApplicationID)
|
||||
if err == nil && app != nil {
|
||||
response.Active = true
|
||||
response.Scope = grant.Scope
|
||||
response.RegisteredClaims = oauth2_provider.NewJwtRegisteredClaimsFromUser(app.ClientID, grant.UserID, nil /*exp*/)
|
||||
}
|
||||
if user, err := user_model.GetUserByID(ctx, grant.UserID); err == nil {
|
||||
response.Username = user.Name
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
// RFC 7662 returns inactive token metadata for invalid/unknown tokens.
|
||||
// https://www.rfc-editor.org/rfc/rfc7662.html#section-2.2
|
||||
log.Trace("Ignoring invalid token during introspection: %v", err)
|
||||
ctx.JSON(http.StatusOK, response)
|
||||
return
|
||||
}
|
||||
|
||||
grant, err := auth.GetOAuth2GrantByID(ctx, token.GrantID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetOAuth2GrantByID", err)
|
||||
return
|
||||
}
|
||||
if grant == nil || grant.ApplicationID != introspectingApp.ID {
|
||||
// RFC 7662 allows the server to reply inactive when the caller must not learn more.
|
||||
// https://www.rfc-editor.org/rfc/rfc7662.html#section-2.2
|
||||
ctx.JSON(http.StatusOK, response)
|
||||
return
|
||||
}
|
||||
|
||||
response.Active = true
|
||||
response.Scope = grant.Scope
|
||||
response.RegisteredClaims = oauth2_provider.NewJwtRegisteredClaimsFromUser(introspectingApp.ClientID, grant.UserID, nil /*exp*/)
|
||||
user, err := user_model.GetUserByID(ctx, grant.UserID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetUserByID", err)
|
||||
return
|
||||
}
|
||||
response.Username = user.Name
|
||||
|
||||
ctx.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user