Skip to content

Commit 9ed768a

Browse files
authored
Improve oauth2 scope token handling (#32633)
1 parent 25cacaf commit 9ed768a

File tree

4 files changed

+21
-12
lines changed

4 files changed

+21
-12
lines changed

routers/api/v1/api.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -321,7 +321,7 @@ func tokenRequiresScopes(requiredScopeCategories ...auth_model.AccessTokenScopeC
321321
}
322322

323323
if !allow {
324-
ctx.Error(http.StatusForbidden, "tokenRequiresScope", fmt.Sprintf("token does not have at least one of required scope(s): %v", requiredScopes))
324+
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))
325325
return
326326
}
327327

services/oauth2_provider/access_token.go

+14-8
Original file line numberDiff line numberDiff line change
@@ -74,26 +74,32 @@ type AccessTokenResponse struct {
7474
// GrantAdditionalScopes returns valid scopes coming from grant
7575
func GrantAdditionalScopes(grantScopes string) auth.AccessTokenScope {
7676
// scopes_supported from templates/user/auth/oidc_wellknown.tmpl
77-
scopesSupported := []string{
77+
generalScopesSupported := []string{
7878
"openid",
7979
"profile",
8080
"email",
8181
"groups",
8282
}
8383

84-
var tokenScopes []string
85-
for _, tokenScope := range strings.Split(grantScopes, " ") {
86-
if slices.Index(scopesSupported, tokenScope) == -1 {
87-
tokenScopes = append(tokenScopes, tokenScope)
84+
var accessScopes []string // the scopes for access control, but not for general information
85+
for _, scope := range strings.Split(grantScopes, " ") {
86+
if scope != "" && !slices.Contains(generalScopesSupported, scope) {
87+
accessScopes = append(accessScopes, scope)
8888
}
8989
}
9090

9191
// since version 1.22, access tokens grant full access to the API
9292
// with this access is reduced only if additional scopes are provided
93-
accessTokenScope := auth.AccessTokenScope(strings.Join(tokenScopes, ","))
94-
if accessTokenWithAdditionalScopes, err := accessTokenScope.Normalize(); err == nil && len(tokenScopes) > 0 {
95-
return accessTokenWithAdditionalScopes
93+
if len(accessScopes) > 0 {
94+
accessTokenScope := auth.AccessTokenScope(strings.Join(accessScopes, ","))
95+
if normalizedAccessTokenScope, err := accessTokenScope.Normalize(); err == nil {
96+
return normalizedAccessTokenScope
97+
}
98+
// TODO: if there are invalid access scopes (err != nil),
99+
// then it is treated as "all", maybe in the future we should make it stricter to return an error
100+
// at the moment, to avoid breaking 1.22 behavior, invalid tokens are also treated as "all"
96101
}
102+
// fallback, empty access scope is treated as "all" access
97103
return auth.AccessTokenScopeAll
98104
}
99105

services/oauth2_provider/additional_scopes_test.go

+4-1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ func TestGrantAdditionalScopes(t *testing.T) {
1414
grantScopes string
1515
expectedScopes string
1616
}{
17+
{"", "all"}, // for old tokens without scope, treat it as "all"
1718
{"openid profile email", "all"},
1819
{"openid profile email groups", "all"},
1920
{"openid profile email all", "all"},
@@ -22,12 +23,14 @@ func TestGrantAdditionalScopes(t *testing.T) {
2223
{"read:user read:repository", "read:repository,read:user"},
2324
{"read:user write:issue public-only", "public-only,write:issue,read:user"},
2425
{"openid profile email read:user", "read:user"},
26+
27+
// TODO: at the moment invalid tokens are treated as "all" to avoid breaking 1.22 behavior (more details are in GrantAdditionalScopes)
2528
{"read:invalid_scope", "all"},
2629
{"read:invalid_scope,write:scope_invalid,just-plain-wrong", "all"},
2730
}
2831

2932
for _, test := range tests {
30-
t.Run(test.grantScopes, func(t *testing.T) {
33+
t.Run("scope:"+test.grantScopes, func(t *testing.T) {
3134
result := GrantAdditionalScopes(test.grantScopes)
3235
assert.Equal(t, test.expectedScopes, string(result))
3336
})

tests/integration/oauth_test.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -565,7 +565,7 @@ func TestOAuth_GrantScopesReadUserFailRepos(t *testing.T) {
565565

566566
errorParsed := new(errorResponse)
567567
require.NoError(t, json.Unmarshal(errorResp.Body.Bytes(), errorParsed))
568-
assert.Contains(t, errorParsed.Message, "token does not have at least one of required scope(s): [read:repository]")
568+
assert.Contains(t, errorParsed.Message, "token does not have at least one of required scope(s), required=[read:repository]")
569569
}
570570

571571
func TestOAuth_GrantScopesReadRepositoryFailOrganization(t *testing.T) {
@@ -708,7 +708,7 @@ func TestOAuth_GrantScopesReadRepositoryFailOrganization(t *testing.T) {
708708

709709
errorParsed := new(errorResponse)
710710
require.NoError(t, json.Unmarshal(errorResp.Body.Bytes(), errorParsed))
711-
assert.Contains(t, errorParsed.Message, "token does not have at least one of required scope(s): [read:user read:organization]")
711+
assert.Contains(t, errorParsed.Message, "token does not have at least one of required scope(s), required=[read:user read:organization]")
712712
}
713713

714714
func TestOAuth_GrantScopesClaimPublicOnlyGroups(t *testing.T) {

0 commit comments

Comments
 (0)