You're right, it isn't easy and there aren't many good examples out there. Examples i saw made it so you couldn't use other spring security stuff side by side. I did something similar recently, here's what i did.
You need a custom token to hold your header value
public class CustomToken extends AbstractAuthenticationToken {
private final String value;
//Getters and Constructor. Make sure getAutheticated returns false at first.
//I made mine "immutable" via:
@Override
public void setAuthenticated(boolean isAuthenticated) {
//It doesn't make sense to let just anyone set this token to authenticated, so we block it
//Similar precautions are taken in other spring framework tokens, EG: UsernamePasswordAuthenticationToken
if (isAuthenticated) {
throw new IllegalArgumentException(MESSAGE_CANNOT_SET_AUTHENTICATED);
}
super.setAuthenticated(false);
}
}
You need a spring security filter to extract the header and ask the manager to authenticate it, something like thisemphasized text
public class CustomFilter extends AbstractAuthenticationProcessingFilter {
public CustomFilter(RequestMatcher requestMatcher) {
super(requestMatcher);
this.setAuthenticationSuccessHandler((request, response, authentication) -> {
/*
* On success the desired action is to chain through the remaining filters.
* Chaining is not possible through the success handlers, because the chain is not accessible in this method.
* As such, this success handler implementation does nothing, and chaining is accomplished by overriding the successfulAuthentication method as per:
* http://docs.spring.io/autorepo/docs/spring-security/3.2.4.RELEASE/apidocs/org/springframework/security/web/authentication/AbstractAuthenticationProcessingFilter.html#successfulAuthentication(javax.servlet.http.HttpServletRequest,%20javax.servlet.http.HttpServletResponse,%20javax.servlet.FilterChain,%20org.springframework.security.core.Authentication)
* "Subclasses can override this method to continue the FilterChain after successful authentication."
*/
});
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException, IOException, ServletException {
String tokenValue = request.getHeader("SOMEHEADER");
if(StringUtils.isEmpty(tokenValue)) {
//Doing this check is kinda dumb because we check for it up above in doFilter
//..but this is a public method and we can't do much if we don't have the header
//also we can't do the check only here because we don't have the chain available
return null;
}
CustomToken token = new CustomToken(tokenValue);
token.setDetails(authenticationDetailsSource.buildDetails(request));
return this.getAuthenticationManager().authenticate(token);
}
/*
* Overriding this method to maintain the chaining on authentication success.
* http://docs.spring.io/autorepo/docs/spring-security/3.2.4.RELEASE/apidocs/org/springframework/security/web/authentication/AbstractAuthenticationProcessingFilter.html#successfulAuthentication(javax.servlet.http.HttpServletRequest,%20javax.servlet.http.HttpServletResponse,%20javax.servlet.FilterChain,%20org.springframework.security.core.Authentication)
* "Subclasses can override this method to continue the FilterChain after successful authentication."
*/
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
//if this isn't called, then no auth is set in the security context holder
//and subsequent security filters can still execute.
//so in SOME cases you might want to conditionally call this
super.successfulAuthentication(request, response, chain, authResult);
//Continue the chain
chain.doFilter(request, response);
}
}
Register your custom filter in spring security chain
@Configuration
public static class ResourceEndpointsSecurityConfig extends WebSecurityConfigurerAdapter {
//Note, we don't register this as a bean as we don't want it to be added to the main Filter chain, just the spring security filter chain
protected AbstractAuthenticationProcessingFilter createCustomFilter() throws Exception {
CustomFilter filter = new CustomFilter( new RegexRequestMatcher("^/.*", null));
filter.setAuthenticationManager(this.authenticationManagerBean());
return filter;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
//fyi: This adds it to the spring security proxy filter chain
.addFilterBefore(createCustomFilter(), AnonymousAuthenticationFilter.class)
}
}
A custom auth provider to validate that token extracted with the filter.
public class CustomAuthenticationProvider implements AuthenticationProvider {
@Override
public Authentication authenticate(Authentication auth)
throws AuthenticationException {
CustomToken token = (CustomToken)auth;
try{
//Authenticate token against redis or whatever you want
//This i found weird, you need a Principal in your Token...I use User
//I found this to be very redundant in spring security, but Controller param resolving will break if you don't do this...anoying
org.springframework.security.core.userdetails.User principal = new User(...);
//Our token resolved to a username so i went with this token...you could make your CustomToken take the principal. getCredentials returns "NO_PASSWORD"..it gets cleared out anyways. also the getAuthenticated for the thing you return should return true now
return new UsernamePasswordAuthenticationToken(principal, auth.getCredentials(), principal.getAuthorities());
} catch(Expection e){
//TODO throw appropriate AuthenticationException types
throw new BadCredentialsException(MESSAGE_AUTHENTICATION_FAILURE, e);
}
}
@Override
public boolean supports(Class<?> authentication) {
return CustomToken.class.isAssignableFrom(authentication);
}
}
Finally, register your provider as a bean so the authentication manager finds it in some @Configuration class. You probably could just @Component it too, i prefer this method
@Bean
public AuthenticationProvider createCustomAuthenticationProvider(injectedDependencies) {
return new CustomAuthenticationProvider(injectedDependencies);
}