Search code examples
spring-bootspring-securityspring-oauth2

Spring Boot Security - Use token from Cookies instead of Authorization header


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);
    }

}

Solution

  • 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 jwt is a cookie, I had to define a custom implementation of BearerTokenResolver.

    @Bean
        public BearerTokenResolver bearerTokenResolver(JwtDecoder decoder, JwtTokenService service) {
            return new BearerResolver(JSESSIONID, decoder, service);
        }
    
        private record BearerResolver(
                String JSESSIONID,
                JwtDecoder decoder,
                JwtTokenService service
        ) implements BearerTokenResolver {
            @Override
            public String resolve(HttpServletRequest request) {
                Cookie[] cookies = request.getCookies();
                // ternary operator
                return cookies == null ? null : Arrays
                        .stream(cookies)
                        .filter(cookie -> cookie.getName().equals(JSESSIONID))
                        .filter(this.service::_isTokenNoneExpired)
                        .map(Cookie::getValue)
                        .findFirst()
                        .orElse(null);
            }
        }