Description
Dear all,
JWT revocation can be implemented manually with a publish/subscribe : a revocation event is sent (payload is the token to revoke) and subscribers (API that may receive this token and must reject it) receive this event so they can keep an up to date revocation list. Elements are automatically removed when exp
is before Instant.now()
A built-in support for publish/subscribe could be a part of the in-development spring oidc server.
But on the spring security side, as far as I know it must be coded manually. Example (TTL omitted) :
Set<String> revoked = new HashSet<>();
@Bean
RouterFunction<ServerResponse> revocations(){
return RouterFunctions.route(RequestPredicates.POST("/revocations"), req -> {
String token = req.body(String.class); // content-type is text/plain
revoked.add(token);
return ServerResponse.noContent().build();
});
}
@Bean
WebSecurityConfigurerAdapter securityConfigurerAdapter(JwtDecoder jwtDecoder /*created by auto-configuration*/) {
return new WebSecurityConfigurerAdapter() {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.oauth2ResourceServer(cust -> {
cust.jwt(jwtCust -> {
jwtCust.decoder(token -> {
if(revoked.contains(token)) {
throw new SecurityException();
}
return jwtDecoder.decode(token);
});
});
});
http.csrf().disable();
}
};
}
How about a built-in support in spring Security ?
If the revocation endpoint and the revocations set are declared manually the configuration would looks like :
Set<String> revoked = new HashSet<>();
@Bean
RouterFunction<ServerResponse> revocations() {
return RouterFunctions.route(RequestPredicates.POST("/revocations"), req -> {
String token = req.body(String.class);
revoked.add(token);
return ServerResponse.noContent().build();
});
}
// omitted : scheduled removal of expired tokens
@Bean
WebSecurityConfigurerAdapter securityConfigurerAdapter(JwtDecoder jwtDecoder){
return new WebSecurityConfigurerAdapter() {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.oauth2ResourceServer(cust -> {
cust.jwt(jwtCust -> {
jwtCust.setRevocations(this.revoked);
});
});
http.csrf().disable();
}
};
}
If the revocation endpoint is built-in, like an actuator :
@Bean
WebSecurityConfigurerAdapter securityConfigurerAdapter(){
return new WebSecurityConfigurerAdapter() {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.oauth2ResourceServer(cust -> {
cust.jwt(jwtCust -> {
jwtCust.enableRevocations();
});
});
http.csrf().disable();
}
};
}
With a customizer :
cust.jwt(jwtCust -> {
jwtCust.enableRevocations(revocationCustomizer -> {
revocationCustomizer.rejectIf(token -> /*code that return true or false*/);
revocationCustomizer.rejectResponseHandler(token -> ResponseEntity.status(401).header("WWW-Authenticate", "Bearer error=\"invalid_token\", error_description=\"An error occurred while attempting to decode the Jwt: The token is revoked!\", error_uri=\"https://tools.ietf.org/html/rfc6750#section-3.1\"").build());
});
});
and configuration properties would allow to configure revocation endpoint path and RBAC for this endpoint (exemple : hasRole('admin')
).