Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Spring Boot Data JPA @CreatedBy and @UpdatedBy not populating with authenticating with OIDC

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.

like image 604
Chirdeep Tomar Avatar asked Oct 20 '18 15:10

Chirdeep Tomar


2 Answers

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:

  1. During Spring initialization the auditorProvider() function will try to generate a bean.
  2. The authentication condition is being checked there upfront(during application startup) and this thread(which starts the Spring Boot App) is NOT an authenticated thread at all. Hence it returns an 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.

like image 62
Arun Patra Avatar answered Sep 21 '22 05:09

Arun Patra


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")
        }
    }
}
like image 20
Chirdeep Tomar Avatar answered Sep 22 '22 05:09

Chirdeep Tomar