Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to increase RemoteJWKSet cache TTL in spring-security 5.2

We are using spring-security 5.2 for securing our REST API through JWT validation.

With the spring:security:oauth2:resourceserver:jwt:jwk-set-uri property we indicate the remote JWKS endpoint which translates into Spring creating a NimbusJwtDecoder based on this URI. Further down, a RemoteJWKSet object is created that caches the calls to the JWKS endpoint with a default TTL to 5 minutes.

Is there a way to increase this TTL to minimise the remote calls ? Maybe injecting a new DefaultJWKSetCache instance somewhere with a different TTL ? It seems safe to keep this in cache for as long as possible because when we receive a token with an unknown kid, the call to the JWKS endpoint will be resumed to update the key set.

The call stack for retrieving the key is bellow

JwtAuthenticationProvider
  public Authentication authenticate(Authentication authentication)
    ...
      jwt = this.jwtDecoder.decode(bearer.getToken())
    ...

o.s.security.oauth2.jwt.NimbusJwtDecoder
    public Jwt decode(String token)
    ...
      Jwt createdJwt = createJwt(token, jwt);
    ...

    private Jwt createJwt(String token, JWT parsedJwt)
    ...
      JWTClaimsSet jwtClaimsSet = this.jwtProcessor.process(parsedJwt, null);
    ....

DefaultJWTProcessor
      public JWTClaimsSet process(final JWT jwt, final C context)
        ...
          if (jwt instanceof SignedJWT) {
                return process((SignedJWT)jwt, context);
                }
        ...

      public JWTClaimsSet process(final SignedJWT signedJWT, final C context)
            ...
              List<? extends Key> keyCandidates = selectKeys(signedJWT.getHeader(), claimsSet, context);
          ...

      private List<? extends Key> selectKeys(final JWSHeader header, final JWTClaimsSet claimsSet, final C context)
        ....
          if (getJWSKeySelector() != null) {
                 return getJWSKeySelector().selectJWSKeys(header, context);
                 }      
        ....  


JWSVerificationKeySelector
  public List<Key> selectJWSKeys(final JWSHeader jwsHeader, final C context)
    ...
      List<JWK> jwkMatches = getJWKSource().get(new JWKSelector(jwkMatcher), context);
    ...

RemoteJWKSet
  public List<JWK> get(final JWKSelector jwkSelector, final C context)
  ...
    JWKSet jwkSet = jwkSetCache.get();
        if (jwkSet == null) {
            jwkSet = updateJWKSetFromURL();
        }
  ...


DefaultJWKSetCache  
  public JWKSet get() {

    if (isExpired()) {
      jwkSet = null; // clear
    }

    return jwkSet;
  }

Security dependencies:

+- org.springframework.boot:spring-boot-starter-security:jar:2.2.4.RELEASE:compile
|  +- org.springframework.security:spring-security-config:jar:5.2.1.RELEASE:compile
|  \- org.springframework.security:spring-security-web:jar:5.2.1.RELEASE:compile
+- org.springframework.security:spring-security-oauth2-jose:jar:5.2.2.RELEASE:compile
|  +- org.springframework.security:spring-security-core:jar:5.2.1.RELEASE:compile
|  \- org.springframework.security:spring-security-oauth2-core:jar:5.2.1.RELEASE:compile
+- com.nimbusds:nimbus-jose-jwt:jar:8.8:compile
|  +- com.github.stephenc.jcip:jcip-annotations:jar:1.0-1:compile
|  \- net.minidev:json-smart:jar:2.3:compile (version selected from constraint [1.3.1,2.3])
|     \- net.minidev:accessors-smart:jar:1.2:compile
|        \- org.ow2.asm:asm:jar:5.0.4:compile
+- org.springframework.security:spring-security-oauth2-resource-server:jar:5.2.1.RELEASE:compile
like image 832
chirina Avatar asked Feb 26 '20 08:02

chirina


2 Answers

I ended up doing the following:

    @Bean
    public JwtDecoder jwtDecoder() {
        JWSKeySelector<SecurityContext> jwsKeySelector = null;
        try {
            URL jwksUrl = new URL("https://localhost/.well-known/openid-configuration/jwks");
            long cacheLifespan = 500;
            long refreshTime = 400;
            JWKSetCache jwkSetCache = new DefaultJWKSetCache(cacheLifespan, refreshTime, TimeUnit.MINUTES);
            RemoteJWKSet<SecurityContext> jwkSet = new RemoteJWKSet<>(jwksUrl,null,jwkSetCache);
            jwsKeySelector = JWSAlgorithmFamilyJWSKeySelector.fromJWKSource(jwkSet);
        }
        catch (KeySourceException e) {
            e.printStackTrace();
        }
        catch (MalformedURLException e) {
            e.printStackTrace();
        }

        DefaultJWTProcessor<SecurityContext> jwtProcessor = new DefaultJWTProcessor<>();
        jwtProcessor.setJWSKeySelector(jwsKeySelector);

        return new NimbusJwtDecoder(jwtProcessor);
    }
like image 191
Michael R Avatar answered Sep 17 '22 07:09

Michael R


Looks like I'm a bit late to the party, but I was the one to implement this feature for 5.4 release and now you're able to configure it with Spring Cache:

var jwkSetCache = new ConcurrentMapCache("jwkSetCache", CacheBuilder.newBuilder()
    // can set the value here or better populate from properties
    .expireAfterWrite(Duration.ofMinutes(30))
    .build().asMap(), false);
var decoder = NimbusJwtDecoder.withJwkSetUri(jwkSetUri)
    .restOperations(restOperations)
    .cache(jwkSetCache)
    .build();
like image 22
Tyulpan Tyulpan Avatar answered Sep 19 '22 07:09

Tyulpan Tyulpan