mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-31 21:28:11 +09: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