Single Sign-On and Basic Auth with Spring Security
The requirements for some of our apps have led to interesting explorations of the Spring Security configuration. I hope to show you by way of example how non-standard implementations can still be achieved with elegance once you understand the Spring Security architecture.
A common pattern we have at Auto Trader is to have internal APIs protected by HTTP Basic Authentication (Basic Auth). A more recent development is to use an internal single sign-on (SSO) mechanism. As well as being more convenient for developers, it also means other internal users can gain access and be traced through the internal systems, without having to create a new Basic Auth credential for each new use case.
We deploy apps to Kubernetes/GCP using our in-house delivery platform. SSO is a feature of our internal platform: once an internal user is signed in, apps will authenticate that user’s requests based on the presence of a special HTTP header.
In many cases, we still want to support Basic Auth: either for legacy reasons or because we have external clients (with no SSO credentials) that get routed there after arriving through a secure gateway. Therefore there is a requirement to have two authentication strategies applied to the same set of resources. Grant access if the SSO credentials exist, otherwise default to Basic Auth.
At Auto Trader, we have settled on Spring Boot - an opinionated web framework - as our preferred choice for API development. Spring Boot is great to work with, but occasionally when your requirements stray from the path it can take a bit of digging to achieve an elegant implementation. So how do you achieve this with Spring Boot?
Implementation
Cutting right to the chase, here’s how to configure Spring Boot Security to do this:
@Configuration
@EnableWebSecurity
public class SecurityConfiguration {
@Configuration
public static class EndpointSecurity extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.addFilter(createSsoFilter()) // 1
.authorizeRequests()
.anyRequest().authenticated()
.and()
.httpBasic();
}
private RequestHeaderAuthenticationFilter createSsoFilter() throws Exception {
final RequestHeaderAuthenticationFilter ssoFilter = new RequestHeaderAuthenticationFilter();
// We use the presence of x-auth-user for SSO authentication
// Our platform doesn't allow the manual setting of this header from the outside
ssoFilter.setPrincipalRequestHeader("x-auth-user");
ssoFilter.setExceptionIfHeaderMissing(false); // allow basic authentication if no header present
ssoFilter.setAuthenticationManager(authenticationManager());
return ssoFilter;
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
PreAuthenticatedAuthenticationProvider preAuthenticatedAuthenticationProvider = new PreAuthenticatedAuthenticationProvider();
preAuthenticatedAuthenticationProvider.setPreAuthenticatedUserDetailsService(new SingleSignOnUserDetailsService());
auth.authenticationProvider(preAuthenticatedAuthenticationProvider); // 2
auth.userDetailsService(new TypicalUserDetailsService());
}
private class SingleSignOnUserDetailsService implements AuthenticationUserDetailsService<PreAuthenticatedAuthenticationToken> {
public UserDetails loadUserDetails(PreAuthenticatedAuthenticationToken token) throws UsernameNotFoundException {
return Optional.ofNullable(token.getPrincipal())
.map(SingleSignOnUser::fromPrincipal)
.orElse(null);
}
}
private class TypicalUserDetailsService implements UserDetailsService { ... }
}
}
There are two key parts of this code labelled and they require some understanding of Spring Security’s architecture:
- Add a filter to the security filter chain, in our case a
RequestHeaderAuthenticationFilter
. - Add a custom authentication provider, specifically a
PreAuthenticatedAuthenticationProvider
with a customUserDetailsService
.
We are treating our internal SSO as a ‘pre-authentication scenario’ because we want the SSO check to happen first, and if that fails continue to the Basic Auth authentication scenario. This is similar to the SiteMinder SSO example in the Spring Docs.
Please check out this example codebase if you want a fully-fledged runnable demo.
Discussion
Configuring an authentication strategy requires an appropriate authentication provider to be added to a collection of providers (managed by the AuthenticationManager
). It also requires an appropriate filter for Spring to obtain some credentials and then call the appropriate authentication provider to perform authentication on these credentials.
This relationship between providers and filters is somewhat obscured for the common use case of configuring a Basic Auth strategy. Simply appending httpBasic()
to HttpSecurity
will add a BasicAuthenticationFilter
into the security filter chain. Although the httpBasic()
config does a few little extras, in essence, it’s doing this:
http
...
.addFilter(new BasicAuthenticationFilter(this.authenticationManager()))
...
Likewise, setting a UserDetailsService
obscures the addition of a DaoAuthenticationProvider
to the collection of providers.
auth.userDetailsService(new TypicalUserDetailsService());
Instead of explicitly configuring and adding the provider, we indirectly configure the DaoAuthenticationProvider
by telling it where to look for the user accounts using the UserDetailsService
.
Note that the collection of authentication providers are not explicitly coupled to the filter. Any authentication filter, whether it’s BasicAuthenticationFilter
or RequestHeaderAuthenticationFilter
will delegate to the configured AuthenticationManager
to authenticate the credentials it has obtained. The AuthenticationManager
in turn delegates to the collection of authentication providers. Perhaps this makes the architecture more flexible, but it can also have unexpected consequences.
Under the hood, Spring traverses the authentication provider collection and decides whether to perform authentication by checking a supports
method, for example DaoAuthenticationProvider
’s superclass, AbstractUserDetailsAuthenticationProvider
implements:
@Override
public boolean supports(Class<?> authentication) {
return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
}
Whereas the PreAuthenticatedAuthenticationProvider
implements:
@Override
public final boolean supports(Class<?> authentication) {
return PreAuthenticatedAuthenticationToken.class.isAssignableFrom(authentication);
}
The authentication filter is responsible for creating the appropriate token class, stuffing it with the obtained credentials and passing this onto the AuthenticationManager
.
Rather than explicitly saying “for this filter I want to use this provider”, there is an implicit coupling based on the type of token being used, which requires an understanding of the underlying code.
If in our scenario, you decided to create a custom SSO authentication provider that supported UsernamePasswordAuthenticationToken
then the request would have to pass both SSO and Basic Auth to be successfully authenticated, which would not satisfy the requirement.
Instead, treating the SSO as a ‘pre-authentication scenario’ avoids this problem because pre-authentication tokens are distinct from authentication tokens.
If you decide to handwrite custom versions of filters or authentication providers, then you must be aware of this interplay between the framework’s components. It is because of this that I would recommend investing a bit of time upfront to investigate and try to use the purpose-built Spring classes. In our case, RequestHeaderAuthenticationFilter
fit the task perfectly because our internal SSO leverages a header for authentication.
Don’t forget to explore the full working version of the code in this demo app.
Enjoyed that? Read some other posts.