Bilal Dekar
Bilal Dekar

Reputation: 3976

How to get Keycloak username in AuditorAware

I have implemented Auditing with Spring Data JPA, following exactly this documentation. Everything works fine when I run the app, but when I deploy the WAR to Tomcat and try to create an entity, I get an error in the getCurrentAuditor method.

I have secured my app with keycloak, so in AuditorAwareConfig i am trying to get the keycloak username, and after debugging i found out that request.getUserPrincipal() is null :

java.lang.NullPointerException: null
    at com.cevital.cirta.util.AuditorAwareConfig.getCurrentAuditor(AuditorAwareConfig.java:20) ~[classes/:0.0.1-SNAPSHOT

AuditorAwareConfig :

public class AuditorAwareConfig implements AuditorAware<String> {
    @Autowired
    private HttpServletRequest request;

    @Override
    public Optional<String> getCurrentAuditor() {
        KeycloakPrincipal<KeycloakSecurityContext> kp = (KeycloakPrincipal<KeycloakSecurityContext>) request.getUserPrincipal();
        String userName = kp.getKeycloakSecurityContext().getToken().getPreferredUsername();
        return Optional.ofNullable(userName);
    }
}

Upvotes: 5

Views: 3496

Answers (4)

Cristian Florescu
Cristian Florescu

Reputation: 1816

For Apache Tomcat 9 update in /WEB-INF/keycloak.json and add the following attribute within first section after realm, resource etc.

"principal-attribute" : "preferred_username",

Upvotes: 0

lindem
lindem

Reputation: 11

I have the same problem and my solution is:

Configuration Aware class in order to get user:

import java.security.Principal;
import java.util.Optional;
import javax.servlet.http.HttpServletRequest;
import org.keycloak.adapters.springsecurity.token.KeycloakAuthenticationToken;
import org.keycloak.representations.AccessToken;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.AuditorAware;

public class AuditorAwareConfig implements AuditorAware<String> {
    @Autowired
    private HttpServletRequest request;

    @Override
    public Optional<String> getCurrentAuditor() {
        AccessToken accessToken = this.getKeycloakToken(request.getUserPrincipal());
        String userName = accessToken.getPreferredUsername();
        
        return Optional.ofNullable(userName);
    }
    
    private AccessToken getKeycloakToken(Principal principal) {
        KeycloakAuthenticationToken keycloakAuthenticationToken = (KeycloakAuthenticationToken) principal;
        return keycloakAuthenticationToken.getAccount().getKeycloakSecurityContext().getToken();
    }

Class enabling JPA Auditing:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.domain.AuditorAware;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;

@Configuration
@EnableJpaAuditing(auditorAwareRef = "auditorProvider")
public class AuditConfig {
    
    @Bean
    AuditorAware<String> auditorProvider() {
        return new AuditorAwareConfig();
    }

}

Auditable class:

@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
@JsonIgnoreProperties(
        value = {"createdAt", "createdBy","updatedAt", "updatedBy"},
        allowGetters = true
)
public abstract class Auditable {

    @CreatedDate
    @Column(name="AUD_CREATE_AT", nullable = false, updatable = false)
    private Instant createdAt;
    
    @CreatedBy
    @Column(name="AUD_CREATE_BY", nullable = false, updatable = false)
    private String createdBy;

    @LastModifiedDate
    @Column(name="AUD_UPDATE_AT",nullable = false)
    private Instant updatedAt;
    
    @LastModifiedBy
    @Column(name="AUD_UPDATE_BY",nullable = false)
    private String updatedBy;
    //Getters & Setters

At entity class:

@Entity
@Table(name="FOO")
public class FooEntity extends Auditable implements Serializable
...

Upvotes: 1

deduper
deduper

Reputation: 1964

TL;DR — In your application.properties file, add keycloak.principal-attribute=preferred_username. Conditionally, you might also need to set keycloak.public-client=false (or remove it completely).


The long-winded version

According to your other Keycloak-related question

This is my Spring Configuration :

application.properties:

keycloak.realm=cirta
keycloak.auth-server-url=http://localhost:8085/auth
keycloak.resource=cirta-api
keycloak.public-client=true
keycloak.cors=true    
keycloak.bearer-only=true

You probably need to change that to this…

keycloak.realm=cirta
keycloak.auth-server-url=http://localhost:8085/auth
keycloak.resource=cirta-api
keycloak.public-client=false # or delete the property, since false is the default
keycloak.cors=true    
keycloak.bearer-only=true
keycloak.principal-attribute=preferred_username # add this

In the TL;DR I qualified my public-client suggestion as „Conditionally“. By that I mean after adding the keycloak.principal-attribute property, you might need to experiment with toggling keycloak.public-client on and off; depending on which setting works for your specific setup.

Refer to the Keycloak docs for more details…

public-client

If set to true, the adapter will not send credentials for the client to Keycloak. This is OPTIONAL. The default value is false.

principal-attribute

OpenID Connect ID Token attribute to populate the UserPrincipal name with. If token attribute is null, defaults to sub. Possible values are sub, preferred_username, email, name, nickname, given_name, family_name.

Upvotes: 3

akuma8
akuma8

Reputation: 4691

I recently did the same thing in my applications but I didn't use the Keycloak adapter, Spring Security 5 provides all we need to secure our applications with Keycloak or with any Oauth2 provider. Another difference, I use Hibernate Envers, which allow me to also audit delete operations.

To get the authenticated user, this is how I proceed.

   public static String extractUsernameFromAuthentication() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        String username;
        if ( isNull( authentication ) ) {
            return null;
        }
        if ( authentication instanceof JwtAuthenticationToken ) {
            JwtAuthenticationToken token = (JwtAuthenticationToken) authentication;
            username = (String) ( token ).getTokenAttributes().get( "preferred_username" );
        } else {
            username = authentication.getName();
        }
        return username;
    }

Remember, I do not use the Keycloak Adapter.

Upvotes: 6

Related Questions