How to increase RemoteJWKSet cache TTL in spring-security 5.2
Asked Answered
N

4

11

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
Nilsanilsen answered 26/2, 2020 at 8:31 Comment(5)
Is the source code for this version on Github? Couldn't find it...Groschen
The sources for nimbus-jose-jwt are on bitbucket bitbucket.org/connect2id/nimbus-jose-jwt/src/master/src/main/… I also updated the post with the dependencies version.Nilsanilsen
Hey! I actually have the same question, have you found any workarounds?Fussbudget
It is not a good idea to cache a JWK Set for a very long time. The only way to revoke a JWK in case of compromise is to remove it from the published JWK Set, and a long cache expiry time means that it will be a long time before your application notices that the key has been revoked.Hollyanne
@NeilMadden I am trying to understand about flow of token verification, what happens if user try to logout or change their password and try to hit protected API with old token and let's say resource server TTL doesn't expire?Titrate
F
10

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();
Fussbudget answered 8/4, 2021 at 12:50 Comment(3)
could you comment on this - #70049715Gabi
Note: CacheBuilder is from Guava libraryRoughish
Alex is right on CacheBuilder, in the code sample I've used simple ConcurrentMapCache, but for real application I would definitely consider better caching options like Redis (probably the best choice, assuming there are most likely multiple resource servers requesting jwks).Fussbudget
H
8

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);
    }
Harar answered 9/7, 2020 at 22:49 Comment(2)
could you comment on this - #70049715Gabi
Is there a way of creating this JwtDecoder bean dynamically? I am having multiple tenants (issuers) and If I check them in every req they won't be cached right?Metameric
S
0

Spring Security 5.4 allows to pass a cache to the decoderbuilder method. So you can pass your own cache and nimbusjwtdecoder will use that cache to get value.

For clearing cache u can have a scheduler job in your configuration.

@Scheduled(fixedRateString = "5000")
    public void clearCachesAfterEvictionTime() {
        Optional.ofNullable(cacheManager().getCache("JWKSetCache")).ifPresent(Cache::clear);
    }

Hope it helps.

Surrebuttal answered 18/10, 2022 at 14:43 Comment(0)
L
-1

Nimbus allows two ways to override default HTTP connect and read timeouts

By passing a configured ResourceRetriever, for example:

int httpConnectTimeoutMs = 5_000;
int httpReadTimeoutMs = 5_000;
int httpSizeLimitBytes = 100_000;

JWKSource<?> jwkSource = new RemoteJWKSet<>(
        new URL("https://demo.c2id.com/jwks.json"),
        new DefaultResourceRetriever(
            httpConnectTimeoutMs, httpReadTimeoutMs, httpSizeLimitBytes
        )
    );

By setting the following Java system properties (suitable when there is no direct way to construct the RemoteJWKSet, can occur in frameworks that use this library internally):

Setting a HTTP connect timeout of 5 seconds:

com.nimbusds.jose.jwk.source.RemoteJWKSet.defaultHttpConnectTimeout=5000

Setting a HTTP read timeout of 2.5 seconds:

com.nimbusds.jose.jwk.source.RemoteJWKSet.defaultHttpReadTimeout=2500

Refer to https://connect2id.com/products/nimbus-jose-jwt/examples/validating-jwt-access-tokens#remote-jwk-set-timeouts for more details

Lefty answered 19/1, 2023 at 22:9 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.