Spring Security Cookie + JWT authentication
To get this to work the way described in the original post, this is what needs to happen...
Custom Filter
public class CookieAuthenticationFilter extends AbstractAuthenticationProcessingFilter { public CookieAuthenticationFilter( RequestMatcher requestMatcher ) { super( requestMatcher ); setAuthenticationManager( super.getAuthenticationManager() ); } @Override public Authentication attemptAuthentication( HttpServletRequest request, HttpServletResponse response ) throws AuthenticationException, IOException, ServletException { String token = ""; // get token from a Cookie Cookie[] cookies = request.getCookies(); if( cookies == null || cookies.length < 1 ) { throw new AuthenticationServiceException( "Invalid Token" ); } Cookie sessionCookie = null; for( Cookie cookie : cookies ) { if( ( "someSessionId" ).equals( cookie.getName() ) ) { sessionCookie = cookie; break; } } // TODO: move the cookie validation into a private method if( sessionCookie == null || StringUtils.isEmpty( sessionCookie.getValue() ) ) { throw new AuthenticationServiceException( "Invalid Token" ); } JWTAuthenticationToken jwtAuthentication = new JWTAuthenticationToken( sessionCookie.getValue(), null, null ); return jwtAuthentication; } @Override public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { super.doFilter(req, res, chain); }}
Authentication Provider
attach the provider to the UsernamePasswordAuthenticationToken which is generated by UsernamePasswordAuthenticationFilter, which attaches itself to "/login" POST. For formlogin with POST to "/login" will generate UsernamePasswordAuthenticationToken and your provider will be triggered
@Componentpublic class ApiAuthenticationProvider implements AuthenticationProvider { @Autowired TokenAuthenticationService tokenAuthService; @Override public Authentication authenticate( Authentication authentication ) throws AuthenticationException { String login = authentication.getName(); String password = authentication.getCredentials().toString(); // perform API call to auth against a 3rd party // get User data User user = new User(); // create a JWT token String jwtToken = "some-token-123" return new JWTAuthenticationToken( jwtToken, user, new ArrayList<>() ); } @Override public boolean supports( Class<?> authentication ) { return authentication.equals( UsernamePasswordAuthenticationToken.class ); }}
Custom Authentication object
For the JWT we want to have our own Authentication token object to carry the data that we want along the stack.
public class JWTAuthenticationToken extends AbstractAuthenticationToken { User principal; String token; public JWTAuthenticationToken( String token, User principal, Collection<? extends GrantedAuthority> authorities ) { super( authorities ); this.token = token; this.principal = principal; } @Override public Object getCredentials() { return null; } @Override public Object getPrincipal() { return principal; } public void setToken( String token ) { this.token = token; } public String getToken() { return token; }}
Authentication Success Handler
This is getting called when our custom provider did it's job by authenticating the user against a 3rd party and generating a JWT token, This is the place where the Cookie gets into the Response.
@Componentpublic class AuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler { @Override public void onAuthenticationSuccess( HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { if( !(authentication instanceof JWTAuthenticationToken) ) { return; } JWTAuthenticationToken jwtAuthenticaton = (JWTAuthenticationToken) authentication; // Add a session cookie Cookie sessionCookie = new Cookie( "someSessionId", jwtAuthenticaton.getToken() ); response.addCookie( sessionCookie ); //clearAuthenticationAttributes(request); // call the original impl super.onAuthenticationSuccess( request, response, authentication );}
}
Hooking This All Together
@Configuration@EnableWebSecuritypublic class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired @Required ApiAuthenticationProvider apiAuthProvider; @Autowired @Required AuthenticationSuccessHandler authSuccessHandler; @Autowired @Required SimpleUrlAuthenticationFailureHandler authFailureHandler; @Override protected void configure( AuthenticationManagerBuilder auth ) throws Exception { auth.authenticationProvider( apiAuthProvider ); } @Override protected void configure( HttpSecurity httpSecurity ) throws Exception { httpSecurity // don't create session .sessionManagement() .sessionCreationPolicy( SessionCreationPolicy.STATELESS ) .and() .authorizeRequests() .antMatchers( "/", "/login", "/register" ).permitAll() .antMatchers( "/js/**", "/css/**", "/img/**" ).permitAll() .anyRequest().authenticated() .and() // login .formLogin() .failureHandler( authFailureHandler ) //.failureUrl( "/login" ) .loginPage("/login") .successHandler( authSuccessHandler ) .and() // JWT cookie filter .addFilterAfter( getCookieAuthenticationFilter( new AndRequestMatcher( new AntPathRequestMatcher( "/account" ) ) ) , UsernamePasswordAuthenticationFilter.class ); } @Bean SimpleUrlAuthenticationFailureHandler getAuthFailureHandler() { SimpleUrlAuthenticationFailureHandler handler = new SimpleUrlAuthenticationFailureHandler( "/login" ); handler.setDefaultFailureUrl( "/login" ); //handler.setUseForward( true ); return handler; } CookieAuthenticationFilter getCookieAuthenticationFilter( RequestMatcher requestMatcher ) { CookieAuthenticationFilter filter = new CookieAuthenticationFilter( requestMatcher ); filter.setAuthenticationFailureHandler( authFailureHandler ); return filter; }}
The easiest way would be to add Spring Session into your project and extend HttpSessionStrategy that provides convenient hooks for session created/destroyed events and has method to extract session from HttpServletRequest.