0
0
mirror of https://github.com/go-gitea/gitea.git synced 2026-06-17 20:27:10 +02:00

Merge 8a38a381e05fb7a99394f3cc13a7a1c45cec6b37 into c68925152b1b6c8f92806cdbda9c4672dcc1608f

This commit is contained in:
Lunny Xiao 2026-06-17 12:31:14 -03:00 committed by GitHub
commit d72b9c2de6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 84 additions and 7 deletions

View File

@ -128,6 +128,7 @@ func InfoOAuth(ctx *context.Context) {
// IntrospectOAuth introspects an oauth token
func IntrospectOAuth(ctx *context.Context) {
var introspectingApp *auth.OAuth2Application
clientIDValid := false
authHeader := ctx.Req.Header.Get("Authorization")
if parsed, ok := httpauth.ParseAuthorizationHeader(authHeader); ok && parsed.BasicAuth != nil {
@ -140,6 +141,9 @@ func IntrospectOAuth(ctx *context.Context) {
return
}
clientIDValid = err == nil && app.ValidateClientSecret([]byte(clientSecret))
if clientIDValid {
introspectingApp = app
}
}
if !clientIDValid {
ctx.Resp.Header().Set("WWW-Authenticate", `Basic realm="Gitea OAuth2"`)
@ -158,13 +162,10 @@ func IntrospectOAuth(ctx *context.Context) {
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 err == nil && grant != nil && grant.ApplicationID == introspectingApp.ID {
response.Active = true
response.Scope = grant.Scope
response.RegisteredClaims = oauth2_provider.NewJwtRegisteredClaimsFromUser(introspectingApp.ClientID, grant.UserID, nil /*exp*/)
if user, err := user_model.GetUserByID(ctx, grant.UserID); err == nil {
response.Username = user.Name
}

View File

@ -14,6 +14,7 @@ import (
"net/http"
"net/http/httptest"
"net/url"
"strconv"
"strings"
"testing"
@ -122,6 +123,7 @@ func TestOAuth2(t *testing.T) {
t.Run("RefreshTokenInvalidation", testRefreshTokenInvalidation)
t.Run("RefreshTokenCrossClientUsage", testRefreshTokenCrossClientUsage)
t.Run("OAuthIntrospection", testOAuthIntrospection)
t.Run("OAuthIntrospectionCrossClientIsolation", testOAuthIntrospectionCrossClientIsolation)
t.Run("OAuthGrantScopesReadUserFailRepos", testOAuthGrantScopesReadUserFailRepos)
t.Run("OAuthGrantScopesBasicRespectsWriteUser", testOAuthGrantScopesBasicRespectsWriteUser)
t.Run("OAuthGrantScopesReadRepositoryFailOrganization", testOAuthGrantScopesReadRepositoryFailOrganization)
@ -705,6 +707,80 @@ func testOAuthIntrospection(t *testing.T) {
assert.Contains(t, resp.Body.String(), "no valid authorization")
}
func testOAuthIntrospectionCrossClientIsolation(t *testing.T) {
resourceOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
clientA := createOAuthTestApplication(t, "user1", "introspection-primary-client", []string{"https://primary.example/oauth/callback"})
clientB := createOAuthTestApplication(t, "user2", "introspection-secondary-client", []string{"https://secondary.example/oauth/callback"})
code, verifier := issueOAuthAuthorizationCode(t, resourceOwner, clientA, clientA.RedirectURIs[0], "openid profile")
req := NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
"grant_type": "authorization_code",
"client_id": clientA.ClientID,
"client_secret": clientA.ClientSecret,
"redirect_uri": clientA.RedirectURIs[0],
"code": code,
"code_verifier": verifier,
})
resp := MakeRequest(t, req, http.StatusOK)
type tokenResponse struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
}
tokenParsed := new(tokenResponse)
require.NoError(t, json.Unmarshal(resp.Body.Bytes(), tokenParsed))
require.NotEmpty(t, tokenParsed.AccessToken)
require.NotEmpty(t, tokenParsed.RefreshToken)
type introspectResponse struct {
Active bool `json:"active"`
Scope string `json:"scope,omitempty"`
Username string `json:"username,omitempty"`
Subject string `json:"sub,omitempty"`
Audience []string `json:"aud,omitempty"`
}
assertBlockedIntrospection := func(token string) {
t.Helper()
req = NewRequestWithValues(t, "POST", "/login/oauth/introspect", map[string]string{
"token": token,
})
req.SetBasicAuth(clientB.ClientID, clientB.ClientSecret)
resp = MakeRequest(t, req, http.StatusOK)
blocked := new(introspectResponse)
require.NoError(t, json.Unmarshal(resp.Body.Bytes(), blocked))
assert.False(t, blocked.Active)
assert.Empty(t, blocked.Scope)
assert.Empty(t, blocked.Username)
assert.Empty(t, blocked.Subject)
assert.Empty(t, blocked.Audience)
}
assertAllowedIntrospection := func(token string) {
t.Helper()
req = NewRequestWithValues(t, "POST", "/login/oauth/introspect", map[string]string{
"token": token,
})
req.SetBasicAuth(clientA.ClientID, clientA.ClientSecret)
resp = MakeRequest(t, req, http.StatusOK)
allowed := new(introspectResponse)
require.NoError(t, json.Unmarshal(resp.Body.Bytes(), allowed))
assert.True(t, allowed.Active)
assert.Equal(t, "openid profile", allowed.Scope)
assert.Equal(t, resourceOwner.Name, allowed.Username)
assert.Equal(t, strconv.FormatInt(resourceOwner.ID, 10), allowed.Subject)
assert.Equal(t, []string{clientA.ClientID}, allowed.Audience)
}
assertBlockedIntrospection(tokenParsed.AccessToken)
assertAllowedIntrospection(tokenParsed.AccessToken)
assertBlockedIntrospection(tokenParsed.RefreshToken)
assertAllowedIntrospection(tokenParsed.RefreshToken)
}
func testOAuthGrantScopesReadUserFailRepos(t *testing.T) {
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
accessToken := issueOAuthAccessTokenForScope(t, user, "openid read:user")