How I do pass parameter to configuration bean in Kotlin - spring-boot

I have declared OAuthClientConfiguration setting for WebClient using ReactiveClientRegistrationRepository. But I need to pass some params from service (MyService)
WebClientConfiguration defines as follows:
#Configuration
class WebClientConfiguration {
#Bean
fun clientRegistrations(credentials: Credentials):ReactiveClientRegistrationRepository? {
val registration = ClientRegistration
.withRegistrationId("okta")
.tokenUri(access_token)
.clientId(credentials.client_id)
.clientSecret(credentials.client_secret)
.scope(credentials.scope)
.authorizationGrantType(AuthorizationGrantType(authorizationGrantType))
.build()
return InMemoryReactiveClientRegistrationRepository(registration)
}
#Bean
fun webClient(clientRegistrations: ReactiveClientRegistrationRepository?): WebClient? {
val clientService = InMemoryReactiveOAuth2AuthorizedClientService(clientRegistrations)
val authorizedClientManager =
AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager(clientRegistrations, clientService)
val oauth = ServerOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager)
oauth.setDefaultClientRegistrationId("okta")
return WebClient.builder()
.filter(oauth)
.build()
}
}
And WebClient calling is defined as follows:
#Service
class MyService (private val webClient: WebClient) {
fun doRequest(): String? {
return webClient.get()
.retrieve()
}
}
How I do pass the credentials to the configuration beans

Related

How to add multiple client credentials on spring boot webclient using webflux

Currently my setup as following.
#Configuration
public class Oauth2WebClientConfig {
private final Environment env;
#Autowired
public Oauth2WebClientConfig(Environment env) {
this.env = env;
}
// == Oauth2 Configuration ==
#Bean
ReactiveClientRegistrationRepository clientRegistration() {
ClientRegistration clientRegistration = ClientRegistration
.withRegistrationId("custom")
.tokenUri(env.getProperty("accessTokenUri"))
.clientId(env.getProperty("clientID"))
.clientSecret(env.getProperty("clientSecret"))
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
.scope(env.getProperty("scope"))
.build();
return new InMemoryReactiveClientRegistrationRepository(clientRegistration);
}
#Bean
ReactiveOAuth2AuthorizedClientService authorizedClientService() {
return new InMemoryReactiveOAuth2AuthorizedClientService(clientRegistration());
}
// == Oauth2 Configuration ==
// == WebFlux Configuration ==
#Bean
WebClient webClient(ReactiveClientRegistrationRepository clientRegistrations, ReactiveOAuth2AuthorizedClientService authorizedClientService) {
ServerOAuth2AuthorizedClientExchangeFilterFunction oauth = new ServerOAuth2AuthorizedClientExchangeFilterFunction(
new AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager(clientRegistrations, authorizedClientService));
oauth.setDefaultClientRegistrationId("custom");
return WebClient.builder()
.filter(oauth)
.build();
}
// == WebFlux Configuration ==
}
I need to add multiple client credentials is there a way to do it without creating multiple ReactiveClientRegistrationRepository bean & WebClient bean?

Spring Boot 2 OIDC (OAuth2) client / resource server not propagating the access token in the WebClient

Sample project available on Github
I have successfully configured two Spring Boot 2 application2 as client/resource servers against Keycloak and SSO between them is fine.
Besides, I am testing authenticated REST calls to one another, propagating the access token as an Authorization: Bearer ACCESS_TOKEN header.
After starting Keycloak and the applications I access either http://localhost:8181/resource-server1 or http://localhost:8282/resource-server-2 and authenticate in the Keycloak login page. The HomeController uses a WebClient to invoke the HelloRestController /rest/hello endpoint of the other resource server.
#Controller
class HomeController(private val webClient: WebClient) {
#GetMapping
fun home(httpSession: HttpSession,
#RegisteredOAuth2AuthorizedClient authorizedClient: OAuth2AuthorizedClient,
#AuthenticationPrincipal oauth2User: OAuth2User): String {
val authentication = SecurityContextHolder.getContext().authentication
println(authentication)
val pair = webClient.get().uri("http://localhost:8282/resource-server-2/rest/hello").retrieve()
.bodyToMono(Pair::class.java)
.block()
return "home"
}
}
This call returns a 302 since the request is not authenticated (it's not propagating the access token):
2019-12-25 14:09:03.737 DEBUG 8322 --- [nio-8181-exec-5] o.s.s.w.a.ExceptionTranslationFilter : Access is denied (user is anonymous); redirecting to authentication entry point
org.springframework.security.access.AccessDeniedException: Access is denied
at org.springframework.security.access.vote.AffirmativeBased.decide(AffirmativeBased.java:84) ~[spring-security-core-5.2.1.RELEASE.jar:5.2.1.RELEASE]
at org.springframework.security.access.intercept.AbstractSecurityInterceptor.beforeInvocation(AbstractSecurityInterceptor.java:233) ~[spring-security-core-5.2.1.RELEASE.jar:5.2.1.RELEASE]
OAuth2Configuration:
#Configuration
class OAuth2Config : WebSecurityConfigurerAdapter() {
#Bean
fun webClient(): WebClient {
return WebClient.builder()
.filter(ServletBearerExchangeFilterFunction())
.build()
}
#Bean
fun clientRegistrationRepository(): ClientRegistrationRepository {
return InMemoryClientRegistrationRepository(keycloakClientRegistration())
}
private fun keycloakClientRegistration(): ClientRegistration {
val clientRegistration = ClientRegistration
.withRegistrationId("resource-server-1")
.clientId("resource-server-1")
.clientSecret("c00670cc-8546-4d5f-946e-2a0e998b9d7f")
.clientAuthenticationMethod(ClientAuthenticationMethod.BASIC)
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.redirectUriTemplate("{baseUrl}/login/oauth2/code/{registrationId}")
.scope("openid", "profile", "email", "address", "phone")
.authorizationUri("http://localhost:8080/auth/realms/insight/protocol/openid-connect/auth")
.tokenUri("http://localhost:8080/auth/realms/insight/protocol/openid-connect/token")
.userInfoUri("http://localhost:8080/auth/realms/insight/protocol/openid-connect/userinfo")
.userNameAttributeName(IdTokenClaimNames.SUB)
.jwkSetUri("http://localhost:8080/auth/realms/insight/protocol/openid-connect/certs")
.clientName("Keycloak")
.providerConfigurationMetadata(mapOf("end_session_endpoint" to "http://localhost:8080/auth/realms/insight/protocol/openid-connect/logout"))
.build()
return clientRegistration
}
override fun configure(http: HttpSecurity) {
http.authorizeRequests { authorizeRequests ->
authorizeRequests
.anyRequest().authenticated()
}.oauth2Login(withDefaults())
.logout { logout ->
logout.logoutSuccessHandler(oidcLogoutSuccessHandler())
}
}
private fun oidcLogoutSuccessHandler(): LogoutSuccessHandler? {
val oidcLogoutSuccessHandler = OidcClientInitiatedLogoutSuccessHandler(clientRegistrationRepository())
oidcLogoutSuccessHandler.setPostLogoutRedirectUri(URI.create("http://localhost:8181/resource-server-1"))
return oidcLogoutSuccessHandler
}
}
As you can see I'm setting a ServletBearerExchangeFilterFunction in the WebClient. This is what I've seen debugging:
The SubscriberContext isn't setting anything because authentication.getCredentials() instanceof AbstractOAuth2Token is false. Actually it is just a String:
public class OAuth2AuthenticationToken extends AbstractAuthenticationToken {
...
#Override
public Object getCredentials() {
// Credentials are never exposed (by the Provider) for an OAuth2 User
return "";
}
What's the problem here? How can I automate the propagation of the token?
There doesn't seem to be an out of the box solution for pure OAuth2/OIDC login applications, I've created a Github issue for this.
In the meantime, I've created a specific ServletBearerExchangeFilterFunction that retrieves the access token from the OAuth2AuthorizedClientRepository.
This is my custom solution:
#Autowired
lateinit var oAuth2AuthorizedClientRepository: OAuth2AuthorizedClientRepository
#Bean
fun webClient(): WebClient {
val servletBearerExchangeFilterFunction = ServletBearerExchangeFilterFunction("resource-server-1", oAuth2AuthorizedClientRepository)
return WebClient.builder()
.filter(servletBearerExchangeFilterFunction)
.build()
}
...
private fun keycloakClientRegistration(): ClientRegistration {
return ClientRegistration
.withRegistrationId("resource-server-1")
...
const val SECURITY_REACTOR_CONTEXT_ATTRIBUTES_KEY = "org.springframework.security.SECURITY_CONTEXT_ATTRIBUTES"
class ServletBearerExchangeFilterFunction(private val clientRegistrationId: String,
private val oAuth2AuthorizedClientRepository: OAuth2AuthorizedClientRepository?) : ExchangeFilterFunction {
/**
* {#inheritDoc}
*/
override fun filter(request: ClientRequest, next: ExchangeFunction): Mono<ClientResponse> {
return oauth2Token()
.map { token: AbstractOAuth2Token -> bearer(request, token) }
.defaultIfEmpty(request)
.flatMap { request: ClientRequest -> next.exchange(request) }
}
private fun oauth2Token(): Mono<AbstractOAuth2Token> {
return Mono.subscriberContext()
.flatMap { ctx: Context -> currentAuthentication(ctx) }
.map { authentication ->
val authorizedClient = oAuth2AuthorizedClientRepository?.loadAuthorizedClient<OAuth2AuthorizedClient>(clientRegistrationId, authentication, null)
if (authorizedClient != null) {
authorizedClient.accessToken
} else {
Unit
}
}
.filter { it != null }
.cast(AbstractOAuth2Token::class.java)
}
private fun currentAuthentication(ctx: Context): Mono<Authentication> {
return Mono.justOrEmpty(getAttribute(ctx, Authentication::class.java))
}
private fun <T> getAttribute(ctx: Context, clazz: Class<T>): T? { // NOTE: SecurityReactorContextConfiguration.SecurityReactorContextSubscriber adds this key
if (!ctx.hasKey(SECURITY_REACTOR_CONTEXT_ATTRIBUTES_KEY)) {
return null
}
val attributes: Map<Class<T>, T> = ctx[SECURITY_REACTOR_CONTEXT_ATTRIBUTES_KEY]
return attributes[clazz]
}
private fun bearer(request: ClientRequest, token: AbstractOAuth2Token): ClientRequest {
return ClientRequest.from(request)
.headers { headers: HttpHeaders -> headers.setBearerAuth(token.tokenValue) }
.build()
}
}

Spring WebFlux Route always returns 404

I am working on a simple project which uses Spring Boot 2 with Spring WebFlux using Kotlin.
I wrote test for my handler function (in which I mock the dependencies using Mockito).
However, it seems like my route function does not trigger the handler, as all of my requests return HTTP 404 NOT FOUND (even though the route is correct).
I have looked at various other projects to find out what how these tests are supposed to be written (here, here), but the problem persists.
The code is as follows (and can also be found on GitHub):
UserRouterTest
#ExtendWith(SpringExtension::class, MockitoExtension::class)
#Import(UserHandler::class)
#WebFluxTest
class UserRouterTest {
#MockBean
private lateinit var userService: UserService
#Autowired
private lateinit var userHandler: UserHandler
#Test
fun givenExistingCustomer_whenGetCustomerByID_thenCustomerFound() {
val expectedCustomer = User("test", "test")
val id = expectedCustomer.userID
`when`(userService.getUserByID(id)).thenReturn(Optional.ofNullable(expectedCustomer))
val router = UserRouter().userRoutes(userHandler)
val client = WebTestClient.bindToRouterFunction(router).build()
client.get()
.uri("/users/$id")
.accept(MediaType.ALL)
.exchange()
.expectStatus().isOk
.expectHeader().contentType(MediaType.APPLICATION_JSON_UTF8)
.expectBody(User::class.java)
}
}
User
#Entity
class User(var username : String, var password: String) {
#Id
val userID = UUID.randomUUID()
}
UserRepository
#Repository
interface UserRepository : JpaRepository<User, UUID>{
}
UserService
#Service
class UserService(
private val userRepository: UserRepository
) {
fun getUserByID(id: UUID): Optional<User> {
return Optional.of(
try {
userRepository.getOne(id)
} catch (e: EntityNotFoundException) {
User("test", "test")
}
)
}
fun addUser(user: User) {
userRepository.save(user)
}
}
UserHandler
#Component
class UserHandler(
private val userService: UserService
) {
fun getUserWithID(request: ServerRequest): Mono<ServerResponse> {
val id = try {
UUID.fromString(request.pathVariable("userID"))
} catch (e: IllegalArgumentException) {
return ServerResponse.badRequest().syncBody("Invalid user id")
}
val user = userService.getUserByID(id).get()
return ServerResponse.ok().contentType(MediaType.APPLICATION_JSON_UTF8)
.body(BodyInserters.fromObject(user))
}
}
UserRouter
#Configuration
class UserRouter {
#Bean
fun userRoutes(userHandler: UserHandler) = router {
contentType(MediaType.APPLICATION_JSON_UTF8).nest {
GET("/users/{userID}", userHandler::getUserWithID)
GET("") { ServerResponse.ok().build() }
}
}
}
EDIT
To route based on the presence of one or more query parameter (regardless of their values), we can do the following:
UserRouter
#Configuration
class UserRouter {
#Bean
fun userRoutes(userHandler: UserHandler) = router {
GET("/users/{userID}", userHandler::getUserWithID)
(GET("/users/")
and queryParam("username") { true }
and queryParam("password") { true }
)
.invoke(userHandler::getUsers)
}
}
Note that GET("/users/?username={username}", userHandler::getUsersWithUsername) does not work.
The way the router is configured - contentType(MediaType.APPLICATION_JSON_UTF8).nest - will only match requests that have this content type, so you would have to either remove the contentType prerequisite or change the test to include it
client.get()
.uri("/users/$id")
.accept(MediaType.ALL)
.header("Content-Type", "application/json;charset=UTF-8")
.exchange()
.expectStatus().isOk
.expectHeader().contentType(MediaType.APPLICATION_JSON_UTF8)
.expectBody(User::class.java)

Using WebClient to propagate request headers received in a Spring Webflux applications to downstream services

I have two kinds of Webflux applications, annotation-based and route-based. These applications are called with a set of headers, some of which (Open Tracing) I need to propagate in downstream calls using WebClient.
If these were normal Spring WebMvc applications I would use a Filter to keep the selected headers in a ThreadLocal, access it in a RestTemplate interceptor to send them to subsequent services and clear the ThreadLocal.
What's the proper way to replicate this behaviour in WebFlux applications?
I solved it using Project Reactor's Context to store the headers in a WebFilter. Then they are gotten in the WebClient's ExchangeFilterFunction. Here's the whole solution:
WebFilter
class OpenTracingFilter(private val openTracingHeaders: Set<String>) : WebFilter {
private val logger = LoggerFactory.getLogger(javaClass)
override fun filter(exchange: ServerWebExchange, chain: WebFilterChain): Mono<Void> {
return chain.filter(exchange)
.subscriberContext { ctx ->
var updatedContext = ctx
exchange.request.headers.forEach {
if (openTracingHeaders.contains(it.key.toLowerCase())) {
logger.debug("Found OpenTracing Header - key {} - value {}", it.key, it.value[0])
updatedContext = updatedContext.put(it.key, it.value[0])
}
}
updatedContext
}
}
}
OpenTracingExchangeFilterFunction
class OpenTracingExchangeFilterFunction(private val headers: Set<String>) : ExchangeFilterFunction {
private val logger = LoggerFactory.getLogger(javaClass)
override fun filter(request: ClientRequest, next: ExchangeFunction): Mono<ClientResponse> {
logger.debug("OpenTracingExchangeFilterFunction - filter()")
return OpenTracingClientResponseMono(request, next, headers)
}
}
OpenTracingClientResponseMono
class OpenTracingClientResponseMono(private val request: ClientRequest,
private val next: ExchangeFunction,
private val headersToPropagate: Set<String>) : Mono<ClientResponse>() {
private val logger = LoggerFactory.getLogger(javaClass)
override fun subscribe(subscriber: CoreSubscriber<in ClientResponse>) {
val context = subscriber.currentContext()
val requestBuilder = ClientRequest.from(request)
requestBuilder.headers { httpHeaders ->
headersToPropagate.forEach {
if(context.hasKey(it)) {
logger.debug("Propagating header key {} - value{}", it, context.get<String>(it))
httpHeaders[it] = context.get<String>(it)
}
}
}
val mutatedRequest = requestBuilder.build()
next.exchange(mutatedRequest).subscribe(subscriber)
}
}
OpenTracingConfiguration
#Configuration
class OpenTracingConfiguration(private val openTracingConfigurationProperties: OpenTracingConfigurationProperties) {
#Bean
fun webClient(): WebClient {
return WebClient.builder().filter(openTracingExchangeFilterFunction()).build()
}
#Bean
fun openTracingFilter(): WebFilter {
return OpenTracingFilter(openTracingConfigurationProperties.headers)
}
#Bean
fun openTracingExchangeFilterFunction(): OpenTracingExchangeFilterFunction {
return OpenTracingExchangeFilterFunction(openTracingConfigurationProperties.headers)
}
}
OpenTracingConfigurationProperties
#Configuration
#ConfigurationProperties("opentracing")
class OpenTracingConfigurationProperties {
lateinit var headers: Set<String>
}
application.yml
opentracing:
headers:
- x-request-id
- x-b3-traceid
- x-b3-spanid
- x-b3-parentspanid
- x-b3-sampled
- x-b3-flags
- x-ot-span-context
I needed to pass x-request-id header to a downstream service in my application. Achieved this by adding WebFilter that writes x-request-id to a reactor context
class ContextWebFilter : WebFilter {
override fun filter(exchange: ServerWebExchange, chain: WebFilterChain): Mono<Void> {
val headers = exchange.request.headers
val xRequestId = headers[X_REQUEST_ID]?.firstOrNull() ?: ""
val requestId = xRequestId.ifBlank { UUID.randomUUID().toString() }
return chain
.filter(exchange)
.contextWrite { it.put(X_REQUEST_ID, requestId) }
}
companion object {
const val X_REQUEST_ID = "X-REQUEST-ID"
}
}
and updating WebClient with ExchangeFilterFunction that updates outgoing request
WebClient.builder()
.filter(
ExchangeFilterFunction.ofRequestProcessor { request ->
Mono.deferContextual { context ->
val xRId = context.getOrDefault<String>("X-REQUEST-ID", "")
logger.debug("Set X-REQUEST-ID={} as a header to outgoing call", xRId)
Mono.just(
ClientRequest.from(request)
.header("X-REQUEST-ID", xRId)
.build()
)
}
}
)
.baseUrl("http://localhost:8080")
.build()

Custom authentication with spring-security and reactive spring

I'm having a custom authentication scheme. I'm having a REST endpoint that has userId in http uri path and token in http header. I would like to check that such request is perform by valid user with valid token. Users and tokens are stored in mongo collection.
I don't know in which class I should authorize user.
My SecurityConfig:
#EnableWebFluxSecurity
class SecurityConfig {
#Bean
fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
val build = http
.httpBasic().disable()
.formLogin().disable()
.csrf().disable()
.logout().disable()
.authenticationManager(CustomReactiveAuthenticationManager())
.securityContextRepository(CustomServerSecurityContextRepository())
.authorizeExchange().pathMatchers("/api/measurement/**").hasAuthority("ROLE_USER")
.anyExchange().permitAll().and()
return build.build()
}
#Bean
fun userDetailsService(): MapReactiveUserDetailsService {
val user = User.withDefaultPasswordEncoder()
.username("sampleDeviceIdV1")
.password("foo")
.roles("USER")
.build()
return MapReactiveUserDetailsService(user)
}
}
My ServerSecurityContextRepository:
class CustomServerSecurityContextRepository : ServerSecurityContextRepository {
override fun load(exchange: ServerWebExchange): Mono<SecurityContext> {
val authHeader = exchange.request.headers.getFirst(HttpHeaders.AUTHORIZATION)
val path = exchange.request.uri.path
return if (path.startsWith("/api/measurement/") && authHeader != null && authHeader.startsWith(prefix = "Bearer ")) {
val deviceId = path.drop(17)
val authToken = authHeader.drop(7)
val auth = UsernamePasswordAuthenticationToken(deviceId, authToken)
Mono.just(SecurityContextImpl(auth))
} else {
Mono.empty()
}
}
override fun save(exchange: ServerWebExchange?, context: SecurityContext?): Mono<Void> {
return Mono.empty()
}
}
Two questions arise:
Is ServerSecurityContextRepository good place to obtain username and token from exchange - or there is a better place to do it?
Where should I perform authentication (check token and username against mongo collection)?
My custom AuthenticationManager does not get called anywhere. Should I do everything inside ServerSecurityContextRepository or perform user and token validation inside ReactiveAuthenticationManager? Or maybe other class would be even more suitable?
It turns out that some tutorials on the web are plain wrong.
I've managed to configure everything using following code:
class DeviceAuthenticationConverter : Function<ServerWebExchange, Mono<Authentication>> {
override fun apply(exchange: ServerWebExchange): Mono<Authentication> {
val authHeader: String? = exchange.request.headers.getFirst(HttpHeaders.AUTHORIZATION)
val path: String? = exchange.request.uri.path
return when {
isValidPath(path) && isValidHeader(authHeader) -> Mono.just(UsernamePasswordAuthenticationToken(path?.drop(17), authHeader?.drop(7)))
else -> Mono.empty()
}
}
private fun isValidPath(path: String?) = path != null && path.startsWith(API_MEASUREMENT)
private fun isValidHeader(authHeader: String?) = authHeader != null && authHeader.startsWith(prefix = "Bearer ")
}
And config:
#EnableWebFluxSecurity
class SecurityConfig {
companion object {
const val API_MEASUREMENT = "/api/measurement/"
const val API_MEASUREMENT_PATH = "$API_MEASUREMENT**"
const val DEVICE = "DEVICE"
const val DEVICE_ID = "deviceId"
}
#Bean
fun securityWebFilterChain(http: ServerHttpSecurity, authenticationManager: ReactiveAuthenticationManager) =
http
.httpBasic().disable()
.formLogin().disable()
.csrf().disable()
.logout().disable()
.authorizeExchange().pathMatchers(API_MEASUREMENT_PATH).hasRole(DEVICE)
.anyExchange().permitAll().and().addFilterAt(authenticationWebFilter(authenticationManager), AUTHENTICATION).build()
#Bean
fun userDetailsService(tokenRepository: TokenRepository) = MongoDeviceTokenReactiveUserDetailsService(tokenRepository)
#Bean
fun tokenRepository(template: ReactiveMongoTemplate, passwordEncoder: PasswordEncoder) = MongoTokenRepository(template, passwordEncoder)
#Bean
fun tokenFacade(tokenRepository: TokenRepository) = TokenFacade(tokenRepository)
#Bean
fun authManager(userDetailsService: ReactiveUserDetailsService) = UserDetailsRepositoryReactiveAuthenticationManager(userDetailsService)
private fun authenticationWebFilter(reactiveAuthenticationManager: ReactiveAuthenticationManager) =
AuthenticationWebFilter(reactiveAuthenticationManager).apply {
setAuthenticationConverter(DeviceAuthenticationConverter())
setRequiresAuthenticationMatcher(
ServerWebExchangeMatchers.pathMatchers(HttpMethod.POST, API_MEASUREMENT_PATH)
)
}
#Bean
fun passwordEncoder() = PasswordEncoderFactories.createDelegatingPasswordEncoder()
}

Resources