Skip to content

Commit 50e997c

Browse files
authored
Merge branch 'main' into patch-21
2 parents 09bae8c + a3881ff commit 50e997c

File tree

8 files changed

+537
-18
lines changed

8 files changed

+537
-18
lines changed

options/locale/locale_en-US.ini

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -459,6 +459,7 @@ authorize_application = Authorize Application
459459
authorize_redirect_notice = You will be redirected to %s if you authorize this application.
460460
authorize_application_created_by = This application was created by %s.
461461
authorize_application_description = If you grant the access, it will be able to access and write to all your account information, including private repos and organisations.
462+
authorize_application_with_scopes = With scopes: %s
462463
authorize_title = Authorize "%s" to access your account?
463464
authorization_failed = Authorization failed
464465
authorization_failed_desc = The authorization failed because we detected an invalid request. Please contact the maintainer of the app you have tried to authorize.

routers/web/auth/oauth2_provider.go

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,18 @@ func InfoOAuth(ctx *context.Context) {
104104
Picture: ctx.Doer.AvatarLink(ctx),
105105
}
106106

107-
groups, err := oauth2_provider.GetOAuthGroupsForUser(ctx, ctx.Doer)
107+
var accessTokenScope auth.AccessTokenScope
108+
if auHead := ctx.Req.Header.Get("Authorization"); auHead != "" {
109+
auths := strings.Fields(auHead)
110+
if len(auths) == 2 && (auths[0] == "token" || strings.ToLower(auths[0]) == "bearer") {
111+
accessTokenScope, _ = auth_service.GetOAuthAccessTokenScopeAndUserID(ctx, auths[1])
112+
}
113+
}
114+
115+
// since version 1.22 does not verify if groups should be public-only,
116+
// onlyPublicGroups will be set only if 'public-only' is included in a valid scope
117+
onlyPublicGroups, _ := accessTokenScope.PublicOnly()
118+
groups, err := oauth2_provider.GetOAuthGroupsForUser(ctx, ctx.Doer, onlyPublicGroups)
108119
if err != nil {
109120
ctx.ServerError("Oauth groups for user", err)
110121
return
@@ -304,6 +315,9 @@ func AuthorizeOAuth(ctx *context.Context) {
304315
return
305316
}
306317

318+
// check if additional scopes
319+
ctx.Data["AdditionalScopes"] = oauth2_provider.GrantAdditionalScopes(form.Scope) != auth.AccessTokenScopeAll
320+
307321
// show authorize page to grant access
308322
ctx.Data["Application"] = app
309323
ctx.Data["RedirectURI"] = form.RedirectURI

services/auth/basic.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,8 +77,8 @@ func (b *Basic) Verify(req *http.Request, w http.ResponseWriter, store DataStore
7777
log.Trace("Basic Authorization: Attempting login with username as token")
7878
}
7979

80-
// check oauth2 token
81-
uid := CheckOAuthAccessToken(req.Context(), authToken)
80+
// get oauth2 token's user's ID
81+
_, uid := GetOAuthAccessTokenScopeAndUserID(req.Context(), authToken)
8282
if uid != 0 {
8383
log.Trace("Basic Authorization: Valid OAuthAccessToken for user[%d]", uid)
8484

services/auth/oauth2.go

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -26,33 +26,35 @@ var (
2626
_ Method = &OAuth2{}
2727
)
2828

29-
// CheckOAuthAccessToken returns uid of user from oauth token
30-
func CheckOAuthAccessToken(ctx context.Context, accessToken string) int64 {
29+
// GetOAuthAccessTokenScopeAndUserID returns access token scope and user id
30+
func GetOAuthAccessTokenScopeAndUserID(ctx context.Context, accessToken string) (auth_model.AccessTokenScope, int64) {
31+
var accessTokenScope auth_model.AccessTokenScope
3132
if !setting.OAuth2.Enabled {
32-
return 0
33+
return accessTokenScope, 0
3334
}
3435

3536
// JWT tokens require a ".", if the token isn't like that, return early
3637
if !strings.Contains(accessToken, ".") {
37-
return 0
38+
return accessTokenScope, 0
3839
}
3940

4041
token, err := oauth2_provider.ParseToken(accessToken, oauth2_provider.DefaultSigningKey)
4142
if err != nil {
4243
log.Trace("oauth2.ParseToken: %v", err)
43-
return 0
44+
return accessTokenScope, 0
4445
}
4546
var grant *auth_model.OAuth2Grant
4647
if grant, err = auth_model.GetOAuth2GrantByID(ctx, token.GrantID); err != nil || grant == nil {
47-
return 0
48+
return accessTokenScope, 0
4849
}
4950
if token.Kind != oauth2_provider.KindAccessToken {
50-
return 0
51+
return accessTokenScope, 0
5152
}
5253
if token.ExpiresAt.Before(time.Now()) || token.IssuedAt.After(time.Now()) {
53-
return 0
54+
return accessTokenScope, 0
5455
}
55-
return grant.UserID
56+
accessTokenScope = oauth2_provider.GrantAdditionalScopes(grant.Scope)
57+
return accessTokenScope, grant.UserID
5658
}
5759

5860
// CheckTaskIsRunning verifies that the TaskID corresponds to a running task
@@ -120,10 +122,10 @@ func (o *OAuth2) userIDFromToken(ctx context.Context, tokenSHA string, store Dat
120122
}
121123

122124
// Otherwise, check if this is an OAuth access token
123-
uid := CheckOAuthAccessToken(ctx, tokenSHA)
125+
accessTokenScope, uid := GetOAuthAccessTokenScopeAndUserID(ctx, tokenSHA)
124126
if uid != 0 {
125127
store.GetData()["IsApiToken"] = true
126-
store.GetData()["ApiTokenScope"] = auth_model.AccessTokenScopeAll // fallback to all
128+
store.GetData()["ApiTokenScope"] = accessTokenScope
127129
}
128130
return uid
129131
}

services/oauth2_provider/access_token.go

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ package oauth2_provider //nolint
66
import (
77
"context"
88
"fmt"
9+
"slices"
10+
"strings"
911

1012
auth "code.gitea.io/gitea/models/auth"
1113
"code.gitea.io/gitea/models/db"
@@ -69,6 +71,32 @@ type AccessTokenResponse struct {
6971
IDToken string `json:"id_token,omitempty"`
7072
}
7173

74+
// GrantAdditionalScopes returns valid scopes coming from grant
75+
func GrantAdditionalScopes(grantScopes string) auth.AccessTokenScope {
76+
// scopes_supported from templates/user/auth/oidc_wellknown.tmpl
77+
scopesSupported := []string{
78+
"openid",
79+
"profile",
80+
"email",
81+
"groups",
82+
}
83+
84+
var tokenScopes []string
85+
for _, tokenScope := range strings.Split(grantScopes, " ") {
86+
if slices.Index(scopesSupported, tokenScope) == -1 {
87+
tokenScopes = append(tokenScopes, tokenScope)
88+
}
89+
}
90+
91+
// since version 1.22, access tokens grant full access to the API
92+
// 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
96+
}
97+
return auth.AccessTokenScopeAll
98+
}
99+
72100
func NewAccessTokenResponse(ctx context.Context, grant *auth.OAuth2Grant, serverKey, clientKey JWTSigningKey) (*AccessTokenResponse, *AccessTokenError) {
73101
if setting.OAuth2.InvalidateRefreshTokens {
74102
if err := grant.IncreaseCounter(ctx); err != nil {
@@ -161,7 +189,13 @@ func NewAccessTokenResponse(ctx context.Context, grant *auth.OAuth2Grant, server
161189
idToken.EmailVerified = user.IsActive
162190
}
163191
if grant.ScopeContains("groups") {
164-
groups, err := GetOAuthGroupsForUser(ctx, user)
192+
accessTokenScope := GrantAdditionalScopes(grant.Scope)
193+
194+
// since version 1.22 does not verify if groups should be public-only,
195+
// onlyPublicGroups will be set only if 'public-only' is included in a valid scope
196+
onlyPublicGroups, _ := accessTokenScope.PublicOnly()
197+
198+
groups, err := GetOAuthGroupsForUser(ctx, user, onlyPublicGroups)
165199
if err != nil {
166200
log.Error("Error getting groups: %v", err)
167201
return nil, &AccessTokenError{
@@ -192,10 +226,10 @@ func NewAccessTokenResponse(ctx context.Context, grant *auth.OAuth2Grant, server
192226

193227
// returns a list of "org" and "org:team" strings,
194228
// that the given user is a part of.
195-
func GetOAuthGroupsForUser(ctx context.Context, user *user_model.User) ([]string, error) {
229+
func GetOAuthGroupsForUser(ctx context.Context, user *user_model.User, onlyPublicGroups bool) ([]string, error) {
196230
orgs, err := db.Find[org_model.Organization](ctx, org_model.FindOrgOptions{
197231
UserID: user.ID,
198-
IncludePrivate: true,
232+
IncludePrivate: !onlyPublicGroups,
199233
})
200234
if err != nil {
201235
return nil, fmt.Errorf("GetUserOrgList: %w", err)
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// Copyright 2024 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package oauth2_provider //nolint
5+
6+
import (
7+
"testing"
8+
9+
"github.com/stretchr/testify/assert"
10+
)
11+
12+
func TestGrantAdditionalScopes(t *testing.T) {
13+
tests := []struct {
14+
grantScopes string
15+
expectedScopes string
16+
}{
17+
{"openid profile email", "all"},
18+
{"openid profile email groups", "all"},
19+
{"openid profile email all", "all"},
20+
{"openid profile email read:user all", "all"},
21+
{"openid profile email groups read:user", "read:user"},
22+
{"read:user read:repository", "read:repository,read:user"},
23+
{"read:user write:issue public-only", "public-only,write:issue,read:user"},
24+
{"openid profile email read:user", "read:user"},
25+
{"read:invalid_scope", "all"},
26+
{"read:invalid_scope,write:scope_invalid,just-plain-wrong", "all"},
27+
}
28+
29+
for _, test := range tests {
30+
t.Run(test.grantScopes, func(t *testing.T) {
31+
result := GrantAdditionalScopes(test.grantScopes)
32+
assert.Equal(t, test.expectedScopes, string(result))
33+
})
34+
}
35+
}

templates/user/auth/grant.tmpl

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,11 @@
88
<div class="ui attached segment">
99
{{template "base/alert" .}}
1010
<p>
11+
{{if not .AdditionalScopes}}
1112
<b>{{ctx.Locale.Tr "auth.authorize_application_description"}}</b><br>
12-
{{ctx.Locale.Tr "auth.authorize_application_created_by" .ApplicationCreatorLinkHTML}}
13+
{{end}}
14+
{{ctx.Locale.Tr "auth.authorize_application_created_by" .ApplicationCreatorLinkHTML}}<br>
15+
{{ctx.Locale.Tr "auth.authorize_application_with_scopes" (HTMLFormat "<b>%s</b>" .Scope)}}
1316
</p>
1417
</div>
1518
<div class="ui attached segment">

0 commit comments

Comments
 (0)