Home > Software engineering >  How to use a custom object instead of KeycloakPrincipal
How to use a custom object instead of KeycloakPrincipal

Time:10-29

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 declare CustomObject customObject = auth.getPrincipal(); (note there is no cast here)
  • @PreAuthorise("isAuthenticated()") ResponseEntity<?> controllerMethod(@AuthenticationPrincipal CustomObject customObject)

Last, a production ready authorities converter here

  • Related