Skip to content

Commit

Permalink
Add scope filtering support for jwt.Parser/jwt.CachingParser
Browse files Browse the repository at this point in the history
  • Loading branch information
vasayxtx committed Dec 13, 2024
1 parent 8e54d4a commit b55d668
Show file tree
Hide file tree
Showing 3 changed files with 83 additions and 7 deletions.
12 changes: 12 additions & 0 deletions auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ func NewJWTParser(cfg *Config, opts ...JWTParserOption) (JWTParser, error) {
TrustedIssuerNotFoundFallback: options.trustedIssuerNotFoundFallback,
LoggerProvider: options.loggerProvider,
ClaimsTemplate: options.claimsTemplate,
ScopeFilter: options.scopeFilter,
}

if cfg.JWT.ClaimsCache.Enabled {
Expand Down Expand Up @@ -90,6 +91,7 @@ type jwtParserOptions struct {
prometheusLibInstanceLabel string
trustedIssuerNotFoundFallback jwt.TrustedIssNotFoundFallback
claimsTemplate jwt.Claims
scopeFilter jwt.ScopeFilter
}

// JWTParserOption is an option for creating JWTParser.
Expand Down Expand Up @@ -123,6 +125,16 @@ func WithJWTParserClaimsTemplate(claimsTemplate jwt.Claims) JWTParserOption {
}
}

// WithJWTParserScopeFilter sets the scope filter for JWTParser.
// If it's used, then only access policies in scope that match at least one of the filtering policies will be returned.
// It's useful when the claims cache is used (cfg.JWT.ClaimsCache.Enabled is true),
// and we want to store only some of the access policies in the cache to reduce memory usage.
func WithJWTParserScopeFilter(scopeFilter jwt.ScopeFilter) JWTParserOption {
return func(options *jwtParserOptions) {
options.scopeFilter = scopeFilter
}
}

// NewTokenIntrospector creates a new TokenIntrospector with the given configuration, token provider and scope filter.
// If cfg.Introspection.ClaimsCache.Enabled or cfg.Introspection.NegativeCache.Enabled is true,
// then idptoken.CachingIntrospector created, otherwise - idptoken.Introspector.
Expand Down
44 changes: 42 additions & 2 deletions auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,12 @@ func TestNewJWTParser(t *gotesting.T) {
Issuer: idpSrv.URL(),
ExpiresAt: jwtgo.NewNumericDate(time.Now().Add(10 * time.Second)),
},
Scope: []jwt.AccessPolicy{{ResourceNamespace: "my-service", Role: "ro_admin"}},
Scope: []jwt.AccessPolicy{
{ResourceNamespace: "my-service", Role: "ro_admin"},
{ResourceNamespace: "other-service", Role: "ro_admin"},
{ResourceNamespace: "my-service", Role: "admin"},
{ResourceNamespace: "other-service", Role: "admin"},
},
}
token := idptest.MustMakeTokenStringSignedWithTestKey(claims)

Expand All @@ -66,6 +71,7 @@ func TestNewJWTParser(t *gotesting.T) {
name string
token string
cfg *Config
opts []JWTParserOption
expectedClaims jwt.Claims
checkFn func(t *gotesting.T, jwtParser JWTParser)
}{
Expand Down Expand Up @@ -109,10 +115,44 @@ func TestNewJWTParser(t *gotesting.T) {
require.Equal(t, 1, cachingParser.ClaimsCache.Len())
},
},
{
name: "new jwt parser with scope filter",
cfg: &Config{JWT: JWTConfig{TrustedIssuerURLs: []string{idpSrv.URL()}}},
token: token,
expectedClaims: &jwt.DefaultClaims{
RegisteredClaims: claims.RegisteredClaims,
Scope: []jwt.AccessPolicy{
{ResourceNamespace: "my-service", Role: "ro_admin"},
{ResourceNamespace: "my-service", Role: "admin"},
},
},
opts: []JWTParserOption{WithJWTParserScopeFilter(jwt.ScopeFilter{{ResourceNamespace: "my-service"}})},
checkFn: func(t *gotesting.T, jwtParser JWTParser) {
require.IsType(t, &jwt.Parser{}, jwtParser)
},
},
{
name: "new caching jwt parser with scope filter",
cfg: &Config{JWT: JWTConfig{TrustedIssuerURLs: []string{idpSrv.URL()}, ClaimsCache: ClaimsCacheConfig{Enabled: true}}},
token: token,
expectedClaims: &jwt.DefaultClaims{
RegisteredClaims: claims.RegisteredClaims,
Scope: []jwt.AccessPolicy{
{ResourceNamespace: "other-service", Role: "ro_admin"},
{ResourceNamespace: "other-service", Role: "admin"},
},
},
opts: []JWTParserOption{WithJWTParserScopeFilter(jwt.ScopeFilter{{ResourceNamespace: "other-service"}})},
checkFn: func(t *gotesting.T, jwtParser JWTParser) {
require.IsType(t, &jwt.CachingParser{}, jwtParser)
cachingParser := jwtParser.(*jwt.CachingParser)
require.Equal(t, 1, cachingParser.ClaimsCache.Len())
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *gotesting.T) {
jwtParser, err := NewJWTParser(tt.cfg)
jwtParser, err := NewJWTParser(tt.cfg, tt.opts...)
require.NoError(t, err)

parsedClaims, err := jwtParser.Parse(context.Background(), tt.token)
Expand Down
34 changes: 29 additions & 5 deletions jwt/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,31 @@ type CachingKeysProvider interface {

// ParserOpts additional options for parser.
type ParserOpts struct {
SkipClaimsValidation bool
RequireAudience bool
ExpectedAudience []string
// SkipClaimsValidation is a flag that indicates whether claims validation (e.g. checking expiration time) should be skipped.
// It doesn't affect signature verification.
SkipClaimsValidation bool

// RequireAudience is a flag that indicates whether audience should be required.
RequireAudience bool

// ExpectedAudience is a list of expected audience patterns.
// If it's set, then only tokens with audience that matches at least one of the patterns will be accepted.
ExpectedAudience []string

// TrustedIssuerNotFoundFallback is a function called when given issuer is not found in the list of trusted ones.
TrustedIssuerNotFoundFallback TrustedIssNotFoundFallback
LoggerProvider func(ctx context.Context) log.FieldLogger
ClaimsTemplate Claims

// LoggerProvider is a function that provides a logger for the Parser.
LoggerProvider func(ctx context.Context) log.FieldLogger

// ClaimsTemplate is a template for claims object that will be used for unmarshalling JWT.
// By default, DefaultClaims is used.
ClaimsTemplate Claims

// ScopeFilter is a filter that will be applied to access policies in JWT scope after parsing.
// If it's set, then only access policies in scope that match at least one of the filtering policies will be returned.
// It's useful when the CachingParser is used, and we want to store only some of the access policies in the cache to reduce memory usage.
ScopeFilter ScopeFilter
}

type audienceMatcher func(aud string) bool
Expand All @@ -58,6 +77,8 @@ type Parser struct {
trustedIssuerNotFoundFallback TrustedIssNotFoundFallback

loggerProvider func(ctx context.Context) log.FieldLogger

scopeFilter ScopeFilter
}

// NewParser creates new JWT parser with specified keys provider.
Expand Down Expand Up @@ -88,6 +109,7 @@ func NewParserWithOpts(keysProvider KeysProvider, opts ParserOpts) *Parser {
trustedIssuerNotFoundFallback: opts.TrustedIssuerNotFoundFallback,
loggerProvider: opts.LoggerProvider,
claimsTemplate: claimsTemplate,
scopeFilter: opts.ScopeFilter,
}
}

Expand Down Expand Up @@ -148,6 +170,8 @@ func (p *Parser) Parse(ctx context.Context, token string) (Claims, error) {
}
}

claims.ApplyScopeFilter(p.scopeFilter)

return claims, nil
}

Expand Down

0 comments on commit b55d668

Please sign in to comment.