I'm running Keycloak 24.0.0 with a self signed certificate.
My springboot application authenticates against Keycloak using the client secret authentication method and authorisation code grant type (via spring security 6.2):
private ClientRegistration keycloakClientRegistration() {
return ClientRegistration.withRegistrationId("keycloak")
.clientId(keycloakInitializer.clientId())
.clientSecret(keycloakInitializer.clientSecret())
.authorizationUri("%s/realms/%s/protocol/openid-connect/auth".formatted(keycloakInitializer.serverUrl(), keycloakInitializer.realm()))
.tokenUri("%s/realms/%s/protocol/openid-connect/token".formatted(keycloakInitializer.serverUrl(), keycloakInitializer.realm()))
.userInfoUri("%s/realms/%s/protocol/openid-connect/userinfo".formatted(keycloakInitializer.serverUrl(), keycloakInitializer.realm()))
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_POST) // Check if this is correct
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.redirectUri("{baseUrl}/login/oauth2/code/{registrationId}")
.scope(Scopes.names())
.userNameAttributeName(IdTokenClaimNames.SUB) // Check if this is correct
.issuerUri("%s/realms/%s".formatted(keycloakInitializer.serverUrl(), keycloakInitializer.realm()))
.jwkSetUri("%s/realms/%s/protocol/openid-connect/certs".formatted(keycloakInitializer.serverUrl(), keycloakInitializer.realm()))
.clientName(keycloakInitializer.clientName())
.build();
}
I'm overriding the JwtDecoder with a custom one that accepts a RestTemplate that optionally (based on config) accepts self-signed certs and skips hostname validation:
@Bean
public JwtDecoder jwtDecoder(RestTemplate restTemplate) {
return NimbusJwtDecoder.withIssuerLocation("%s/realms/%s".formatted(keycloakInitializer.serverUrl(), keycloakInitializer.realm()))
.restOperations(restTemplate).build();
}
@Bean
public RestTemplate restTemplate(ClientHttpRequestFactory clientHttpRequestFactory) {
return new RestTemplate(clientHttpRequestFactory);
}
@Bean
public ClientHttpRequestFactory clientHttpRequestFactory(SslBundles sslBundles, @Value("${keycloak.accept-untrusted-certs}") boolean acceptUntrustedCerts) {
SSLFactory defaultSslFactory = SSLFactory.builder()
.withUnsafeTrustMaterial()
.withUnsafeHostnameVerifier()
.build();
CloseableHttpClient httpClient;
if (acceptUntrustedCerts) {
LOGGER.info("Accepting untrusted certs for keycloak and ignoring hostname verification.");
httpClient = HttpClients.custom().setConnectionManager(PoolingHttpClientConnectionManagerBuilder.create()
.setSSLSocketFactory(SSLConnectionSocketFactoryBuilder.create()
.setSslContext(defaultSslFactory.getSslContext())
.setHostnameVerifier(defaultSslFactory.getHostnameVerifier())
.build())
.build())
.build();
} else {
try {
SSLContext sslContext = sslBundles.getBundle("keycloak").createSslContext();
httpClient = HttpClients.custom().setConnectionManager(PoolingHttpClientConnectionManagerBuilder.create()
.setSSLSocketFactory(SSLConnectionSocketFactoryBuilder.create()
.setSslContext(sslContext)
.build())
.build())
.build();
LOGGER.info("Accepting supplied cert for keycloak and applying hostname verification.");
} catch (NoSuchSslBundleException e) {
LOGGER.info("Could not find an SSL Context for keycloak. Using default system SSL settings.");
httpClient = HttpClients.createDefault();
}
}
return new HttpComponentsClientHttpRequestFactory(httpClient);
}
However, it looks like the default OAuth2UserService instantiates its own RestTemplate, so isn't using mine.
I've tried to override this by providing an OAuth2UserService with my own RestTemplate:
private OAuth2UserService<OAuth2UserRequest, OAuth2User> oauth2UserService(RestTemplate restTemplate) {
DefaultOAuth2UserService defaultOAuth2UserService = new DefaultOAuth2UserService();
defaultOAuth2UserService.setRestOperations(restTemplate);
return defaultOAuth2UserService;
}
private OAuth2UserService<OidcUserRequest, OidcUser> oidcUserService(OAuth2UserService<OAuth2UserRequest, OAuth2User> userService) {
OidcUserService oidcUserService = new OidcUserService();
oidcUserService.setOauth2UserService(userService);
return oidcUserService;
}
@Bean
public SecurityFilterChain resourceServerFilterChain(HttpSecurity http, RestTemplate restTemplate) throws Exception {
OAuth2UserService<OAuth2UserRequest, OAuth2User> userService = oauth2UserService(restTemplate);
http.cors(Customizer.withDefaults())
.csrf((csrf) -> csrf
.csrfTokenRepository(new CookieCsrfTokenRepository())
.csrfTokenRequestHandler(new SpaCsrfTokenRequestHandler())
)
.authorizeHttpRequests(
auth -> auth
.requestMatchers(new AntPathRequestMatcher("/api/**"))
.authenticated()
.anyRequest()
.permitAll()
)
.addFilterAfter(new CsrfCookieFilter(), BasicAuthenticationFilter.class);
http.oauth2ResourceServer((oauth2) -> oauth2.jwt(Customizer.withDefaults()));
http.oauth2Client(Customizer.withDefaults());
http.oauth2Login((oauth2) -> oauth2
.userInfoEndpoint(userInfo -> userInfo
.oidcUserService(oidcUserService(userService))
.userService(userService)
))
.logout(logout -> logout.addLogoutHandler(keycloakLogoutHandler).logoutSuccessUrl("/"));
return http.build();
}
But whenever I authenticate against keycloak, I get the following error:
[invalid_token_response] An error occurred while attempting to retrieve the OAuth 2.0 Access Token Response: I/O error on POST request for "https://keycloak:8443/realms/MyRealm/protocol/openid-connect/token": PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
If I override some other endpoint services to use a custom RestTemplate, like so:
@Bean
public SecurityFilterChain resourceServerFilterChain(HttpSecurity http, ClientHttpRequestFactory clientHttpRequestFactory) throws Exception {
OAuth2UserService<OAuth2UserRequest, OAuth2User> userService = oauth2UserService(clientHttpRequestFactory);
http.cors(Customizer.withDefaults())
.csrf((csrf) -> csrf
.csrfTokenRepository(new CookieCsrfTokenRepository())
.csrfTokenRequestHandler(new SpaCsrfTokenRequestHandler())
)
.authorizeHttpRequests(
auth -> auth
.requestMatchers(new AntPathRequestMatcher("/api/**"))
.authenticated()
.anyRequest()
.permitAll()
)
.addFilterAfter(new CsrfCookieFilter(), BasicAuthenticationFilter.class);
http.oauth2ResourceServer((oauth2) -> oauth2.jwt(Customizer.withDefaults()));
http.oauth2Client(Customizer.withDefaults());
http.oauth2Login((oauth2) -> oauth2
.userInfoEndpoint(userInfo -> userInfo
.oidcUserService(oidcUserService(userService))
.userService(userService)
).tokenEndpoint(token -> token
.accessTokenResponseClient(authorizationCodeTokenResponseClient(clientHttpRequestFactory))
))
.logout(logout -> logout.addLogoutHandler(keycloakLogoutHandler).logoutSuccessUrl("/"));
return http.build();
}
private OAuth2UserService<OAuth2UserRequest, OAuth2User> oauth2UserService(ClientHttpRequestFactory clientHttpRequestFactory) {
RestTemplate restTemplate = new RestTemplate(clientHttpRequestFactory);
restTemplate.setErrorHandler(new OAuth2ErrorResponseErrorHandler());
DefaultOAuth2UserService defaultOAuth2UserService = new DefaultOAuth2UserService();
defaultOAuth2UserService.setRestOperations(restTemplate);
return defaultOAuth2UserService;
}
private OAuth2UserService<OidcUserRequest, OidcUser> oidcUserService(OAuth2UserService<OAuth2UserRequest, OAuth2User> userService) {
OidcUserService oidcUserService = new OidcUserService();
oidcUserService.setOauth2UserService(userService);
return oidcUserService;
}
private DefaultAuthorizationCodeTokenResponseClient authorizationCodeTokenResponseClient(ClientHttpRequestFactory clientHttpRequestFactory) {
RestTemplate restTemplate = new RestTemplate(clientHttpRequestFactory);
restTemplate.setMessageConverters(Arrays.asList(new FormHttpMessageConverter(), new OAuth2AccessTokenResponseHttpMessageConverter()));
restTemplate.setErrorHandler(new OAuth2ErrorResponseErrorHandler());
DefaultAuthorizationCodeTokenResponseClient tokenResponseClient = new DefaultAuthorizationCodeTokenResponseClient();
tokenResponseClient.setRestOperations(restTemplate);
return tokenResponseClient;
}
then I get a different error:
[invalid_id_token] An error occurred while attempting to decode the Jwt: Couldn't retrieve remote JWK set: org.springframework.web.client.ResourceAccessException: I/O error on GET request for "https://keycloak:8443/realms/MyRealm/protocol/openid-connect/certs": PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
This feels like lots of overriding just to tell it to accept my cert.
How do I configure the OAuth2 client to accept self-signed certificates and ignore hostname validation without importing the cert into the trust store?
Don't hack around the hostname that much.
cacert
files. I propose a script for that.hostname
property. In a docker compose file, this is done by defining the KC_HOSTNAME
"environment" (and maybe KC_HOSTNAME_STRICT_BACKCHANNEL: true
too). This will instruct Keycloak what to use as host in the URIs it puts in the OpenID configuration.isser-uri
, let the auto-configuration from OpenID conf set the URIs for JWK, authorization, token, userinfo, etc.).And that's it. No need to hack the conf around and provide this many @Bean
overrides.