I have an issue with Spring Boot security. What I want is to have two different authentication for the same project at the same time in Spring Boot. The one is SSO (keycloak authentication) for all path except '/download/export/*' , the other one is Spring Boot basic authentication. Here is my configuration file:
@Configuration
@EnableWebSecurityp
public class MultiHttpSecurityConfig {
@Configuration
@Order(1)
public static class DownloadableExportFilesSecurityConfig extends WebSecurityConfigurerAdapter
{
@Override
protected void configure(HttpSecurity http) throws Exception
{
http
.antMatcher("/download/export/test")
.authorizeRequests()
.anyRequest().hasRole("USER1")
.and()
.httpBasic(); }
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception
{
auth.inMemoryAuthentication()
.withUser("user").password("password1").roles("USER1");
}
}
@Configuration
@ComponentScan(basePackageClasses = KeycloakSecurityComponents.class)
public static class SecurityConfig extends KeycloakWebSecurityConfigurerAdapter
{
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception
{
auth.authenticationProvider(keycloakAuthenticationProvider());
}
@Bean
@Override
protected SessionAuthenticationStrategy sessionAuthenticationStrategy()
{
return new RegisterSessionAuthenticationStrategy(new SessionRegistryImpl());
}
@Override
protected void configure(HttpSecurity http) throws Exception
{
super.configure(http);
http
.regexMatcher("^(?!.*/download/export/test)")
.authorizeRequests()
.anyRequest().hasAnyRole("ADMIN", "SUPER_ADMIN")
.and()
.logout().logoutSuccessUrl("/bye");
}
}
The problem with above code is the following: If I request url '/download/export/test', than it asks me the username/password (Basic authentication). After successful login it asks me again for username/password (but this time keycloak authentication) , even if the requested url is excluded from SecurityConfig (Keycloak Adapter).
It gives me only a warning:
2016-06-20 16:31:28.771 WARN 6872 --- [nio-8087-exec-6] o.k.a.s.token.SpringSecurityTokenStore : Expected a KeycloakAuthenticationToken, but found org.springframework.security.authentication.UsernamePasswordAuthenticationToken@3fb541cc: Principal: org.springframework.security.core.userdetails.User@36ebcb: Username: user; Password: [PROTECTED]; Enabled: true; AccountNonExpired: true; credentialsNonExpired: true; AccountNonLocked: true; Granted Authorities: ROLE_USER1; Credentials: [PROTECTED]; Authenticated: true; Details: org.springframework.security.web.authentication.WebAuthenticationDetails@957e: RemoteIpAddress: 127.0.0.1; SessionId: 4C1BD3EA1FD7F50477548DEC4B5B5162; Granted Authorities: ROLE_USER1
Do you have any ideas how to use keycloak and basic authentication together?
Many many thanks! Carlo
Yes that's possible for clients with Access Type: confidential and Direct Access Grants Enabled .
Which authentication is best in Spring boot? You can use custom token based implementation, you can create a custom token that you can store in DB but JWT is a good choice.
The problem you're having is that KeycloakAuthenticationProcessingFilter.java
intercepts every request with HTTP Authorization header. If your request is not authenticated with Keycloak (Even if you're authenticated with any other authentication provider! - in your case with basic authentication) you'll always either be redirected to Keycloak's login page (in your case) or get 401 Unauthorized (if your Keycloak client in keycloak.json
is configured to bearer-only).
By default KeycloakAuthenticationProcessingFilter.java
is invoked if request matches KeycloakAuthenticationProcessingFilter.DEFAULT_REQUEST_MATCHER
:
public static final RequestMatcher DEFAULT_REQUEST_MATCHER =
new OrRequestMatcher(
new AntPathRequestMatcher(DEFAULT_LOGIN_URL),
new RequestHeaderRequestMatcher(AUTHORIZATION_HEADER),
new QueryParamPresenceRequestMatcher(OAuth2Constants.ACCESS_TOKEN)
);
This means that any request that matches DEFAULT_LOGIN_URL (/sso/login) OR contains Authorization HTTP header (in your case) OR has access_token as query parameter, will be processed by KeycloakAuthenticationProcessingFilter.java
.
That's why you have to replace RequestHeaderRequestMatcher(AUTHORIZATION_HEADER)
with your own implementation that will skip invocation of KeycloakAuthenticationProcessingFilter.java
when request is authenticated with basic authentication.
Below is a full solution that enables you to use both Basic authentication and Keycloak authentication simultaneously on the same paths. Pay special attention to IgnoreKeycloakProcessingFilterRequestMatcher implementation which is replacing default RequestHeaderRequestMatcher. This matcher will match only requests containing Authorization HTTP header which value is not prefixed with "Basic "
.
In example below, user with role TESTER
can access /download/export/test
while all other paths are available to users with ADMIN
or SUPER_ADMIN
roles (which I assume, in your case, are accounts on Keycloak server).
@KeycloakConfiguration
public class MultiHttpSecurityConfig extends KeycloakWebSecurityConfigurerAdapter {
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("tester")
.password("testerPassword")
.roles("TESTER");
auth.authenticationProvider(keycloakAuthenticationProvider());
}
@Bean
@Override
protected KeycloakAuthenticationProcessingFilter keycloakAuthenticationProcessingFilter() throws Exception {
RequestMatcher requestMatcher =
new OrRequestMatcher(
new AntPathRequestMatcher(DEFAULT_LOGIN_URL),
new QueryParamPresenceRequestMatcher(OAuth2Constants.ACCESS_TOKEN),
// We're providing our own authorization header matcher
new IgnoreKeycloakProcessingFilterRequestMatcher()
);
return new KeycloakAuthenticationProcessingFilter(authenticationManagerBean(), requestMatcher);
}
// Matches request with Authorization header which value doesn't start with "Basic " prefix
private class IgnoreKeycloakProcessingFilterRequestMatcher implements RequestMatcher {
IgnoreKeycloakProcessingFilterRequestMatcher() {
}
public boolean matches(HttpServletRequest request) {
String authorizationHeaderValue = request.getHeader("Authorization");
return authorizationHeaderValue != null && !authorizationHeaderValue.startsWith("Basic ");
}
}
@Override
protected void configure(HttpSecurity http) throws Exception {
super.configure(http);
http.authorizeRequests()
.antMatchers("/download/export/test")
.hasRole("TESTER")
.anyRequest()
.hasAnyRole("ADMIN", "SUPER_ADMIN")
.and()
.httpBasic();
}
@Bean
@Override
protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
return new RegisterSessionAuthenticationStrategy(new SessionRegistryImpl());
}
}
I solved this by configuring an exception on KeycloakAuthenticationProcessingFilter for the path:
...
@Configuration
@Order(2)
static class KeyCloakSecurityConfig extends KeycloakWebSecurityConfigurerAdapter {
@Bean
public KeycloakAuthenticationProcessingFilter keycloakAuthenticationProcessingFilter() throws Exception {
KeycloakAuthenticationProcessingFilter filter = new KeycloakAuthenticationProcessingFilter(
authenticationManagerBean()
, new AndRequestMatcher(
KeycloakAuthenticationProcessingFilter.DEFAULT_REQUEST_MATCHER,
new NegatedRequestMatcher(new AntPathRequestMatcher(YOUR_BASIC_AUTHD_PATH))));
filter.setSessionAuthenticationStrategy(sessionAuthenticationStrategy());
return filter;
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With