Reputation: 3976
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
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
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
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 aresub
,preferred_username
,name
,nickname
,given_name
,family_name
.…
Upvotes: 3
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