I have configured spring security with keycloak using the Oauth2 client feature in the BFF, keycloak will play the role of an idp broker and it is configured successfully with Microsoft (we will be add support later for google, facebook, etc...) every thing work as expected!
we have a requirement in our app that the users should not land on the keycloak login page, instead we will add custom "login with Micorosoft" (or with his/her preferred login provider) button in our ui app and user will be redirected to the Microsoft login page to connect.
My OAuth2 setup (and it is working):
The question is: how to bypass keycloak login page and redirect user directly to his choice of provider that we already configured and support? How we can do that from srping security so we can tell keycloak to redirect to a specific idp login page?
What I have found so far:
Any help please ?
You already found the answer to this question: provide a kc_idp_hint
parameter with the authorization request.
kc_idp_hint
for Keycloak, audience
for Auth0, etc.)?This is done with a custom (Server)OAuth2AuthorizationRequestResolver
.
I'll expose solutions relying on an OAuth2 client registration per login option, with a sample for:
default-registration-id
as ID)google-registration-id
with the assumption that you named google
the IDP in Keycloak)facebook-registration-id
with the assumption that you named facebook
the IDP in Keycloak)This implies configuration properties looking something like that:
oauth2-issuer: change-me
oauth2-client-id: change-me
oauth2-client-secret: change-me
scope: "openid profile email offline_access"
spring:
security:
oauth2:
client:
provider:
keycloak:
issuer-uri: ${oauth2-issuer}
user-name-attribute: preferred_username
registration:
# registration for direct access to Google login page
google-registration-id:
provider: keycloak
client-id: ${oauth2-client-id}
client-secret: ${oauth2-client-secret}
authorization-grant-type: authorization_code
scope: $scope
# registration for direct access to Facebook login page
facebook-registration-id:
provider: keycloak
client-id: ${oauth2-client-id}
client-secret: ${oauth2-client-secret}
authorization-grant-type: authorization_code
scope: $scope
# registration for accessing Keycloak login page
default-registration-id:
provider: keycloak
client-id: ${oauth2-client-id}
client-secret: ${oauth2-client-secret}
authorization-grant-type: authorization_code
scope: $scope
Depending on which OAuth2 client registration the frontend activates (by redirecting the user to the BFF /oauth2/authorization/{registration-id}
endpoint), the user will see a different login screen.
As I guess that your Spring BFF configured as a confidential OAuth2 client is a reactive Spring Cloud Gateway instance, I'll focus on the reactive authorization-request resolver: ServerOAuth2AuthorizationRequestResolver
.
Two options depending you are using just spring-boot-starter-oauth2-client
, or spring-addons-starter-oidc
(a starter of mine) in addition to it.
spring-addons-starter-oidc
Let's start simple. All you need in that case is adding the kc_idp_hint
for each registration ID in application properties:
com:
c4-soft:
springaddons:
oidc:
client:
authorization-params:
google-registration-id:
kc_idp_hint: google
facebook-registration-id:
kc_idp_hint: facebook
spring-boot-starter-oauth2-client
In that case, you'll have to write a custom authorization-request resolver yourself and then configure the security filter-chain with it.
Here is a possible impl:
@Component
public class KcIdpHintServerOAuth2AuthorizationRequestResolver implements ServerOAuth2AuthorizationRequestResolver {
private final DefaultServerOAuth2AuthorizationRequestResolver defaultDelegate;
private final Map<String, ServerOAuth2AuthorizationRequestResolver> authorizationRequestResolversByRegistrationId;
public KcIdpHintServerOAuth2AuthorizationRequestResolver(ReactiveClientRegistrationRepository clientRegistrationRepository) {
this.defaultDelegate = new DefaultServerOAuth2AuthorizationRequestResolver(clientRegistrationRepository);
// Rather than a static conf like that, you might parse a map from custom configuration properties
this.authorizationRequestResolversByRegistrationId = Map.of(
"google-registration-id", authorizationRequestResolverWithHintFor(clientRegistrationRepository, "google"),
"facebook-registration-id", authorizationRequestResolverWithHintFor(clientRegistrationRepository, "facebook"));
}
@Override
public Mono<OAuth2AuthorizationRequest> resolve(ServerWebExchange exchange) {
return defaultDelegate.resolve(exchange);
}
@Override
public Mono<OAuth2AuthorizationRequest> resolve(ServerWebExchange exchange, String clientRegistrationId) {
final var delegate = authorizationRequestResolversByRegistrationId.getOrDefault(clientRegistrationId, defaultDelegate);
return delegate.resolve(exchange, clientRegistrationId);
}
private
ServerOAuth2AuthorizationRequestResolver
authorizationRequestResolverWithHintFor(ReactiveClientRegistrationRepository clientRegistrationRepository, String hint) {
final var authorizationRequestResolver = new DefaultServerOAuth2AuthorizationRequestResolver(clientRegistrationRepository);
authorizationRequestResolver.setAuthorizationRequestCustomizer(
oauth2AuthorizationRequestBuilder -> oauth2AuthorizationRequestBuilder.additionalParameters(Map.of("kc_idp_hint", hint)));
return authorizationRequestResolver;
}
}
@Bean
SecurityWebFilterChain clientSecurityFilterChain(
ServerHttpSecurity http,
ServerOAuth2AuthorizationRequestResolver authorizationRequestResolver) {
http.oauth2Login(oauth2 -> {
oauth2.authorizationRequestResolver(authorizationRequestResolver);
});
...
return http.build();
}