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

pass Token to authorization.TokenKeyProvider when get keys #2382

Closed
wenerme opened this issue Jan 17, 2022 · 4 comments · Fixed by #2420
Closed

pass Token to authorization.TokenKeyProvider when get keys #2382

wenerme opened this issue Jan 17, 2022 · 4 comments · Fixed by #2420
Labels
enhancement New feature or request

Comments

@wenerme
Copy link
Contributor

wenerme commented Jan 17, 2022

Is your feature request related to a problem? Please describe.

JWT has issuer, the cert path is predictable

  1. predictable by provider - e.g. jwk for keycloak always located at <issuer>/protocol/openid-connect/certs
  2. predictable by make one more request to OpenID Endpoint (<issuer>/.well-known/openid-configuration) Configuration

benefit

  • This allowed the impl decouple from the real JWK place - e.g. only limit on domain names
  • support customized jwt auth
    • jwk url is based on namespace
    • saas like service

Describe the solution you'd like

make TokenKeyProvider more general

type TokenKeyProvider interface {
	GetKey(token *jwt.Token) (interface{}, error)
	SupportedMethods() []string
	Close()
}

Describe alternatives you've considered

Current TokenKeyProvider restricted to hmac, rsa, ecdsa, but this restriction should consider for default impl only, not for TokenKeyProvider interface.

Additional context

@wenerme wenerme added the enhancement New feature or request label Jan 17, 2022
@sergeybykov
Copy link
Member

@wenerme Thank you for opening the issue. Could you please help me understand the problem you are suggesting to solve here.

Validation of token happens of the hot path, and the expectation is that all necessary keys have already been prefetched by the token key provider (otherwise, the roundtrip of fetching a key would add to the latency of the call). Hence, the provider needs to be able to fetch those keys without knowing what JWT tokens will need to be validated. So, I'm confused why a token key provider should ever need to see a JWT token.

If token keys are frefetched and each key has a unique kid, why wouldn't be insufficient to pass an alg/kid pair to the provider to looks up the right key?

@wenerme
Copy link
Contributor Author

wenerme commented Jan 18, 2022

@sergeybykov This is my current impl

func NewFollowIssuerTokenKeyProvider() *FollowIssuerTokenKeyProvider {
	cache, err := lru.New(200)
	if err != nil {
		panic(err)
	}
	return &FollowIssuerTokenKeyProvider{cache: cache}
}

type FollowIssuerTokenKeyProvider struct {
	cache *lru.Cache
}

func (wa *FollowIssuerTokenKeyProvider) ParseWithClaims(tokenString string, claims jwt.Claims) (*jwt.Token, error) {
	return jwt.ParseWithClaims(tokenString, claims, wa.GetKey)
}

func (wa *FollowIssuerTokenKeyProvider) GetKey(token *jwt.Token) (key interface{}, err error) {
	kc, _ := token.Claims.(jwt.MapClaims)
	if kc == nil {
		return nil, errors.New("server invalid claims")
	}
	iss, _ := kc["iss"].(string)
	if iss == "" {
		return nil, errors.New("missing issuer")
	}

	// keycloak
	// /protocol/openid-connect/certs
	// common
	// /.well-known/openid-configuration
	// TODO refresh
	cv, _ := wa.cache.Get(iss)
	set, _ := cv.(jwk.Set)
	if set == nil {// for keycloak only
		jwkUrl := iss + "/protocol/openid-connect/certs"
		set, err = jwk.Fetch(context.Background(), jwkUrl)
		if err != nil {
			zap.S().With("jwk", jwkUrl, "err", err).Error("load jwk")
			return nil, errors.Wrapf(err, "load jwk: %q", jwkUrl)
		}
		zap.S().With("jwk", jwkUrl).Info("load jwk")
		wa.cache.Add(iss, set)
	}

	kid, _ := token.Header["kid"].(string)
	if kid == "" {
		keys, _ := set.Get(0)
		if keys != nil && set.Len() == 1 && keys.KeyID() == "" {
			return key, keys.Raw(&key)
		}
		return nil, errors.New("jwt header missing kid")
	}
	if keys, found := set.LookupKeyID(kid); found {
		return key, keys.Raw(&key)
	}
	return nil, fmt.Errorf("jwk key not found: %q", kid)
}

GetKey should get the current context, but GetClaims can not get context.

when GetClaims, will check the iss domain name first.

It's impossible to prefetch pubkey, issuer like *.example.com is all valid issuer, they point to different keycloak like service.

@sergeybykov
Copy link
Member

Thanks for the explanation. This would be a breaking change. Let me ask a couple of people what they think and circle back here.

@yiminc
Copy link
Member

yiminc commented Jan 22, 2022

@wenerme, the current interface is more user friendly as it is easier to deal with for most use cases. However, your use case is also a valid case. We don't want to break existing users. So proposal would be to create a separate interface like:

type RawTokenKeyProvider interface {
	GetKey(token *jwt.Token) (interface{}, error)
	SupportedMethods() []string
	Close()
}

And adjust defaultJWTClaimMapper so it can work with either interfaces. Basically keep NewDefaultJWTClaimMapper() that takes TokenKeyProvider, and add another one NewDefaultJWTClaimMapperWithRawKeyProvider() that takes RawTokenKeyProvider.
Does that sounds reasonable?
We would welcome PR for this if you are willing to contribute. :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants