Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add scope filtering support for jwt.Parser/jwt.CachingParser #26

Merged
merged 1 commit into from
Dec 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading