3

I am working on a Angular11 frontend app coupled with a Java Spring boot backend.

Context

I am trying to create an authentication with cookies with JWT. To do so I am inspired by

My endpoint /authenticate returns a jwt access token and set a jwt refresh token in the cookies (httpOnly).
My endpoint /refreshtoken gets the refresh token from the cookies and validates it before generating a new jwt access token and a new jwt refresh token.
Here is the code of my AuthenticationController

    @RestController
    @AllArgsConstructor
    @CrossOrigin(origins = "http://localhost:4200", allowCredentials = "true")
    public class AuthenticationController {
        
            private final AuthenticationManager authenticationManager;
            private final JwtTokenUtil jwtTokenUtil;
            private final UserDetailsServiceImpl userDetailsService;
            private final static String REFRESH_TOKEN_COOKIE = "resfreshtoken";
        
            @PostMapping(value = "/authenticate")
            public ResponseEntity<JwtAuthenticationResponse> createAuthenticationToken(@RequestBody JwtAuthenticationRequest authenticationRequest, HttpServletResponse response) throws Exception {
                try {
                    this.authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(authenticationRequest.getUsername(), authenticationRequest.getPassword()));
                }
                catch (DisabledException e) {
                    throw new Exception("USER_DISABLED", e);
                }
                catch (BadCredentialsException e) {
                    throw new Exception("INVALID_CREDENTIALS", e);
                }
                final UserDetails userDetails = this.userDetailsService.loadUserByUsername(authenticationRequest.getUsername());
                final String token = this.jwtTokenUtil.generateToken(userDetails.getUsername());
                final String refreshToken = this.jwtTokenUtil.generateRefreshToken(new HashMap<>(), userDetails.getUsername());
        
                Cookie cookie = new Cookie(REFRESH_TOKEN_COOKIE, refreshToken);
                cookie.setHttpOnly(true);
                // cookie.setDomain("localhost");
                // cookie.setPath("/");
                response.addCookie(cookie);
        
                return ResponseEntity.ok(new JwtAuthenticationResponse(token));
            }
        
            @GetMapping(value = "/refreshtoken")
            public ResponseEntity<?> refreshToken(@CookieValue(value = REFRESH_TOKEN_COOKIE, required = false) String refreshToken, HttpServletRequest request, HttpServletResponse response) {
                Cookie[] cookies = request.getCookies(); // ALWAYS returns null
                log.debug("refreshToken {}", refreshToken); // ALWAYS null
                try {
                    Claims claims = this.jwtTokenUtil.getAllClaimsFromToken(refreshToken);
                    final String username = claims.get("sub").toString();
                    final String newAccessToken = this.jwtTokenUtil.generateToken(username);
                    final String newRefreshToken = this.jwtTokenUtil.generateRefreshToken(claims, username);
                    CookieUtil.writeCookie(response, REFRESH_TOKEN_COOKIE, newRefreshToken);
        
                    return ResponseEntity.ok(new JwtAuthenticationResponse(newAccessToken));
                }
                catch (SignatureException | MalformedJwtException | UnsupportedJwtException | IllegalArgumentException ex) {
                    throw new BadCredentialsException("INVALID_CREDENTIALS", ex);
                }
                catch (ExpiredJwtException e) {
                    // user should re-login
                }
    
                  return new ResponseEntity<>("Something went wrong", HttpStatus.BAD_REQUEST);
                }
        }

On Angular frontend, here are the options I add to my /refreshtoken Http request (withCredentials=true)

refreshToken(): Observable<AuthenticationResponse> {
        return this.http.get<AuthenticationResponse>(this.apiUrl + 'refreshtoken', { withCredentials: true }).pipe(
            tap(response => LocalStorageUtils.save(LocalStorageKey.JWT_ACCESS_TOKEN_KEY, response.jwtAccessToken))
        );
    }

My issue

If I try to reach my endpoint /authenticate then /refreshtoken from postman, everything works well and I can see the cookie
If I call /authenticate from my frontend, I can see the cookie in the response with the header SET-COOKIE (and in the cookies tab) enter image description here But then if I call /refreshtoken from my frontend, I cannot get the cookies with request.getCookies() nor @CookieValue() on the backend, it always returns null !
It seems like the cookie is well received on the frontend but not set anywere (I cannot see it in application tab -> cookies on Chrome).
Therefore it is not sent with the /refreshtoken request

If anyone can help or has an idea about what is going wrong, I would be grateful ! I can provide more details about my code if it is needed.

TCH
  • 421
  • 1
  • 6
  • 25

2 Answers2

4

You need to add withCredentials: true to your httpOptions and pass it with HttpClient every request. You can also configure it globally with HttpClient and Interceptors.

Keshavram Kuduwa
  • 942
  • 10
  • 40
  • I updated my question with some more details. I tried your suggestion and it is a good clue, but it still does not work in my case. My cookie is still not sent with the /refreshtoken request. – TCH Mar 17 '21 at 11:41
  • Can you add the `.ts` part for authentication as well? – Keshavram Kuduwa Mar 17 '21 at 14:46
  • 1
    You actually put me on the right way. My mistake was to put `withCredentials: true` only for the /refreshtoken request. But actually, I have to put it on /authenticate request as well which allows it to set the cookies in the browser ! thank you ! – TCH Mar 17 '21 at 15:19
2

As @TCH mentioned, for the API which returns the Set-Cookie header we need to set request header with withCredentials: true. I also did a similar mistake and I want to highlight this point :-)