Sample project with basic 2-factor authentication using Spring Security. The source code is in github.
The first factor is a standard login form (username and password), but any Spring Security login will work the same way. The second factor in this sample is a toy one (the user's favourite colour), but it is easy to change that bit into something more realistic. The key parts of the implementation are as follows.
The application resources are protected with an additional role that would not be present in a normal authentication:
@Override
protected void configure(HttpSecurity http) throws Exception {
http
...
.authorizeRequests().antMatchers("/factor/**").authenticated()
.anyRequest().hasRole("FACTOR");
}
So there is a special role that protects all resources except "/factor/**", which is where we are going to handle the second factor.
An AccessDeniedHandler
is used that checks for the special role, and
redirects to a prompt for the additional information:
@Override
protected void configure(HttpSecurity http) throws Exception {
http.requestCache().requestCache(requestCache)
.and().exceptionHandling().accessDeniedHandler(accessDeniedHandler())
...;
}
A @Controller
is provided that prompts the user for the extra factor
and handles the response. The implementation here is super simple: it
just presents the user with a form to type in their favourite
colour:
@GetMapping("/factor")
public String factor() {
return "factor";
}
If the favourite colour is "red" then they are authenticated, and the
response is handled by a
SavedRequestAwareAuthenticationSuccessHandler
(just like a normal
login success):
@PostMapping("/factor")
public void accept(@RequestParam String factor, Principal principal,
HttpServletRequest request, HttpServletResponse response) throws Exception {
if (!"red".equals(factor)) {
response.sendRedirect("/factor?error=true");
return;
}
Authentication successful = addFactorRole(principal);
SecurityContextHolder.getContext().setAuthentication(successful);
handler.onAuthenticationSuccess(request, response, successful);
}
For a real implementation you could do a token-based authentication (for instance) instead.
We want to remember the user's original request across the whole authentication, and the default strategy provided by Spring Security discards the saved request after the first stage. So to extend its memory, we add a request cache and set it up so that only an authentication with the special role causes the saved request to be discarded:
@Bean
public RequestCache savedRequestCache() {
return new HttpSessionRequestCache() {
@Override
public void removeRequest(HttpServletRequest currentRequest,
HttpServletResponse response) {
Authentication authentication = SecurityContextHolder.getContext()
.getAuthentication();
if (authentication != null && authentication.getAuthorities()
.contains(new SimpleGrantedAuthority("ROLE_FACTOR"))) {
super.removeRequest(currentRequest, response);
}
}
};
}