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?