Summary
The whole context can be found on Stackoverflow.
Actual Behavior
Despite configuring a WebClient to propagate the access token after OIDC logging of a Spring Boot Oauth2 Client application...
@Bean
fun webClient(): WebClient {
return WebClient.builder()
.filter(ServletBearerExchangeFilterFunction())
.build()
}
The token is not propagated since ServletBearerExchangeFilterFunction doesn't have access to the access token from OAuth2AuthenticationToken:
private Mono<AbstractOAuth2Token> oauth2Token() {
return Mono.subscriberContext()
.flatMap(this::currentAuthentication)
.filter(authentication -> authentication.getCredentials() instanceof AbstractOAuth2Token)
.map(Authentication::getCredentials)
.cast(AbstractOAuth2Token.class);
}
Expected Behavior
The access token is propagated by the WebClient.
Configuration
https://github.com/codependent/spring-boot-2-oidc-sample
Version
5.2.x
Sample
https://github.com/codependent/spring-boot-2-oidc-sample
Comment From: codependent
The point here is that ServletBearerExchangeFilterFunction is meant to be used in a Resource Server.
However, in my opinion, OIDC clients should also be able to propagate the access token to downstream services. I've modified the ServletBearerExchangeFilterFunction to allow this clients to propagate the access token, retrieving it from the OAuth2AuthorizedClientRepository:
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()
}
}
Would you consider including something like this for pure OAuth2/OIDC login applications?
Comment From: jzheaux
@codependent Have you considered ServerOAuth2AuthorizedClientExchangeFilterFunction?
https://github.com/spring-projects/spring-security/blob/master/samples/boot/oauth2webclient-webflux/src/main/java/sample/config/WebClientConfig.java#L38
Comment From: codependent
Oops, I can't believe I missed ServerOAuth2AuthorizedClientExchangeFilterFunction in the documentation. Thanks @jzheaux.
Comment From: Vostan
@codeconsole
I guess as you mentioned in the above comment about ServletBearerExchangeFilterFunction should work with ResourServer. you might be able to help me with this issue, that would be highly appreciated.
https://stackoverflow.com/questions/61511710/spring-cloud-gateway-spring-security-resource-server