I have a Spring (not Spring Boot) REST API that is secured using Spring Security. Many endpoints require authentication in the form of a JWT before they can be accessed. Often the principal is fetched to access information from the token:
CustomObject customObject = (CustomObject) SecurityContextHolder
.getContext()
.getAuthentication()
.getPrincipal();
I'm attempting to change the REST API to accept Keycloak tokens. Following the Keycloak documentation I've added the keycloak-spring-security-adapter
dependency, setup a local Keycloak instance using Docker for development and added a keycloak.json
to the API project. Everything seems to work, the API accepts an access token in the Authorization header of requests. However, when attempting to access information from the token an exception occurs because the KeycloakPrincipal
class can't be cast to our CustomObject
class.
I don't want to go through the whole project and change all casts when getting the principal from CustomObject
to KeycloakPrincipal
as that would be a significant amount of work. Besides, using the KeycloakPrincipal
object makes our code implementation specific (Keycloak in this case), what if we want to move to a different token provider.
Is it possible to change the default KeycloakPrincipal
set on the security context to a custom object so the above code for getting the principal still works? If so, what would be the best way to do that, through a Spring filter maybe?
CodePudding user response:
Do not use Keycloak adapters, it is very deprecated.
I strongly recommend you take the time to read this set of 3 tutorials which end with exactly what you want: configure spring-security to populate security-context with a custom Authentication
implementation containing data retrieved from a JWT access-token (issued by Keycloak or whatever).
Bad news is it based on spring-boot. Good news is everything is open-source and you can inspect any @Bean configured.
If you don't have spring-boot, you'll have a little more work to provide your application context with a SecurityFilterChain
bean for an OAuth2 resource-server with a JWT decoder. I let you refer to the doc to have one defined in your conf.
Once you have it, defining your own Converter<Jwt, ? extends AbstractAuthenticationToken>
on resource-server JWT decoder configurer should be enough:
interface ClaimsToAuthoritiesConverter extends Converter<Map<String, Object>, Collection<? extends GrantedAuthority>> {}
@Bean
ClaimsToAuthoritiesConverter authoritiesConverter() {
return (Map<String, Object> claims) -> {
// Override this with the actual private-claim(s) your authorization-server puts roles into
// like resource_access.some-client.roles or whatever
final var realmAccessClaim = (Map<String, Object>) claims.getOrDefault("realm_access", Map.of());
final var rolesClaim = (Collection<String>) realmAccessClaim.getOrDefault("roles", List.of());
return rolesClaim.stream().map(SimpleGrantedAuthority::new).toList();
};
}
@Bean
SecurityFilterChain filterChain(HttpSecurity http, Converter<Map<String, Object>, Collection<? extends GrantedAuthority>> authoritiesConverter) throws Exception {
http.oauth2ResourceServer().jwt()
// omitted regular JWT decoder configuration like issuer, jwk-set-uri, etc.
.jwtAuthenticationConverter(jwt -> new YourOwnAuthentication(jwt.getTokenValue(), jwt.getClaims(), authoritiesConverter));
// Some more security conf like CORS etc.
return http.build();
}
This converter from Jwt
to your own implementation of AbstractAuthenticationToken
is called after the JWT access-token was sucessfully decoded and validated. In other words, it is pretty safe to interfere at this stage, it is just a matter of formating valid authentication data in the most convenient way for your business code and security rules.
Tips for designing YourOwnAuthentication
:
public class YourOwnAuthentication extends AbstractAuthenticationToken {
private final CustomObject principal;
private final String bearerString;
public YourOwnAuthentication (String bearerString, Map<String, Object> claims, Converter<Map<String, Object>, Collection<? extends GrantedAuthority>> authoritiesConverter) {
super(authoritiesConverter.convert(claims));
super.setAuthenticated(true);
this.bearerString = bearerString;
this.principal = new CustomObject(claims); // I connot figure out how you'll actually build that
}
@Override
public String getCredentials() {
return bearerString;
}
@Override
public CustomObject getPrincipal() {
return principal;
}
public String getBearerString() {
return bearerString;
}
}
Side note: in addition to "manually" access your CustomObject
instance from security-context as you do in your question you'll also be able to access it with spring "magic" authentication parameters for @Controller methods:
@PreAuthorise("isAuthenticated()") ResponseEntity<?> controllerMethod(YourOwnAuthentication auth)
and then declareCustomObject customObject = auth.getPrincipal();
(note there is no cast here)@PreAuthorise("isAuthenticated()") ResponseEntity<?> controllerMethod(@AuthenticationPrincipal CustomObject customObject)
Last, a production ready authorities converter here