0

Hello I am trying to migrate from Spring Security Session authentication and Authorization to auth via JWT. I have a question regarding a specific situation I encountered. Instead of using the Authorization header for authentication, I am interested in using cookies to avoid storing the token in local storage. However, my integration test keeps failing due to the absence of a 'Bearer token.' I would like to know if anyone else has faced a similar scenario where they needed to send the JWT token as cookies instead of using Authorization headers. If so, how did you address the error message look below? Any insights or solutions would be greatly appreciated. Thank you.

Error main] .s.r.w.a.BearerTokenAuthenticationFilter : Did not process request since did not find bearer token

Integration Test

@Test
    @Order(3)
    void login() throws Exception {
        MvcResult login = this.MOCK_MVC
                .perform(post("******")
                        .with(csrf())
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(new LoginDTO(ADMIN_EMAIL, ADMIN_PASSWORD).convertToJSON().toString())
                )
                .andExpect(status().isOk())
                .andReturn();

        Cookie cookie = login.getResponse().getCookie(COOKIE_NAME);

        // Test route
        this.MOCK_MVC
                .perform(get("****").cookie(cookie))
                .andExpect(status().isOk());
    }

Login Method

/**
     * Note Transactional annotation is used because Entity class has properties with fetch type LAZY
     * @param dto consist of principal(username or email) and password.
     * @param req of type HttpServletRequest
     * @param res of type HttpServletResponse
     * @throws AuthenticationException is thrown when credentials do not exist or bad credentials
     * @return ResponseEntity of type HttpStatus
     * */
    @Transactional
    public ResponseEntity<?> login(LoginDTO dto, HttpServletRequest req, HttpServletResponse res) {
        Authentication authentication = this.authManager.authenticate(
                UsernamePasswordAuthenticationToken.unauthenticated(dto.getPrincipal(), dto.getPassword())
        );

        // Jwt Token
        String token = this.jwtTokenService.generateToken(authentication);

        // Add Jwt Cookie to Header
        Cookie jwtCookie = new Cookie(COOKIENAME, token);
        jwtCookie.setDomain(DOMAIN);
        jwtCookie.setPath(COOKIE_PATH);
        jwtCookie.setSecure(COOKIE_SECURE);
        jwtCookie.setHttpOnly(HTTPONLY);
        jwtCookie.setMaxAge(COOKIEMAXAGE);

        // Add custom cookie to response
        res.addCookie(jwtCookie);

        // Second cookie where UI can access to validate if user is logged in
        Cookie cookie = new Cookie(LOGGEDSESSION, UUID.randomUUID().toString());
        cookie.setDomain(DOMAIN);
        cookie.setPath(COOKIE_PATH);
        cookie.setSecure(COOKIE_SECURE);
        cookie.setHttpOnly(false);
        cookie.setMaxAge(COOKIEMAXAGE);

        // Add custom cookie to response
        res.addCookie(cookie);

        return new ResponseEntity<>(OK);
    }

FilterChain

@Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http
                .csrf(csrf -> csrf.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()))
                .cors(Customizer.withDefaults())
                .authorizeHttpRequests(auth -> {
                    auth.requestMatchers(publicRoutes()).permitAll();
                    auth.anyRequest().authenticated();
                })
                .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                .oauth2ResourceServer((oauth2) -> oauth2.jwt(Customizer.withDefaults()))
                .exceptionHandling((ex) -> ex.authenticationEntryPoint(this.authEntryPoint))
//                .addFilterBefore(new JwtFilter(), BearerTokenAuthenticationFilter.class)
                .logout(out -> out
                        .logoutUrl("****")
                        .deleteCookies(COOKIE_NAME, LOGGEDSESSION)
                        .logoutSuccessHandler((request, response, authentication) ->
                                SecurityContextHolder.clearContext()
                        )
                )
                .build();
    }

Finally, I want to draw your attention to the SecurityFilterChain mentioned earlier, where you'll notice that I have commented out the addFilterBefore method. Initially, my approach was to handle each incoming request by extracting the desired cookie containing the JWT token and adding it to the request headers. This approach works well when the cookie exists but not when the cookie is null, for instance, during the user sign-in process. Note HeaderMapRequestWrapper implementation is similar to link

@Component @Slf4j
    public class JwtFilter extends OncePerRequestFilter {

    @Value(value = "${server.servlet.session.cookie.name}")
    private String COOKIENAME;

    @Override
    protected void doFilterInternal(
            @NotNull HttpServletRequest request,
            @NotNull HttpServletResponse response,
            @NotNull FilterChain filterChain
    ) throws ServletException, IOException {
        Cookie[] cookies = request.getCookies();
        log.info("Cookies Array " + Arrays.toString(cookies)); // Null on login requests

        HeaderMapRequestWrapper requestWrapper = new HeaderMapRequestWrapper(request);

        if (cookies != null) {
            Optional<String> cookie = Arrays.stream(cookies)
                    .map(Cookie::getName)
                    .filter(name -> name.equals(COOKIENAME))
                    .findFirst();

            cookie.ifPresent(s -> requestWrapper.addHeader(HttpHeaders.AUTHORIZATION, "Bearer %s".formatted(s)));
        }

        filterChain.doFilter(requestWrapper, response);
    }

}
seju
  • 119
  • 1
  • 8

1 Answers1

1

I was able to solved this issue by looking at spring docs. Since by default, Resource Server looks for a bearer token in the Authorization header and in my case I am sending as a cookie, I had to define a custom implementation of BearerTokenResolver.

Bean

@Bean
    public BearerTokenResolver bearerTokenResolver(JwtDecoder jwtDecoder) {
        return new CustomBearerTokenResolver(jwtDecoder);
    }

Custom impl

@Slf4j
public class CustomBearerTokenResolver implements BearerTokenResolver {

    @Value(value = "${server.servlet.session.cookie.name}")
    private String COOKIENAME;
    private final JwtDecoder jwtDecoder;

    public CustomBearerTokenResolver(JwtDecoder jwtDecoder) {
        this.jwtDecoder = jwtDecoder;
    }

    @Override
    public String resolve(final HttpServletRequest request) {
        Cookie[] cookies = request.getCookies();

        if (cookies != null) {
            Optional<Cookie> cookie = Arrays.stream(cookies)
                    .filter(name -> name.getName().equals(COOKIENAME))
                    .findFirst();

            if (cookie.isPresent()) {
                try { // Note this is an expensive compute
                    String token = cookie.get().getValue();
                    this.jwtDecoder.decode(token);
                    return token;
                } catch (JwtException e) {
                    log.error("Jwt Exception {}", e.getMessage());
                    return null;
                }
            }
        }

        return null;
    }

} 
seju
  • 119
  • 1
  • 8