1
1
mirror of https://github.com/go-gitea/gitea synced 2024-12-23 00:54:28 +00:00

Improve oauth2 scope token handling (#32633)

This commit is contained in:
wxiaoguang 2024-11-26 10:03:02 +08:00 committed by GitHub
parent 25cacaf0aa
commit 9ed768adc4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 21 additions and 12 deletions

View File

@ -321,7 +321,7 @@ func tokenRequiresScopes(requiredScopeCategories ...auth_model.AccessTokenScopeC
} }
if !allow { if !allow {
ctx.Error(http.StatusForbidden, "tokenRequiresScope", fmt.Sprintf("token does not have at least one of required scope(s): %v", requiredScopes)) ctx.Error(http.StatusForbidden, "tokenRequiresScope", fmt.Sprintf("token does not have at least one of required scope(s), required=%v, token scope=%v", requiredScopes, scope))
return return
} }

View File

@ -74,26 +74,32 @@ type AccessTokenResponse struct {
// GrantAdditionalScopes returns valid scopes coming from grant // GrantAdditionalScopes returns valid scopes coming from grant
func GrantAdditionalScopes(grantScopes string) auth.AccessTokenScope { func GrantAdditionalScopes(grantScopes string) auth.AccessTokenScope {
// scopes_supported from templates/user/auth/oidc_wellknown.tmpl // scopes_supported from templates/user/auth/oidc_wellknown.tmpl
scopesSupported := []string{ generalScopesSupported := []string{
"openid", "openid",
"profile", "profile",
"email", "email",
"groups", "groups",
} }
var tokenScopes []string var accessScopes []string // the scopes for access control, but not for general information
for _, tokenScope := range strings.Split(grantScopes, " ") { for _, scope := range strings.Split(grantScopes, " ") {
if slices.Index(scopesSupported, tokenScope) == -1 { if scope != "" && !slices.Contains(generalScopesSupported, scope) {
tokenScopes = append(tokenScopes, tokenScope) accessScopes = append(accessScopes, scope)
} }
} }
// since version 1.22, access tokens grant full access to the API // since version 1.22, access tokens grant full access to the API
// with this access is reduced only if additional scopes are provided // with this access is reduced only if additional scopes are provided
accessTokenScope := auth.AccessTokenScope(strings.Join(tokenScopes, ",")) if len(accessScopes) > 0 {
if accessTokenWithAdditionalScopes, err := accessTokenScope.Normalize(); err == nil && len(tokenScopes) > 0 { accessTokenScope := auth.AccessTokenScope(strings.Join(accessScopes, ","))
return accessTokenWithAdditionalScopes if normalizedAccessTokenScope, err := accessTokenScope.Normalize(); err == nil {
return normalizedAccessTokenScope
} }
// TODO: if there are invalid access scopes (err != nil),
// then it is treated as "all", maybe in the future we should make it stricter to return an error
// at the moment, to avoid breaking 1.22 behavior, invalid tokens are also treated as "all"
}
// fallback, empty access scope is treated as "all" access
return auth.AccessTokenScopeAll return auth.AccessTokenScopeAll
} }

View File

@ -14,6 +14,7 @@ func TestGrantAdditionalScopes(t *testing.T) {
grantScopes string grantScopes string
expectedScopes string expectedScopes string
}{ }{
{"", "all"}, // for old tokens without scope, treat it as "all"
{"openid profile email", "all"}, {"openid profile email", "all"},
{"openid profile email groups", "all"}, {"openid profile email groups", "all"},
{"openid profile email all", "all"}, {"openid profile email all", "all"},
@ -22,12 +23,14 @@ func TestGrantAdditionalScopes(t *testing.T) {
{"read:user read:repository", "read:repository,read:user"}, {"read:user read:repository", "read:repository,read:user"},
{"read:user write:issue public-only", "public-only,write:issue,read:user"}, {"read:user write:issue public-only", "public-only,write:issue,read:user"},
{"openid profile email read:user", "read:user"}, {"openid profile email read:user", "read:user"},
// TODO: at the moment invalid tokens are treated as "all" to avoid breaking 1.22 behavior (more details are in GrantAdditionalScopes)
{"read:invalid_scope", "all"}, {"read:invalid_scope", "all"},
{"read:invalid_scope,write:scope_invalid,just-plain-wrong", "all"}, {"read:invalid_scope,write:scope_invalid,just-plain-wrong", "all"},
} }
for _, test := range tests { for _, test := range tests {
t.Run(test.grantScopes, func(t *testing.T) { t.Run("scope:"+test.grantScopes, func(t *testing.T) {
result := GrantAdditionalScopes(test.grantScopes) result := GrantAdditionalScopes(test.grantScopes)
assert.Equal(t, test.expectedScopes, string(result)) assert.Equal(t, test.expectedScopes, string(result))
}) })

View File

@ -565,7 +565,7 @@ func TestOAuth_GrantScopesReadUserFailRepos(t *testing.T) {
errorParsed := new(errorResponse) errorParsed := new(errorResponse)
require.NoError(t, json.Unmarshal(errorResp.Body.Bytes(), errorParsed)) require.NoError(t, json.Unmarshal(errorResp.Body.Bytes(), errorParsed))
assert.Contains(t, errorParsed.Message, "token does not have at least one of required scope(s): [read:repository]") assert.Contains(t, errorParsed.Message, "token does not have at least one of required scope(s), required=[read:repository]")
} }
func TestOAuth_GrantScopesReadRepositoryFailOrganization(t *testing.T) { func TestOAuth_GrantScopesReadRepositoryFailOrganization(t *testing.T) {
@ -708,7 +708,7 @@ func TestOAuth_GrantScopesReadRepositoryFailOrganization(t *testing.T) {
errorParsed := new(errorResponse) errorParsed := new(errorResponse)
require.NoError(t, json.Unmarshal(errorResp.Body.Bytes(), errorParsed)) require.NoError(t, json.Unmarshal(errorResp.Body.Bytes(), errorParsed))
assert.Contains(t, errorParsed.Message, "token does not have at least one of required scope(s): [read:user read:organization]") assert.Contains(t, errorParsed.Message, "token does not have at least one of required scope(s), required=[read:user read:organization]")
} }
func TestOAuth_GrantScopesClaimPublicOnlyGroups(t *testing.T) { func TestOAuth_GrantScopesClaimPublicOnlyGroups(t *testing.T) {