Integrating Keycloak and Spring Security with Basic Authentication
Integrating Keycloak and Spring Security with Basic Authentication.
In this guide, we’ll explore integrating Spring Boot (with Spring Security) and Keycloak using basic authentication flow.
I found this approach particularly useful when creating a server acting as a Maven repository. To ensure smooth interaction with the Gradle client, it needed to gracefully handle authentication attempts without an authentication header (respond with a 401 Unauthorized and a correct WWW-Authenticate header), keep the connection open, and handle the next request containing an Authorization header with basic authentication.
Key Insight: This setup runs as bearer-only, but we don’t want to be redirected to the login page that Keycloak provides.
This configuration has been tested against Keycloak 8.0.1.
Using Spring Boot 2.3.4.RELEASE, the following dependencies are used:
implementation platform("org.keycloak.bom:keycloak-adapter-bom:4.8.3.Final")
implementation "org.springframework.boot:spring-boot-starter-security"
implementation "org.keycloak:keycloak-spring-boot-2-adapter"
implementation "org.keycloak:spring-boot-container-bundle"
Create a configuration class that looks something like this:
@KeycloakConfiguration
public class SecurityConfig extends KeycloakWebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
super.configure(http);
http.authorizeRequests()
.antMatchers("/actuator/health").permitAll()
.antMatchers("/actuator/info").permitAll()
.anyRequest().authenticated();
}
@Override
protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
return new NullAuthenticatedSessionStrategy();
}
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) {
KeycloakAuthenticationProvider keycloakAuthenticationProvider = keycloakAuthenticationProvider();
keycloakAuthenticationProvider.setGrantedAuthoritiesMapper(new SimpleAuthorityMapper());
auth.authenticationProvider(keycloakAuthenticationProvider);
}
}
This configures the Keycloak integration, securing all endpoints except /actuator/health and /actuator/info. The sessionAuthenticationStrategy
method is overridden to use NullAuthenticatedSessionStrategy
since no sessions are used (stateless auth). Additionally, the KeycloakAuthenticationProvider
is configured to use the SimpleAuthorityMapper
, allowing easy use of roles configured in Keycloak as authorities in Spring Security.
Important: Note the super.configure(http)
call in the configure
method.
Spring Boot Configuration
When using Spring Boot, add the following configuration to this class:
@Bean
public KeycloakConfigResolver keycloakConfigResolver() {
return new KeycloakSpringBootConfigResolver();
}
// https://issues.redhat.com/browse/KEYCLOAK-8725
@Bean
@Override
@ConditionalOnMissingBean(HttpSessionManager.class)
protected HttpSessionManager httpSessionManager() {
return new HttpSessionManager();
}
// Needed because Spring Boot eagerly registers filter beans to the web application context. This prevents them being registered twice.
@Bean
public FilterRegistrationBean<KeycloakAuthenticationProcessingFilter> keycloakAuthenticationProcessingFilterRegistrationBean(
KeycloakAuthenticationProcessingFilter filter) {
FilterRegistrationBean<KeycloakAuthenticationProcessingFilter> registrationBean = new FilterRegistrationBean<>(filter);
registrationBean.setEnabled(false);
return registrationBean;
}
// Needed because Spring Boot eagerly registers filter beans to the web application context. This prevents them being registered twice.
@Bean
public FilterRegistrationBean<KeycloakPreAuthActionsFilter> keycloakPreAuthActionsFilterRegistrationBean(
KeycloakPreAuthActionsFilter filter) {
FilterRegistrationBean<KeycloakPreAuthActionsFilter> registrationBean = new FilterRegistrationBean<>(filter);
registrationBean.setEnabled(false);
return registrationBean;
}
// Needed because Spring Boot eagerly registers filter beans to the web application context. This prevents them being registered twice.
@Bean
public FilterRegistrationBean<KeycloakAuthenticatedActionsFilter> keycloakAuthenticatedActionsFilterBean(
KeycloakAuthenticatedActionsFilter filter) {
FilterRegistrationBean<KeycloakAuthenticatedActionsFilter> registrationBean = new FilterRegistrationBean<>(filter);
registrationBean.setEnabled(false);
return registrationBean;
}
// Needed because Spring Boot eagerly registers filter beans to the web application context. This prevents them being registered twice.
@Bean
public FilterRegistrationBean<KeycloakSecurityContextRequestFilter> keycloakSecurityContextRequestFilterBean(
KeycloakSecurityContextRequestFilter filter) {
FilterRegistrationBean<KeycloakSecurityContextRequestFilter> registrationBean = new FilterRegistrationBean<>(filter);
registrationBean.setEnabled(false);
return registrationBean;
}
The KeycloakSpringBootConfigResolver
allows Keycloak configuration properties to be specified as regular Spring properties instead of using a keycloak.json configuration file.
These properties are used to configure the Keycloak integration at runtime:
keycloak.ssl-required=external
keycloak.principal-attribute=preferred_username
keycloak.auth-server-url=https://your.keycloak.url/auth
keycloak.realm=your-keycloak-realm
keycloak.public-client=false
keycloak.resource=your-keycloak-client-name
keycloak.credentials.secret=your-client-secret
keycloak.bearer-only=true
keycloak.enable-basic-auth=true
To allow basic authentication, set keycloak.enable-basic-auth to true, and set keycloak.bearer-only to true to disable redirects to the Keycloak provided login page.
If we connect with an Authorization: Basic header this works, but if we do not we get an 401 Unauthorized but with the wrong WWW-Authenticate header. Due to this issue I could not get a proper interaction between my repository and my Gradle client. This is because KeycloakAuthenticationEntryPoint is implemented like:
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
HttpFacade facade = new SimpleHttpFacade(request, response);
if (apiRequestMatcher.matches(request) || adapterDeploymentContext.resolveDeployment(facade).isBearerOnly()) {
commenceUnauthorizedResponse(request, response);
} else {
commenceLoginRedirect(request, response);
}
}
protected void commenceLoginRedirect(HttpServletRequest request, HttpServletResponse response) throws IOException {
String contextAwareLoginUri = request.getContextPath() + loginUri;
log.debug("Redirecting to login URI {}", contextAwareLoginUri);
response.sendRedirect(contextAwareLoginUri);
}
protected void commenceUnauthorizedResponse(HttpServletRequest request, HttpServletResponse response) throws IOException {
response.addHeader(HttpHeaders.WWW_AUTHENTICATE, String.format("Bearer realm=\"%s\"", realm));
response.sendError(HttpStatus.UNAUTHORIZED.value(), HttpStatus.UNAUTHORIZED.getReasonPhrase());
}
Since we have to set keycloak.bearer-only to true to not be redirected, we get a 401 Unauthorized with WWW-Authenticate: Bearer realm=”your-keycloak-realm”. In our use-case this should be: WWW-Authenticate: Basic realm=”your-keycloak-realm”, however the Bearer part is hardcoded in the keycloak spring security adapter.
Custom AuthenticationEntryPoint
To address this issue, implement a custom AuthenticationEntryPoint
based on the existing Keycloak one:
@Override
protected AuthenticationEntryPoint authenticationEntryPoint() throws Exception {
return new KeycloakWithBasicAuthAuthenticationEntryPoint(adapterDeploymentContext());
}
public class KeycloakWithBasicAuthAuthenticationEntryPoint extends KeycloakAuthenticationEntryPoint {
private String realm = "Unknown";
public KeycloakWithBasicAuthAuthenticationEntryPoint(AdapterDeploymentContext adapterDeploymentContext) {
super(adapterDeploymentContext);
}
public KeycloakWithBasicAuthAuthenticationEntryPoint(AdapterDeploymentContext adapterDeploymentContext, RequestMatcher apiRequestMatcher) {
super(adapterDeploymentContext, apiRequestMatcher);
}
@Override
protected void commenceUnauthorizedResponse(HttpServletRequest request, HttpServletResponse response) throws IOException {
response.addHeader(HttpHeaders.WWW_AUTHENTICATE, "Basic realm=\""+realm+"\"");
response.sendError(HttpStatus.UNAUTHORIZED.value(), HttpStatus.UNAUTHORIZED.getReasonPhrase());
}
@Override
public void setRealm(String realm) {
Assert.notNull(realm, "realm cannot be null");
this.realm = realm;
}
}
Full configuration
@KeycloakConfiguration
public class SecurityConfig extends KeycloakWebSecurityConfigurerAdapter {
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) {
KeycloakAuthenticationProvider keycloakAuthenticationProvider = keycloakAuthenticationProvider();
keycloakAuthenticationProvider.setGrantedAuthoritiesMapper(new SimpleAuthorityMapper());
auth.authenticationProvider(keycloakAuthenticationProvider);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
super.configure(http);
http.authorizeRequests()
.antMatchers("/actuator/health").permitAll()
.antMatchers("/actuator/info").permitAll()
.anyRequest().authenticated();
}
@Override
protected AuthenticationEntryPoint authenticationEntryPoint() throws Exception {
return new KeycloakWithBasicAuthAuthenticationEntryPoint(adapterDeploymentContext());
}
@Override
protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
return new NullAuthenticatedSessionStrategy();
}
@Bean
public KeycloakConfigResolver keycloakConfigResolver() {
return new KeycloakSpringBootConfigResolver();
}
// https://issues.redhat.com/browse/KEYCLOAK-8725
@Bean
@Override
@ConditionalOnMissingBean(HttpSessionManager.class)
protected HttpSessionManager httpSessionManager() {
return new HttpSessionManager();
}
// Needed because Spring Boot eagerly registers filter beans to the web application context. This prevents them being registered twice.
@Bean
public FilterRegistrationBean<KeycloakAuthenticationProcessingFilter> keycloakAuthenticationProcessingFilterRegistrationBean(
KeycloakAuthenticationProcessingFilter filter) {
FilterRegistrationBean<KeycloakAuthenticationProcessingFilter> registrationBean = new FilterRegistrationBean<>(filter);
registrationBean.setEnabled(false);
return registrationBean;
}
// Needed because Spring Boot eagerly registers filter beans to the web application context. This prevents them being registered twice.
@Bean
public FilterRegistrationBean<KeycloakPreAuthActionsFilter> keycloakPreAuthActionsFilterRegistrationBean(
KeycloakPreAuthActionsFilter filter) {
FilterRegistrationBean<KeycloakPreAuthActionsFilter> registrationBean = new FilterRegistrationBean<>(filter);
registrationBean.setEnabled(false);
return registrationBean;
}
// Needed because Spring Boot eagerly registers filter beans to the web application context. This prevents them being registered twice.
@Bean
public FilterRegistrationBean<KeycloakAuthenticatedActionsFilter> keycloakAuthenticatedActionsFilterBean(
KeycloakAuthenticatedActionsFilter filter) {
FilterRegistrationBean<KeycloakAuthenticatedActionsFilter> registrationBean = new FilterRegistrationBean<>(filter);
registrationBean.setEnabled(false);
return registrationBean;
}
// Needed because Spring Boot eagerly registers filter beans to the web application context. This prevents them being registered twice.
@Bean
public FilterRegistrationBean<KeycloakSecurityContextRequestFilter> keycloakSecurityContextRequestFilterBean(
KeycloakSecurityContextRequestFilter filter) {
FilterRegistrationBean<KeycloakSecurityContextRequestFilter> registrationBean = new FilterRegistrationBean<>(filter);
registrationBean.setEnabled(false);
return registrationBean;
}
}