From 4eb8d8c4960b7d26679be31824f91218eba1ed55 Mon Sep 17 00:00:00 2001 From: Marcell Mars Date: Thu, 11 Jul 2024 11:12:51 +0200 Subject: [PATCH] OAuth2 provider: support for granular scopes - `CheckOAuthAccessToken` returns both user ID and additional scopes - `grantAdditionalScopes` returns AccessTokenScope ready string (grantScopes) compiled from requested additional scopes by the client - `userIDFromToken` sets returned grantScopes (if any) instead of default `all` --- modules/setting/oauth2.go | 34 +++++++------ routers/web/user/setting/applications.go | 1 + services/auth/basic.go | 2 +- services/auth/oauth2.go | 65 ++++++++++++++++++++---- 4 files changed, 76 insertions(+), 26 deletions(-) diff --git a/modules/setting/oauth2.go b/modules/setting/oauth2.go index 86617f7513..49288e2639 100644 --- a/modules/setting/oauth2.go +++ b/modules/setting/oauth2.go @@ -92,23 +92,25 @@ func parseScopes(sec ConfigSection, name string) []string { } var OAuth2 = struct { - Enabled bool - AccessTokenExpirationTime int64 - RefreshTokenExpirationTime int64 - InvalidateRefreshTokens bool - JWTSigningAlgorithm string `ini:"JWT_SIGNING_ALGORITHM"` - JWTSigningPrivateKeyFile string `ini:"JWT_SIGNING_PRIVATE_KEY_FILE"` - MaxTokenLength int - DefaultApplications []string + Enabled bool + AccessTokenExpirationTime int64 + RefreshTokenExpirationTime int64 + InvalidateRefreshTokens bool + JWTSigningAlgorithm string `ini:"JWT_SIGNING_ALGORITHM"` + JWTSigningPrivateKeyFile string `ini:"JWT_SIGNING_PRIVATE_KEY_FILE"` + MaxTokenLength int + DefaultApplications []string + EnableAdditionalGrantScopes bool }{ - Enabled: true, - AccessTokenExpirationTime: 3600, - RefreshTokenExpirationTime: 730, - InvalidateRefreshTokens: true, - JWTSigningAlgorithm: "RS256", - JWTSigningPrivateKeyFile: "jwt/private.pem", - MaxTokenLength: math.MaxInt16, - DefaultApplications: []string{"git-credential-oauth", "git-credential-manager", "tea"}, + Enabled: true, + AccessTokenExpirationTime: 3600, + RefreshTokenExpirationTime: 730, + InvalidateRefreshTokens: true, + JWTSigningAlgorithm: "RS256", + JWTSigningPrivateKeyFile: "jwt/private.pem", + MaxTokenLength: math.MaxInt16, + DefaultApplications: []string{"git-credential-oauth", "git-credential-manager", "tea"}, + EnableAdditionalGrantScopes: false, } func loadOAuth2From(rootCfg ConfigProvider) { diff --git a/routers/web/user/setting/applications.go b/routers/web/user/setting/applications.go index e3822ca988..24ebf9b922 100644 --- a/routers/web/user/setting/applications.go +++ b/routers/web/user/setting/applications.go @@ -110,5 +110,6 @@ func loadApplicationsData(ctx *context.Context) { ctx.ServerError("GetOAuth2GrantsByUserID", err) return } + ctx.Data["EnableAdditionalGrantScopes"] = setting.OAuth2.EnableAdditionalGrantScopes } } diff --git a/services/auth/basic.go b/services/auth/basic.go index c8cb1735ee..382c8bc90c 100644 --- a/services/auth/basic.go +++ b/services/auth/basic.go @@ -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) diff --git a/services/auth/oauth2.go b/services/auth/oauth2.go index 46d8510143..6a63c62796 100644 --- a/services/auth/oauth2.go +++ b/services/auth/oauth2.go @@ -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 }