Skip to content

Commit

Permalink
Merge pull request #2 from vasayxtx/introspection-nri-jwt-header
Browse files Browse the repository at this point in the history
Determine whether the JWT should be introspected or not by "nri" header
  • Loading branch information
MikeYast authored Oct 3, 2024
2 parents 69ae54b + adedb9d commit 0ecc08d
Show file tree
Hide file tree
Showing 6 changed files with 43 additions and 49 deletions.
1 change: 0 additions & 1 deletion auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,6 @@ func NewTokenIntrospector(
HTTPClient: &http.Client{Timeout: httpClientRequestTimeout},
AccessTokenScope: cfg.Introspection.AccessTokenScope,
Logger: logger,
MinJWTVersion: cfg.Introspection.MinJWTVersion,
ScopeFilter: scopeFilter,
TrustedIssuerNotFoundFallback: options.trustedIssuerNotFoundFallback,
PrometheusLibInstanceLabel: options.prometheusLibInstanceLabel,
Expand Down
14 changes: 0 additions & 14 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ const (
cfgKeyIntrospectionGRPCTLSClientCert = "auth.introspection.grpc.tls.clientCert"
cfgKeyIntrospectionGRPCTLSClientKey = "auth.introspection.grpc.tls.clientKey"
cfgKeyIntrospectionAccessTokenScope = "auth.introspection.accessTokenScope" // nolint:gosec // false positive
cfgKeyIntrospectionMinJWTVer = "auth.introspection.minJWTVersion"
cfgKeyIntrospectionClaimsCacheEnabled = "auth.introspection.claimsCache.enabled"
cfgKeyIntrospectionClaimsCacheMaxEntries = "auth.introspection.claimsCache.maxEntries"
cfgKeyIntrospectionClaimsCacheTTL = "auth.introspection.claimsCache.ttl"
Expand Down Expand Up @@ -68,11 +67,6 @@ type IntrospectionConfig struct {
Endpoint string
AccessTokenScope []string

// MinJWTVersion is a minimum version of JWT that will be accepted for introspection.
// NOTE: it's a temporary solution for determining whether introspection is needed or not,
// and it will be removed in the future.
MinJWTVersion int

ClaimsCache IntrospectionCacheConfig
NegativeCache IntrospectionCacheConfig

Expand Down Expand Up @@ -152,7 +146,6 @@ func (c *Config) SetProviderDefaults(dp config.DataProvider) {
dp.SetDefault(cfgKeyGRPCClientRequestTimeout, DefaultGRPCClientRequestTimeout.String())
dp.SetDefault(cfgKeyJWTClaimsCacheMaxEntries, jwt.DefaultClaimsCacheMaxEntries)
dp.SetDefault(cfgKeyJWKSCacheUpdateMinInterval, jwks.DefaultCacheUpdateMinInterval.String())
dp.SetDefault(cfgKeyIntrospectionMinJWTVer, idptoken.MinJWTVersionForIntrospection)
dp.SetDefault(cfgKeyIntrospectionClaimsCacheMaxEntries, idptoken.DefaultIntrospectionClaimsCacheMaxEntries)
dp.SetDefault(cfgKeyIntrospectionClaimsCacheTTL, idptoken.DefaultIntrospectionClaimsCacheTTL.String())
dp.SetDefault(cfgKeyIntrospectionNegativeCacheMaxEntries, idptoken.DefaultIntrospectionNegativeCacheMaxEntries)
Expand Down Expand Up @@ -257,13 +250,6 @@ func (c *Config) setIntrospectionConfig(dp config.DataProvider) error {
return err
}

if c.Introspection.MinJWTVersion, err = dp.GetInt(cfgKeyIntrospectionMinJWTVer); err != nil {
return err
}
if c.Introspection.MinJWTVersion < 0 {
return dp.WrapKeyErr(cfgKeyIntrospectionMinJWTVer, fmt.Errorf("minimum JWT version should be non-negative"))
}

// Claims cache
if c.Introspection.ClaimsCache.Enabled, err = dp.GetBool(cfgKeyIntrospectionClaimsCacheEnabled); err != nil {
return err
Expand Down
12 changes: 0 additions & 12 deletions config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,6 @@ auth:
ttl: 77s
accessTokenScope:
- token_introspector
minJWTVersion: 3
grpc:
target: "127.0.0.1:1234"
tls:
Expand Down Expand Up @@ -102,7 +101,6 @@ auth:
TTL: time.Second * 77,
},
AccessTokenScope: []string{"token_introspector"},
MinJWTVersion: 3,
GRPC: IntrospectionGRPCConfig{
Target: "127.0.0.1:1234",
TLS: GRPCTLSConfig{
Expand Down Expand Up @@ -240,16 +238,6 @@ auth:
errKey: cfgKeyIntrospectionAccessTokenScope,
errMsg: " unable to cast",
},
{
name: "negative introspection min JWT version",
cfgData: `
auth:
introspection:
minJWTVersion: -1
`,
errKey: cfgKeyIntrospectionMinJWTVer,
errMsg: "minimum JWT version should be non-negative",
},
}

for _, tt := range tests {
Expand Down
3 changes: 1 addition & 2 deletions idptoken/grpc_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,7 @@ func NewGRPCClientWithOpts(
if opts.RequestTimeout == 0 {
opts.RequestTimeout = time.Second * 30
}
dialCtx := context.Background() // context.Background() is ok since we don't use grpc.WithBlock()
conn, err := grpc.DialContext(dialCtx, target,
conn, err := grpc.NewClient(target,
grpc.WithTransportCredentials(transportCreds),
grpc.WithStatsHandler(&statsHandler{logger: opts.Logger}),
grpc.WithDefaultCallOptions(grpc.WaitForReady(true)),
Expand Down
44 changes: 28 additions & 16 deletions idptoken/introspector.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ Released under MIT license.
package idptoken

import (
"bytes"
"context"
"encoding/json"
"errors"
Expand All @@ -32,8 +33,6 @@ const JWTTypeAccessToken = "at+jwt"

const TokenTypeBearer = "bearer"

const MinJWTVersionForIntrospection = 0

const minAccessTokenProviderInvalidationInterval = time.Minute

const tokenIntrospectorPromSource = "token_introspector"
Expand Down Expand Up @@ -95,15 +94,6 @@ type IntrospectorOpts struct {
// when given issuer from JWT is not found in the list of trusted ones.
TrustedIssuerNotFoundFallback TrustedIssNotFoundFallback

// MinJWTVersion is a minimum JWT version for introspection.
// If JWT version is less than this value, then introspection will not be done
// and ErrTokenIntrospectionNotNeeded will be returned.
// Version is a value of "ver" field in JWT header.
// By default, it is 0.
// NOTE: it's a temporary solution for determining whether introspection is needed or not,
// and it will be removed in the future.
MinJWTVersion int

// PrometheusLibInstanceLabel is a label for Prometheus metrics.
// It allows distinguishing metrics from different instances of the same library.
PrometheusLibInstanceLabel string
Expand All @@ -115,8 +105,7 @@ type Introspector struct {
accessTokenProviderInvalidatedAt atomic.Value
accessTokenScope []string

minJWTVersion int
jwtParser *jwtgo.Parser
jwtParser *jwtgo.Parser

grpcClient *GRPCClient
staticHTTPURL string
Expand Down Expand Up @@ -173,7 +162,6 @@ func NewIntrospectorWithOpts(accessTokenProvider IntrospectionTokenProvider, opt
scopeFilterFormURLEncoded: scopeFilterFormURLEncoded,
scopeFilter: opts.ScopeFilter,
staticHTTPURL: opts.StaticHTTPEndpoint,
minJWTVersion: opts.MinJWTVersion,
trustedIssuerStore: idputil.NewTrustedIssuerStore(),
trustedIssuerNotFoundFallback: opts.TrustedIssuerNotFoundFallback,
promMetrics: promMetrics,
Expand Down Expand Up @@ -234,14 +222,16 @@ func (i *Introspector) makeIntrospectFuncForToken(ctx context.Context, token str
if jwtHeaderBytes, err = i.jwtParser.DecodeSegment(token[:jwtHeaderEndIdx]); err != nil {
return i.makeStaticIntrospectFuncOrError(fmt.Errorf("decode JWT header: %w", err))
}
headerDecoder := json.NewDecoder(bytes.NewReader(jwtHeaderBytes))
headerDecoder.UseNumber()
jwtHeader := make(map[string]interface{})
if err = json.Unmarshal(jwtHeaderBytes, &jwtHeader); err != nil {
if err = headerDecoder.Decode(&jwtHeader); err != nil {
return i.makeStaticIntrospectFuncOrError(fmt.Errorf("unmarshal JWT header: %w", err))
}
if typ, ok := jwtHeader["typ"].(string); !ok || !strings.EqualFold(typ, JWTTypeAccessToken) {
return i.makeStaticIntrospectFuncOrError(fmt.Errorf("token type is not %s", JWTTypeAccessToken))
}
if ver, ok := jwtHeader["ver"].(float64); ok && int(ver) < i.minJWTVersion {
if !checkIntrospectionRequiredByJWTHeader(jwtHeader) {
return nil, ErrTokenIntrospectionNotNeeded
}

Expand Down Expand Up @@ -400,3 +390,25 @@ func makeTokenNotIntrospectableError(inner error) error {
func makeBearerToken(token string) string {
return "Bearer " + token
}

// checkIntrospectionRequiredByJWTHeader checks if introspection is required by JWT header.
// Introspection is required by default.
func checkIntrospectionRequiredByJWTHeader(jwtHeader map[string]interface{}) bool {
notRequiredIntrospection, ok := jwtHeader["nri"]
if !ok {
return true
}
var bVal bool
if bVal, ok = notRequiredIntrospection.(bool); ok {
return !bVal
}
var nVal json.Number
if nVal, ok = notRequiredIntrospection.(json.Number); ok {
iVal, err := nVal.Int64()
if err != nil {
return true
}
return iVal == 0
}
return true
}
18 changes: 14 additions & 4 deletions idptoken/introspector_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -153,17 +153,27 @@ func TestIntrospector_IntrospectToken(t *gotesting.T) {
},
},
{
name: "error, dynamic introspection endpoint, introspection is not needed",
introspectorOpts: idptoken.IntrospectorOpts{
MinJWTVersion: 3,
name: "error, dynamic introspection endpoint, nri is 1",
token: idptest.MustMakeTokenStringWithHeader(jwt.Claims{
RegisteredClaims: jwtgo.RegisteredClaims{
Subject: uuid.NewString(),
ID: uuid.NewString(),
ExpiresAt: jwtgo.NewNumericDate(time.Now().Add(time.Hour)),
},
}, idptest.TestKeyID, idptest.GetTestRSAPrivateKey(), map[string]interface{}{"nri": 1}),
checkError: func(t *gotesting.T, err error) {
require.ErrorIs(t, err, idptoken.ErrTokenIntrospectionNotNeeded)
},
},
{
name: "error, dynamic introspection endpoint, nri is true",
token: idptest.MustMakeTokenStringWithHeader(jwt.Claims{
RegisteredClaims: jwtgo.RegisteredClaims{
Subject: uuid.NewString(),
ID: uuid.NewString(),
ExpiresAt: jwtgo.NewNumericDate(time.Now().Add(time.Hour)),
},
}, idptest.TestKeyID, idptest.GetTestRSAPrivateKey(), map[string]interface{}{"ver": 2}),
}, idptest.TestKeyID, idptest.GetTestRSAPrivateKey(), map[string]interface{}{"nri": true}),
checkError: func(t *gotesting.T, err error) {
require.ErrorIs(t, err, idptoken.ErrTokenIntrospectionNotNeeded)
},
Expand Down

0 comments on commit 0ecc08d

Please sign in to comment.