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