I would like to get Spring JPA auditing to work with Spring Boot,I am authenticating with Keycloak using the latest feature of Spring Security.
springBootVersion = '2.1.0.RC1'
I am following the sample by spring security team https://github.com/jzheaux/messaging-app/tree/springone2018-demo/resource-server
ResourceServerConfig.kt
@EnableWebSecurity
class OAuth2ResourceServerSecurityConfiguration(val resourceServerProperties: OAuth2ResourceServerProperties) : WebSecurityConfigurerAdapter() {
@Throws(Exception::class)
override fun configure(http: HttpSecurity) {
http
.authorizeRequests()
.antMatchers("/api/**").authenticated()
.anyRequest().anonymous()
.and()
.oauth2ResourceServer()
.authenticationEntryPoint(MoreInformativeAuthenticationEntryPoint())
.jwt()
.jwtAuthenticationConverter(GrantedAuthoritiesExtractor())
.decoder(jwtDecoder())
}
private fun jwtDecoder(): JwtDecoder {
val issuerUri = this.resourceServerProperties.jwt.issuerUri
val jwtDecoder = JwtDecoders.fromOidcIssuerLocation(issuerUri) as NimbusJwtDecoderJwkSupport
val withIssuer = JwtValidators.createDefaultWithIssuer(issuerUri)
val withAudience = DelegatingOAuth2TokenValidator(withIssuer, AudienceValidator())
jwtDecoder.setJwtValidator(withAudience)
return jwtDecoder
}
}
class MoreInformativeAuthenticationEntryPoint : AuthenticationEntryPoint {
private val delegate = BearerTokenAuthenticationEntryPoint()
private val mapper = ObjectMapper().setSerializationInclusion(JsonInclude.Include.NON_NULL)
@Throws(IOException::class, ServletException::class)
override fun commence(request: HttpServletRequest, response: HttpServletResponse,
reason: AuthenticationException) {
this.delegate.commence(request, response, reason)
if (reason.cause is JwtValidationException) {
val validationException = reason.cause as JwtValidationException
val errors = validationException.errors
this.mapper.writeValue(response.writer, errors)
}
}
}
class GrantedAuthoritiesExtractor : JwtAuthenticationConverter() {
override fun extractAuthorities(jwt: Jwt): Collection<GrantedAuthority> {
val scopes = jwt.claims["scope"].toString().split(" ")
return scopes.map { SimpleGrantedAuthority(it) }
}
}
class AudienceValidator : OAuth2TokenValidator<Jwt> {
override fun validate(token: Jwt): OAuth2TokenValidatorResult {
val audience = token.audience
return if (!CollectionUtils.isEmpty(audience) && audience.contains("mobile-client")) {
OAuth2TokenValidatorResult.success()
} else {
OAuth2TokenValidatorResult.failure(MISSING_AUDIENCE)
}
}
companion object {
private val MISSING_AUDIENCE = BearerTokenError("invalid_token", HttpStatus.UNAUTHORIZED,
"The token is missing a required audience.", null)
}
}
application.yaml
spring:
application:
name: sociter
datasource:
url: jdbc:postgresql://localhost:5432/sociter
username: postgres
password: 123123
driver-class-name: org.postgresql.Driver
jpa:
hibernate:
ddl-auto: update
security:
oauth2:
resourceserver:
jwt:
jwk-set-uri: http://localhost:8080/auth/realms/sociter/protocol/openid-connect/certs
issuer-uri: http://localhost:8080/auth/realms/sociter
JpaAuditingConfiguration.kt
@Configuration
@EnableJpaAuditing
(auditorAwareRef = "auditorProvider")
class JpaAuditingConfiguration {
@Bean
fun auditorProvider(): AuditorAware<String> {
return if (SecurityContextHolder.getContext().authentication != null) {
val oauth2 = SecurityContextHolder.getContext().authentication as JwtAuthenticationToken
val claims = oauth2.token.claims
val userId = claims["sub"]
AuditorAware { Optional.of(userId.toString()) }
} else
AuditorAware { Optional.of("Unknown") }
}
}
BaseEntity.kt
@MappedSuperclass
@JsonIgnoreProperties(value = ["createdOn, updatedOn"], allowGetters = true)
@EntityListeners(AuditingEntityListener::class)
abstract class BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
val id: UUID = UUID.randomUUID()
@Column(nullable = false, updatable = false)
@CreatedDate
var createdOn: LocalDateTime = LocalDateTime.now()
@Column(nullable = true)
@LastModifiedDate
var updatedOn: LocalDateTime? = null
@Column(nullable = true, updatable = false)
@CreatedBy
var createdBy: String? = null
@Column(nullable = true)
@LastModifiedBy
var updatedBy: String? = null
}
I am getting createdBy and UpdatedBy set to as Unknown. During debugging, auditorProvider bean get invoked and sets the user to Unknown but when passing access_token, if condition is still false.
Not sure what I am missing.
I was able to replicate your issue, but in an equivalent Java setup. The issue is in your JpaAuditingConfiguration
class. If you observe your current JpaAuditingConfiguration
class closely this is what happens there:
auditorProvider()
function will try to generate a bean.AuditorAware
instance that will always return Unknown
.You need to change this class as follows(Sorry, I wrote it in Java, please convert it to Kotlin):
@Configuration
@EnableJpaAuditing(auditorAwareRef = "auditorProvider")
public class JPAAuditConfig {
@Bean
public AuditorAware<String> auditorProvider() {
return new AuditorAware<String>() {
@Override
public String getCurrentAuditor() {
if (SecurityContextHolder.getContext().getAuthentication() != null) {
OAuth2Authentication auth = (OAuth2Authentication) SecurityContextHolder.getContext().getAuthentication();
Object principal = auth.getUserAuthentication().getPrincipal();
CustomUserDetails userDetails = (CustomUserDetails) principal;
return userDetails.getUsername();
} else {
return "Unknown";
}
}
};
}
}
You can try this. Also, I suspect that with your current setup you would get updatedOn and createdOn correctly populated. If yes, that means all the JPA and EntityListener magic is actually working. You just need to return the correct implementation of AuditorAware
at runtime.
Also note that, my config does not use JwtAuthenticationToken
and I use a CustomUserDetails
implementation. But that's not related to your problem, and you can of course use your current token type (JwtAuthenticationToken
). Its just that, I had my own little app up and running inside which I replicated your issue.
Arun Patra's answer above works correctly for Java. I had to do the following way with Kotlin.
@Configuration
@EnableJpaAuditing(auditorAwareRef = "auditorProvider")
class JpaAuditingConfiguration {
@Bean
fun auditorProvider(): AuditorAware<String> {
return CustomAuditorAware()
}
}
private class CustomAuditorAware : AuditorAware<String> {
override fun getCurrentAuditor(): Optional<String> {
return if (SecurityContextHolder.getContext().authentication != null) {
val oauth2 = SecurityContextHolder.getContext().authentication as JwtAuthenticationToken
val loggedInUserId = oauth2.token.claims["sub"].toString()
Optional.of(loggedInUserId)
} else {
Optional.of("Unknown")
}
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With