javaspringspring-securitykeycloak

Spring Boot with Spring Security and Keycloak only providing Anonymous Authentication Token


so I followed a guide which set up Keycloak to secure my Spring Boot REST API with Spring Security. Previously, my application relied on a standard user details service to authenticate users. My code relies on being able to access the currently logged-in user's username. However, now that I switched to Keycloak, I only receive the username 'anonymousUser'.

How do I configure this correctly for the following code to return the preferred username:

public static String getCurrentUsername() {

return (String) SecurityContextHolder.getContext().getAuthentication().getPrincipal();

}

I tried to modify the configuration of the client and user on Keycloak, but even for example, adding roles has no effect. Spring Boot only returns the role of [ANONYMOUS_USER].


Solution

  • There is no chance you get any info about current user from AnonymousAuthenticationToken: this implementation of Authentication is put in security-context when the request was not successfully authorized (user could not be identified). This means that either:

    A quite common mistake lately is to write oauth2Login configuration and then send requests with Bearer header, when incoming requests should be authorized with session cookies. If you want the requests to your REST API be authorized with access tokens provided in a Bearer Authorization header, then you have to configure it as an oauth2ResourceServer.

    Another common mistake relates to apps needing more than one authorization mechanism (OAuth2 client, OAuth2 resource server, Basic, etc.). In such cases, you should provide with a filter-chain bean for each. But if one of the first in @Orderintercepts a request it shouldn't (misconfigured securityMatcher), then the security context initialization fails.

    For basic info on how to configure Spring-boot 3 resource-server and client with Keycloak, see my answer to this question: Use Keycloak Spring Adapter with Spring Boot 3, or this Baeldung article I wrote.

    Spring-security default Authentication for successful OAuth2 authorization in resource-servers is JwtAuthenticationToken which return a Jwt instance as principal. You can get any access-token claim from this Jwt instance. Here is a sample with preferred_username. Open an access-token in a tool like https://jwt.io to find the name of the claim with the username value you are looking for (it could be as well sub, email or any private claim you configured in Keycloak)

    ((Jwt) SecurityContextHolder.getContext().getAuthentication().getPrincipal()).getClaims().get(StandardClaimNames.PREFERRED_USERNAME);
    

    If you want getPrincipal() to return the username instead of a Jwt instance, you'll have to provide your own Authentication implementation. You might use JwtAuthenticationToken as a base (but this not the best option, see the note below):

    public class MyAuthentication extends JwtAuthenticationToken {
        public MyAuthentication(Jwt jwt, Collection<? extends GrantedAuthority> authorities) {
            super(jwt, authorities);
        }
        
        @Override
        public String getPrincipal() {
            return getToken().getClaimAsString(StandardClaimNames.PREFERRED_USERNAME);
        }   
    }
    

    Just adapt the Jwt2AuthenticationConverter from the answer I linked above

    @Bean
    public Jwt2AuthenticationConverter authenticationConverter(Jwt2AuthoritiesConverter authoritiesConverter) {
        return jwt -> new MyAuthentication(jwt, authoritiesConverter.convert(jwt));
    }
    

    Important note

    You'd better use authentication.getName() instead of authentication.getPrincipal() to access username. principal is typed as Object in Authentication which makes your expressions very fragile: you can get absolutely any type of data as principal (depending on the type of authentication in the security context) and there are cases where you don't really control it (for instance with the AnonymousAuthenticationToken instance you currently have because of unauthorized request).

    However, JwtAuthenticationToken::getName returns subject (sub claim), so you'll still have to provide your own Authentication implementation for successful authorizations to return preferred_username in a new @Override of getName(). MyAuthentication would then be:

    public class MyAuthentication extends JwtAuthenticationToken {
        public MyAuthentication(Jwt jwt, Collection<? extends GrantedAuthority> authorities) {
            super(jwt, authorities);
        }
        
        //Note that this time getName() is overriden instead of getPrincipal()
        @Override
        public String getName() {
            return getToken().getClaimAsString(StandardClaimNames.PREFERRED_USERNAME);
        }   
    }
    

    Use the same Jwt2AuthenticationConverter bean to switch from default JwtAuthenticationToken to MyAuthentication in case of successful request authorization.

    Spring Boot 3.4 update (Security 6.4)

    The default authentication converter for resource servers with a JWT decoder is now configurable with the claim to take the username from (provided that this claim is at the root level, not a nested one). So, it is not needed anymore to create a new Authentication type or to replace the authentication converter:

    spring:
      security:
        oauth2:
          resourceserver:
            jwt:
              issuer-uri: ${issuer-uri}
              principal-claim-name: preferred_username
    

    Unfortunately, Keycloak roles can be found in several nested claims (realm_access.roles and resource_access.{client-id}.roles), but Spring Boot supports only one claim for it and with the same "root-level" limitation, so it is likely that we need to replace the authentication converter with Keycloak, unless using this starter I wrote (allows to get authorities from as many claims as we need using JsonPath, which enables to read nested claims).