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`
This commit is contained in:
		
							parent
							
								
									3301e7dc75
								
							
						
					
					
						commit
						4eb8d8c496
					
				
					 4 changed files with 76 additions and 26 deletions
				
			
		| 
						 | 
				
			
			@ -100,6 +100,7 @@ var OAuth2 = struct {
 | 
			
		|||
	JWTSigningPrivateKeyFile    string `ini:"JWT_SIGNING_PRIVATE_KEY_FILE"`
 | 
			
		||||
	MaxTokenLength              int
 | 
			
		||||
	DefaultApplications         []string
 | 
			
		||||
	EnableAdditionalGrantScopes bool
 | 
			
		||||
}{
 | 
			
		||||
	Enabled:                     true,
 | 
			
		||||
	AccessTokenExpirationTime:   3600,
 | 
			
		||||
| 
						 | 
				
			
			@ -109,6 +110,7 @@ var OAuth2 = struct {
 | 
			
		|||
	JWTSigningPrivateKeyFile:    "jwt/private.pem",
 | 
			
		||||
	MaxTokenLength:              math.MaxInt16,
 | 
			
		||||
	DefaultApplications:         []string{"git-credential-oauth", "git-credential-manager", "tea"},
 | 
			
		||||
	EnableAdditionalGrantScopes: false,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func loadOAuth2From(rootCfg ConfigProvider) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -110,5 +110,6 @@ func loadApplicationsData(ctx *context.Context) {
 | 
			
		|||
			ctx.ServerError("GetOAuth2GrantsByUserID", err)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		ctx.Data["EnableAdditionalGrantScopes"] = setting.OAuth2.EnableAdditionalGrantScopes
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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,11 +134,16 @@ 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
 | 
			
		||||
			if grantScopes != "" {
 | 
			
		||||
				store.GetData()["ApiTokenScope"] = auth_model.AccessTokenScope(grantScopes)
 | 
			
		||||
			} else {
 | 
			
		||||
				store.GetData()["ApiTokenScope"] = auth_model.AccessTokenScopeAll // fallback to all
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		return uid
 | 
			
		||||
	}
 | 
			
		||||
	t, err := auth_model.GetAccessTokenBySHA(ctx, tokenSHA)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue