fix: validate "scope" for OAuth2, "state" for OpenID (#39642)

1. Fixes: https://github.com/go-gitea/gitea/issues/39641
* The OAuth2 authorize/grant endpoint accepted and persisted arbitrary
scope strings without validation, unlike personal access token creation
which rejects unknown scopes. Normalize and validate form.Scope before
creating or updating a grant, ignore invalid scopes
* `// TODO: at the moment invalid tokens are treated as "all" to avoid
breaking 1.22 behavior` is fixed.
2. To avoid CSRF attack for OpenID endpoint, add a state check

---------

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
Shivansh Gargandwxiaoguang authored and GitHub committed 2026-10-12 20:57:41 +00:00
1 parent 4b5f1f46de
commit f270534e83
9 files changed
+257 -57

No files matched your search

+24 -11
View File
@@ -803,7 +803,7 @@ func testOAuthIntrospectionCrossClientIsolation(t *testing.T) {
func testOAuthGrantScopesReadUserFailRepos(t *testing.T) {
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
accessToken := issueOAuthAccessTokenForScope(t, user, "openid read:user")
accessToken := issueOAuthAccessTokenResponseForScope(t, user, "openid read:user").AccessToken
userReq := NewRequest(t, "GET", "/api/v1/user")
userReq.SetHeader("Authorization", "Bearer "+accessToken)
userResp := MakeRequest(t, userReq, http.StatusOK)
@@ -832,7 +832,7 @@ func testOAuthGrantScopesReadUserFailRepos(t *testing.T) {
func testOAuthGrantScopesBasicRespectsWriteUser(t *testing.T) {
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
accessToken := issueOAuthAccessTokenForScope(t, user, "openid read:user")
accessToken := issueOAuthAccessTokenResponseForScope(t, user, "openid read:user").AccessToken
fullName := "oauth2-basic-scope-test"
updateBody := &api.UserSettingsOptions{
@@ -861,7 +861,7 @@ func testOAuthGrantScopesBasicRespectsWriteUser(t *testing.T) {
assert.Contains(t, basicError.Message, "required=[write:user]")
}
func issueOAuthAccessTokenForScope(t *testing.T, user *user_model.User, scope string) string {
func issueOAuthAccessTokenResponseForScope(t *testing.T, user *user_model.User, scope string) *oauth2_provider.AccessTokenResponse {
t.Helper()
appBody := api.CreateOAuth2ApplicationOptions{
@@ -899,15 +899,9 @@ func issueOAuthAccessTokenForScope(t *testing.T, user *user_model.User, scope st
"code": authcode,
})
accessTokenResp := ctx.MakeRequest(t, accessTokenReq, http.StatusOK)
type response struct {
AccessToken string `json:"access_token"`
TokenType string `json:"token_type"`
ExpiresIn int64 `json:"expires_in"`
RefreshToken string `json:"refresh_token"`
}
parsed := new(response)
parsed := new(oauth2_provider.AccessTokenResponse)
require.NoError(t, json.Unmarshal(accessTokenResp.Body.Bytes(), parsed))
return parsed.AccessToken
return parsed
}
func testOAuthGrantScopesReadRepositoryFailOrganization(t *testing.T) {
@@ -1445,4 +1439,23 @@ func TestOAuthUserInfoTokenScope(t *testing.T) {
req.SetHeader("Authorization", "Bearer "+userToken)
resp := MakeRequest(t, req, http.StatusOK)
assert.Contains(t, resp.Body.String(), "user2@example.com")
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
for _, tc := range []struct {
scope, wantScope string
status int
}{
{"invalid:scope", "no-token-scope", http.StatusForbidden},
{"openid invalid:scope", "openid no-token-scope", http.StatusForbidden},
{"no-token-scope", "no-token-scope", http.StatusForbidden},
{"openid read:user invalid:scope", "openid read:user", http.StatusOK},
} {
t.Run(tc.scope, func(t *testing.T) {
response := issueOAuthAccessTokenResponseForScope(t, user, tc.scope)
assert.Equal(t, tc.wantScope, response.Scope)
req := NewRequest(t, "GET", "/login/oauth/userinfo")
req.SetHeader("Authorization", "Bearer "+response.AccessToken)
MakeRequest(t, req, tc.status)
})
}
}