How to get Keycloak username in AuditorAware
Asked Answered
B

4

5

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);
    }
}
Ban answered 1/10, 2020 at 10:35 Comment(7)
…I deploy the WAR…“ — I will try one more time: (1) «Why do you absolutely MUST have a WAR?» — kp.getKeycloakSecurityContext().getToken()… — (2) What type of token is that? (3) Is this a component in the same project your other question asks about? (4) Are the properties in the application.properties of this project, the same as in the application.properties of that other Keycloak question? (5) Do you appreciate how frustrating it is trying to help someone that's hesitant to answer questions? TIAGalingale
…i have to deploy my app…so users can access it…“ — I will assume that you already know that a Spring Boot jar could be deployed by simply running java -jar /path/to/boot-app.jar… on a remote machine. That users can still access the web app that way. I mention this because Spring Boot is designed to simplify things. I suspect that part of your problem is you may be over-complicating things by unnecessarily „deploying a WAR to Tomcat“. —„…once i fix keycloak with spring secured, this will work…“ — Is it a good idea to pile more components on top of a malconfigured system?Galingale
…once i fix something, something else get messed up…“ — I didn't see that until after I posted my last comment. I alluded to it in the last sentence of my comment: It is not a good idea to pile more components on top of a malconfigured system. My advice is you should fix one thing at a time. Don't add additional complexity until you know your base system is working. One more question for you: Does either of the proposed solutions have any relevance to you? I'm referring to my answer and the other two answers so far. TIA.Galingale
…i mixed the whole thing up, but i couldn't find a one way to use keycloak…“ — The reason you couldn't is because there are as many different ways to use Keycloak as there are apps. Different systems' Keycloak configs should not be expected to be exactly the same. Yes there are commonalities. But your system's constraints sound atypical to my experience. More than likely your solution will be some config unique to your system. Your best chance for a resolution is to share an MRE. If you can't, then good luck. I tried my best…Galingale
…your answer and one other answer is relevant to me…“ — I noticed you ediited the application.properties in your other question from keycloak.public-client=true to keycloak.public-client=false as I proposed in my answer below. What's the reason for not also adding keycloak.principal-attribute=preferred_username — which I also proposed? I think that property is needed as much as (possibly more than) the keycloak.public-client property. If you don't mind me asking. TIA.Galingale
…because as i mentioned in my question i followed the documentation…“ — Did you notice the differences between that tutorial and your app? (1) Browser Form Login vs Bearer Token? (2) No requirement to access the principal vs a requirement to access the principal? That's just the two you've made known. I suspect there are more differences we don't know about. But please correct me if I'm wrong. Does your app also require browser login? Is this post not asking why the principal is not accessible by your program? TIA.Galingale
…i should be able to access the principal in spring boot side, if i've already logged in in angular side…“ — You should IFF you've configured the spring boot side correctly. Yes. So now tell us: What have you already done so far to configure the spring boot side to access the principal? Your answer to that would help me and others tremendously in solving the issue you're reporting in this question. And, of course, you would like us to help you. Isn't that right? TIA.Galingale
D
6

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.

Dael answered 5/10, 2020 at 9:58 Comment(3)
i'm using keycloak with spring securityBan
JwtAuthenticationToken cannot be resolved to a typeBan
Add a breakpoint here Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); and play with it to extract the usernameDael
G
3

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.

Galingale answered 6/10, 2020 at 11:16 Comment(0)
Q
1

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
...
Qulllon answered 20/5, 2021 at 8:44 Comment(0)
T
0

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",
Teetotaler answered 24/2, 2022 at 22:13 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.