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

969 Views Asked by At

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?

2

There are 2 best solutions below

0
Alice Nilsson On BEST ANSWER

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);
}
3
cardouken On

You could set up a custom authentication filter for the JWT token which would authenticate the user using the JWT token and then create an X-Auth token for the authenticated user.

  1. Custom JWT auth filter:
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import java.io.IOException;

public class JwtAuthenticationFilter extends AbstractAuthenticationProcessingFilter {

    public JwtAuthenticationFilter() {
        super("/authJwt");
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
            throws AuthenticationException, IOException, ServletException {
        String token = request.getHeader("Authorization");

        // JWT authentication logic

        return null; // return the authenticated user
    }

    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
                                            Authentication authResult) throws IOException, ServletException {
        // Generate X-Auth token for the authenticated user
        String xAuthToken = "GENERATED_X_AUTH_TOKEN";
        response.setHeader("X-Auth-Token", xAuthToken);

        // Continue processing the request
        chain.doFilter(request, response);
    }
}
  1. Register the custom filter in your WebSecurityConfiguration class:
@Configuration
@EnableWebSecurity()
public class WebSecurityConfiguration {

    @Bean
    public JwtAuthenticationFilter jwtAuthenticationFilter() throws Exception {
        return new JwtAuthenticationFilter();
    }

    @Bean
    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()
                .and()
                .addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
        return httpSecurity.build();
    }
}

Now, when a user sends a request with a valid JWT token to the /authJwt endpoint, the filter authenticates the user using the JWT token, generates an X-Auth token and returns it in the X-Auth-Token header.