1
Fork 0

Merge pull request 'Enhancing Gitea OAuth2 Provider with Granular Scopes for Resource Access' (#4449) from marcellmars/forgejo:forgejo into forgejo

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/4449
Reviewed-by: Earl Warren <earl-warren@noreply.codeberg.org>
This commit is contained in:
Earl Warren 2024-08-16 12:38:15 +00:00
commit 24eb401a0a
8 changed files with 662 additions and 30 deletions

View file

@ -0,0 +1,32 @@
package auth
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestGrantAdditionalScopes(t *testing.T) {
tests := []struct {
grantScopes string
expectedScopes string
}{
{"openid profile email", ""},
{"openid profile email groups", ""},
{"openid profile email all", "all"},
{"openid profile email read:user all", "read:user,all"},
{"openid profile email groups read:user", "read:user"},
{"read:user read:repository", "read:user,read:repository"},
{"read:user write:issue public-only", "read:user,write:issue,public-only"},
{"openid profile email read:user", "read:user"},
{"read:invalid_scope", ""},
{"read:invalid_scope,write:scope_invalid,just-plain-wrong", ""},
}
for _, test := range tests {
t.Run(test.grantScopes, func(t *testing.T) {
result := grantAdditionalScopes(test.grantScopes)
assert.Equal(t, test.expectedScopes, result)
})
}
}

View file

@ -72,7 +72,7 @@ func (b *Basic) Verify(req *http.Request, w http.ResponseWriter, store DataStore
}
// check oauth2 token
uid := CheckOAuthAccessToken(req.Context(), authToken)
uid, _ := CheckOAuthAccessToken(req.Context(), authToken)
if uid != 0 {
log.Trace("Basic Authorization: Valid OAuthAccessToken for user[%d]", uid)

View file

@ -7,6 +7,7 @@ package auth
import (
"context"
"net/http"
"slices"
"strings"
"time"
@ -25,28 +26,69 @@ var (
_ Method = &OAuth2{}
)
// grantAdditionalScopes returns valid scopes coming from grant
func grantAdditionalScopes(grantScopes string) string {
// scopes_supported from templates/user/auth/oidc_wellknown.tmpl
scopesSupported := []string{
"openid",
"profile",
"email",
"groups",
}
var apiTokenScopes []string
for _, apiTokenScope := range strings.Split(grantScopes, " ") {
if slices.Index(scopesSupported, apiTokenScope) == -1 {
apiTokenScopes = append(apiTokenScopes, apiTokenScope)
}
}
if len(apiTokenScopes) == 0 {
return ""
}
var additionalGrantScopes []string
allScopes := auth_model.AccessTokenScope("all")
for _, apiTokenScope := range apiTokenScopes {
grantScope := auth_model.AccessTokenScope(apiTokenScope)
if ok, _ := allScopes.HasScope(grantScope); ok {
additionalGrantScopes = append(additionalGrantScopes, apiTokenScope)
} else if apiTokenScope == "public-only" {
additionalGrantScopes = append(additionalGrantScopes, apiTokenScope)
}
}
if len(additionalGrantScopes) > 0 {
return strings.Join(additionalGrantScopes, ",")
}
return ""
}
// CheckOAuthAccessToken returns uid of user from oauth token
func CheckOAuthAccessToken(ctx context.Context, accessToken string) int64 {
// + non default openid scopes requested
func CheckOAuthAccessToken(ctx context.Context, accessToken string) (int64, string) {
// JWT tokens require a "."
if !strings.Contains(accessToken, ".") {
return 0
return 0, ""
}
token, err := oauth2.ParseToken(accessToken, oauth2.DefaultSigningKey)
if err != nil {
log.Trace("oauth2.ParseToken: %v", err)
return 0
return 0, ""
}
var grant *auth_model.OAuth2Grant
if grant, err = auth_model.GetOAuth2GrantByID(ctx, token.GrantID); err != nil || grant == nil {
return 0
return 0, ""
}
if token.Type != oauth2.TypeAccessToken {
return 0
return 0, ""
}
if token.ExpiresAt.Before(time.Now()) || token.IssuedAt.After(time.Now()) {
return 0
return 0, ""
}
return grant.UserID
grantScopes := grantAdditionalScopes(grant.Scope)
return grant.UserID, grantScopes
}
// OAuth2 implements the Auth interface and authenticates requests
@ -92,10 +134,15 @@ func parseToken(req *http.Request) (string, bool) {
func (o *OAuth2) userIDFromToken(ctx context.Context, tokenSHA string, store DataStore) int64 {
// Let's see if token is valid.
if strings.Contains(tokenSHA, ".") {
uid := CheckOAuthAccessToken(ctx, tokenSHA)
uid, grantScopes := CheckOAuthAccessToken(ctx, tokenSHA)
if uid != 0 {
store.GetData()["IsApiToken"] = true
store.GetData()["ApiTokenScope"] = auth_model.AccessTokenScopeAll // fallback to all
if grantScopes != "" {
store.GetData()["ApiTokenScope"] = auth_model.AccessTokenScope(grantScopes)
} else {
store.GetData()["ApiTokenScope"] = auth_model.AccessTokenScopeAll // fallback to all
}
}
return uid
}