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
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);
}
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();
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With