Skip to content

Commit fb20f58

Browse files
committed
oauth2 access token granular scope
1 parent 32456b6 commit fb20f58

File tree

5 files changed

+63
-17
lines changed

5 files changed

+63
-17
lines changed

routers/web/auth/oauth2_provider.go

+12-1
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,15 @@ 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+
onlyPublicGroups, _ := accessTokenScope.PublicOnly()
115+
groups, err := oauth2_provider.GetOAuthGroupsForUser(ctx, ctx.Doer, onlyPublicGroups)
108116
if err != nil {
109117
ctx.ServerError("Oauth groups for user", err)
110118
return
@@ -304,6 +312,9 @@ func AuthorizeOAuth(ctx *context.Context) {
304312
return
305313
}
306314

315+
// check if additional scopes
316+
ctx.Data["AdditionalScopes"] = oauth2_provider.GrantAdditionalScopes(form.Scope) != auth.AccessTokenScopeAll
317+
307318
// show authorize page to grant access
308319
ctx.Data["Application"] = app
309320
ctx.Data["RedirectURI"] = form.RedirectURI

services/auth/basic.go

+2-2
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

+14-11
Original file line numberDiff line numberDiff line change
@@ -25,33 +25,35 @@ var (
2525
_ Method = &OAuth2{}
2626
)
2727

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

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

3940
token, err := oauth2_provider.ParseToken(accessToken, oauth2_provider.DefaultSigningKey)
4041
if err != nil {
4142
log.Trace("oauth2.ParseToken: %v", err)
42-
return 0
43+
return accessTokenScope, 0
4344
}
4445
var grant *auth_model.OAuth2Grant
4546
if grant, err = auth_model.GetOAuth2GrantByID(ctx, token.GrantID); err != nil || grant == nil {
46-
return 0
47+
return accessTokenScope, 0
4748
}
4849
if token.Kind != oauth2_provider.KindAccessToken {
49-
return 0
50+
return accessTokenScope, 0
5051
}
5152
if token.ExpiresAt.Before(time.Now()) || token.IssuedAt.After(time.Now()) {
52-
return 0
53+
return accessTokenScope, 0
5354
}
54-
return grant.UserID
55+
accessTokenScope = oauth2_provider.GrantAdditionalScopes(grant.Scope)
56+
return accessTokenScope, grant.UserID
5557
}
5658

5759
// OAuth2 implements the Auth interface and authenticates requests
@@ -97,10 +99,11 @@ func parseToken(req *http.Request) (string, bool) {
9799
func (o *OAuth2) userIDFromToken(ctx context.Context, tokenSHA string, store DataStore) int64 {
98100
// Let's see if token is valid.
99101
if strings.Contains(tokenSHA, ".") {
100-
uid := CheckOAuthAccessToken(ctx, tokenSHA)
102+
accessTokenScope, uid := GetOAuthAccessTokenScopeAndUserID(ctx, tokenSHA)
103+
101104
if uid != 0 {
102105
store.GetData()["IsApiToken"] = true
103-
store.GetData()["ApiTokenScope"] = auth_model.AccessTokenScopeAll // fallback to all
106+
store.GetData()["ApiTokenScope"] = accessTokenScope
104107
}
105108
return uid
106109
}

services/oauth2_provider/access_token.go

+32-3
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,30 @@ 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+
accessTokenScope := auth.AccessTokenScope(strings.Join(tokenScopes, ","))
92+
if accessTokenWithAdditionalScopes, err := accessTokenScope.Normalize(); err == nil && len(tokenScopes) > 0 {
93+
return accessTokenWithAdditionalScopes
94+
}
95+
return auth.AccessTokenScopeAll
96+
}
97+
7298
func NewAccessTokenResponse(ctx context.Context, grant *auth.OAuth2Grant, serverKey, clientKey JWTSigningKey) (*AccessTokenResponse, *AccessTokenError) {
7399
if setting.OAuth2.InvalidateRefreshTokens {
74100
if err := grant.IncreaseCounter(ctx); err != nil {
@@ -161,7 +187,10 @@ func NewAccessTokenResponse(ctx context.Context, grant *auth.OAuth2Grant, server
161187
idToken.EmailVerified = user.IsActive
162188
}
163189
if grant.ScopeContains("groups") {
164-
groups, err := GetOAuthGroupsForUser(ctx, user)
190+
accessTokenScope := GrantAdditionalScopes(grant.Scope)
191+
onlyPublicGroups, _ := accessTokenScope.PublicOnly()
192+
193+
groups, err := GetOAuthGroupsForUser(ctx, user, onlyPublicGroups)
165194
if err != nil {
166195
log.Error("Error getting groups: %v", err)
167196
return nil, &AccessTokenError{
@@ -192,10 +221,10 @@ func NewAccessTokenResponse(ctx context.Context, grant *auth.OAuth2Grant, server
192221

193222
// returns a list of "org" and "org:team" strings,
194223
// that the given user is a part of.
195-
func GetOAuthGroupsForUser(ctx context.Context, user *user_model.User) ([]string, error) {
224+
func GetOAuthGroupsForUser(ctx context.Context, user *user_model.User, onlyPublicGroups bool) ([]string, error) {
196225
orgs, err := db.Find[org_model.Organization](ctx, org_model.FindOrgOptions{
197226
UserID: user.ID,
198-
IncludePrivate: true,
227+
IncludePrivate: !onlyPublicGroups,
199228
})
200229
if err != nil {
201230
return nil, fmt.Errorf("GetUserOrgList: %w", err)

templates/user/auth/grant.tmpl

+3
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>
13+
{{end}}
1214
{{ctx.Locale.Tr "auth.authorize_application_created_by" .ApplicationCreatorLinkHTML}}
15+
<p>{{ctx.Locale.Tr "auth.authorize_application_with_scopes" .Scope}}</p>
1316
</p>
1417
</div>
1518
<div class="ui attached segment">

0 commit comments

Comments
 (0)