springspring-bootspring-securityhttpsession

Using JWT authorization in combination with X-Auth-Tokens in Spring Boot


I'm trying to setup Spring Boot 3 to use both authentication using JWT and HTTP sessions with X-Auth tokens. The goal is to use X-Auth tokens as primary authentication method, but users might authenticate using an external provider which grants a JWT access token.

I've successfully managed to create two different authorization endpoints, one at /auth using form based login and returns an X-Auth token, and one at /authJwt. JWT authorization is only enabled at /authJwt and all other endpoints are protected using X-Auth tokens.

Is it possible to enable generation of X-Auth tokens by authentication using a JWT? I've configured HTTP sessions to always be created, and a call to /authJwt returns an X-Auth token in the HTTP header. But the X-Auth token is not valid when trying to authenticate.

This is the security configuration which I'm using (I've removed some irrelevant parts):

@Configuration
@EnableWebSecurity()
public class WebSecurityConfiguration {

    // Endpoints which will be public and not require authentication
    private static final String[] AUTH_WHITELIST = {
        "/auth"
    };

    /**
     * Filter chain for authenticating using JWT tokens
     */
    @Bean
    @Order(1)
    public SecurityFilterChain oAuth2ResourceFilterChain(HttpSecurity httpSecurity) throws Exception {
        httpSecurity
                .securityMatcher("/authJwt")
                .cors().and().csrf().disable()
                .requestCache().disable().exceptionHandling().and()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.ALWAYS)
                .and()
                .authorizeHttpRequests().anyRequest().authenticated()
                .and()
                .oauth2ResourceServer()
                .jwt();
        return httpSecurity.build();
    }

    /**
     * Filter chain for enabling authentication.
     */
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
        httpSecurity
                .cors().and().csrf().disable()
                .requestCache().disable().exceptionHandling().and()
                .formLogin().loginPage("/auth").usernameParameter("loginName").passwordParameter("loginPassword")
                .successHandler((request, response, authentication) -> response.setStatus(HttpServletResponse.SC_OK))
                .and()
                .authorizeHttpRequests(requests -> requests
                    .requestMatchers(AUTH_WHITELIST).permitAll()
                    .anyRequest().authenticated()
                )
                // Return 401 on no session
                .exceptionHandling().authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED))
                .and()
                .logout();
        return httpSecurity.build();
    }
}

This is the configuration for the sessions:

@Configuration()
@EnableSpringHttpSession
public class SpringHttpSessionConfig {

    @Bean
    public MapSessionRepository sessionRepository() {
        return new MapSessionRepository(new ConcurrentHashMap<>());
    }

    @Bean
    public HttpSessionIdResolver httpSessionIdResolver() {
        return HeaderHttpSessionIdResolver.xAuthToken();
    }
}

Can anyone point in the correct direction of exchanging JWT tokens for X-Auth tokens?


Solution

  • I realized I had assumed I needed to use Spring Session, but it was easier to solve it without sessions. Instead I added a custom token store and token filters which manages authorization. The token store:

    @Component
    public class TokenStore {
    
        private final ConcurrentHashMap<String, Authentication> authenticationCache 
           = new ConcurrentHashMap<>();
    
        /**
         * Generates and registers authentication token.
         *
         * @param authentication The authentication used.
         * @return Generated authentication token.
         */
        public String generateToken(Authentication authentication) {
            String token = UUID.randomUUID().toString();
            authenticationCache.put(token, authentication);
            return token;
        }
    
        /**
         * Returns authentication from authentication token.
         *
         * @param token The authentication token to check.
         * @return Authentication if token exists, otherwise null.
         */
        public Authentication getAuth(String token) {
            return authenticationCache.getOrDefault(token, null);
        }
    
        /**
         * Removes authentication token.
         *
         * @param token Authentication token.
         */
        public void removeAuth(String token) {
            authenticationCache.remove(token);
        }
    }
    

    Token filter which is added to the SecurityFilterChain to validate access tokens:

    @Component
    public class TokenFilter extends OncePerRequestFilter {
    
        private final TokenStore tokenStore;
    
        public TokenFilter(TokenStore tokenStore) {
            this.tokenStore = tokenStore;
        }
    
        /**
         * Checks if an authentication token is valid and sets authentication.
         */
        @Override
        protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
            String authToken = request.getHeader(AuthConstants.AUTH_TOKEN_NAME);
            if (authToken != null) {
                Authentication authentication = tokenStore.getAuth(authToken);
                if (authentication != null) {
                    SecurityContext securityContext = SecurityContextHolder.createEmptyContext();
                    securityContext.setAuthentication(authentication);
                    SecurityContextHolder.setContext(securityContext);
                }
            }
            filterChain.doFilter(request, response);
        }
    }
    

    The tokens are genereated either on the success handler of the form login:

    private void successHandler(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {
       String token = tokenStore.generateToken(authentication);
       response.addHeader(AuthConstants.AUTH_TOKEN_NAME, token);
    }
    

    Or in the endpoint for the /authJwt endpoint:

    /**
     * Exchanges a valid JWT to an access token.
     *
     * This endpoint is protected using the OAuth2 filter chain.
     */
    @PostMapping(value = "/authJwt")
    public void verifyClient(HttpServletResponse response, Authentication authentication) {
       String token = tokenStore.generateToken(authentication);
       response.addHeader(AuthConstants.AUTH_TOKEN_NAME, token);
    }