mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-11-03 20:36:07 +01:00 
			
		
		
		
	Improve oauth2 scope token handling (#32633)
This commit is contained in:
		@@ -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
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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))
 | 
				
			||||||
		})
 | 
							})
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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) {
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user