10

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
chirina
  • 133
  • 2
  • 8
  • Is the source code for this version on Github? Couldn't find it... – NatFar Feb 26 '20 at 18:51
  • The sources for nimbus-jose-jwt are on bitbucket https://bitbucket.org/connect2id/nimbus-jose-jwt/src/master/src/main/java/com/nimbusds/jwt/proc/DefaultJWTProcessor.java I also updated the post with the dependencies version. – chirina Feb 27 '20 at 08:08
  • Hey! I actually have the same question, have you found any workarounds? – Tyulpan Tyulpan Apr 03 '20 at 14:43
  • 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. – Neil Madden Sep 06 '21 at 09:40
  • @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? – deen Apr 08 '22 at 16:27

4 Answers4

8

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();
Tyulpan Tyulpan
  • 724
  • 1
  • 7
  • 17
  • could you comment on this - https://stackoverflow.com/questions/70049215/in-nimbus-jose-jwt-what-is-difference-between-lifespan-and-refreshtime – samshers Nov 20 '21 at 22:10
  • Note: `CacheBuilder` is from Guava library – AlexElin Mar 21 '22 at 09:33
  • 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). – Tyulpan Tyulpan Sep 19 '22 at 20:04
7

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);
    }
Michael R
  • 81
  • 1
  • 3
  • could you comment on this - https://stackoverflow.com/questions/70049215/in-nimbus-jose-jwt-what-is-difference-between-lifespan-and-refreshtime – samshers Nov 20 '21 at 22:18
  • 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? – Orbita Jan 31 '22 at 11:57
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.

-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

pete
  • 164
  • 1
  • 4