1
votes

I'm using Spring WebFlux with RouterFunction to define my API endpoints. I have Spring Security configured the following way:

@Bean
fun security(httpSecurity: ServerHttpSecurity): SecurityWebFilterChain {
    return httpSecurity
            .authenticationManager(authenticationManager)
            // disable default security
                .httpBasic().disable()
                .formLogin().disable()
                .logout().disable()
                .csrf().disable()
            .addFilterAt(apiAuthenticationWebFilter(), SecurityWebFiltersOrder.AUTHENTICATION)
            .authorizeExchange()
                .pathMatchers("/app/health").permitAll()
                .pathMatchers("/app/info", "/app/loggers", "/app/metrics").hasAuthority(SecurityRole.SYSTEM)
                .anyExchange()
                .authenticated()
            .and()
                .exceptionHandling()
                .authenticationEntryPoint(jwtAuthenticationFailureHandler)
            .and()
            .build()
}

@Bean
fun securityContextRepository(): NoOpServerSecurityContextRepository {
    return NoOpServerSecurityContextRepository.getInstance()
}

private fun apiAuthenticationWebFilter(): AuthenticationWebFilter {
    val apiAuthenticationWebFilter = AuthenticationWebFilter(authenticationManager)
    apiAuthenticationWebFilter.setAuthenticationFailureHandler(ServerAuthenticationEntryPointFailureHandler(jwtAuthenticationFailureHandler))
    apiAuthenticationWebFilter.setAuthenticationConverter(jwtAuthenticationConverter)
    apiAuthenticationWebFilter.setRequiresAuthenticationMatcher(PathPatternParserServerWebExchangeMatcher("/**"))
    apiAuthenticationWebFilter.setSecurityContextRepository(securityContextRepository())
    return apiAuthenticationWebFilter
}

RouterFunction is declared like this:

@Bean
fun apiRouter(testHandler: TestHandler,
              errorHandler: ErrorHandler): RouterFunction<*> = router {
    (accept(MediaType.APPLICATION_JSON_UTF8).nest {
        GET(TEST_PATH, testHandler::test )
    })
}.andOther(route(RequestPredicates.all(), HandlerFunction { errorHandler.notFoundResponse() }))

And the handler itself:

@Component
class TestHandler(private val testService: TestService) {

    fun test(request: ServerRequest): Mono<ServerResponse> {
        return request.toMono()
                .transform({ testResponse() })
    }

The issue is that request.principal() is always null the same as the ReactiveSecurityContextHolder.getContext().block() result, however authentication configuration works correctly and authenticates user. Interesting point that declaring endpoint using annotated controllers principal is not null and shows correctly authenticated user.

@RestController
class TestController(private val testService: TestService) {

    @GetMapping("/test")
    fun test(principal: Principal): Mono<String> {
        return testService.test().map { message -> message + " " + principal.name }
    }
}

Is it misconfiguration or a bug? There are similar questions asked, maybe it's the same issue?

SAMPLE:

Repo - https://github.com/yyunikov/spring-boot-2-kotlin-starter/

Line - https://github.com/yyunikov/spring-boot-2-kotlin-starter/blob/master/api/src/main/kotlin/com/yunikov/api/test/TestHandler.kt#L18

Curl example:

curl -H "Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJqdGkiOiJhNjQyMjk0NC01ZDFkLTQxODItOGE2ZS1mZGM0NjEwYzhlNTYiLCJzdWIiOiJ5dW5pa292IiwiaWF0IjoxNTIzNzQyNzcxLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjgwODAvIiwiZW1haWwiOiJ1c2VyQHl1bmlrb3YuY29tIiwicm9sZXMiOlsidXNlciJdfQ.RirM-t7GZnSDQIAn_J-thD1UAzdyxKmiR4ktA_BdlYZsFoYKzy5nj-1dBaG60o7M9FcfwFLAWEe4pc7DplDNjw" localhost:8080/test
1
Could this be a duplicate of stackoverflow.com/questions/48156572/… ? - dmendezg
@dmendezg Indeed, seems that it is working via flatMap, can you explain why request.principal().block() returns null? Isn't blocking should wait for result to be available? - Yuriy Yunikov
However flat mapping ReactiveSecurityContextHolder.getContext() still doesn't return a security object, why it's happening? - Yuriy Yunikov

1 Answers

1
votes

The internal implementation is based on a delegator, so it returns null if it hasn't been resolved yet (this behavior results in an exception now)

Here is an example on how to get both.

fun secure(serverRequest: ServerRequest): Mono<ServerResponse> {
    return Mono.zip(
        serverRequest.principal(),
        ReactiveSecurityContextHolder.getContext()
    )
        .flatMap {
            val principal = it.t1 // Not null
            Assert.notNull(principal)
            val securityContext = it.t2 // Not null
            Assert.notNull(securityContext)
            ServerResponse.ok().body(fromObject("Hello $principal!"))
        }
}

Notice that the use of block()/blockFirst()/blockLast() will result in an exception as an IllegalStateException