JWT Authentication in Spring Security

From parent article

Motivation

Upon successful signin, the user receives both an access token and a refresh token. The access token allows the user to access protected resources without being prompted to sign in again. For security, both the access and refresh tokens have expiration dates, with the refresh token’s expiration date being significantly longer than that of the access token. When the access token expires, the user can use the refresh token to obtain a new access token.

What is JWT?

JWT is a self-contained and signed token, meaning that it has the all the information to identify the user, and all you need to verify the token is the secret key and the token itself.

A JSON Web Token (JWT) is composed of 3 parts, separated by dots (.):

Eg.

eyJhbGciOiJIUzUxMiJ9.eyJqdGkiOiI2NmI1YTBmOWQ3NmI4OTdmNWZlNjFlYzYiLCJzdWIiOiJ3YWx0ZXIiLCJpYXQiOjE3MjM5Mzk2MTEsInRva2VuX3R5cGUiOiJBQ0NFU1NfVE9LRU4iLCJleHAiOjE3MjM5NTc2NzF9.oAnyniCt1bWUmPiI7Es15PgzmRW0C2hExl3iETai54ybZz_cvC9yXh1Lwwxdw14zxI3iHmjRSN8xAsXM5ajGEQ        

You can decode the token here.

  1. Header: Contains information about the type of token and the algorithm used for signing. Eg.

{
  "alg": "HS512"
}        

2. Payload: Contains the claims or data, such as user information and token expiration time. Eg.

{
  "jti": "66b5a0f9d76b897f5fe61ec6",
  "sub": "walter",
  "iat": 1723939611,
  "token_type": "ACCESS_TOKEN",
  "exp": 1723957671
}        

3. Signature: This is generated by hashing the header and payload together with a secret key. Eg.

HMACSHA512(
    base64UrlEncode(header) + "." + base64UrlEncode(payload),
    your-256-bit-secret
)        

Verification

To verify the token, you need to check:

HashAlgorithm(Header + Payload, SecretKey) == Signature

Why use JWT?

Benefits of JWT over Opaque Tokens like JSESSIONID:

  • Statelessness: JWTs are self-contained, meaning all the information required to verify the token is included within the token itself. Verification only requires the token and the secret key, eliminating the need to store or manage session data on the server. In contrast, opaque tokens like JSESSIONID require the server to store session IDs and session data.
  • Scalability: JWTs allow for better scalability in distributed systems because they are stateless and self-contained. Any server in a distributed network can verify a JWT as long as it has the secret key, even if the server did not issue the token.
  • Decoupled Front and Backend: JWTs facilitate a decoupled architecture where the frontend simply includes the JWT in each request, and the backend validates it without needing to manage sessions. We can integrate JWT integration with any frontend framework, whereas JSESSIONID often ties the frontend closely to the backend, requiring session management on both the client and server sides.

Reference(Opaque) token: Token not contain any verifiable metadata about the user, a recipient must query the authorization server using the token for information about the user. An example is JSESSIONID which is a randomly generated string.

Access Refresh Token Architecture

Access Token: A short-lived token used to authenticate user requests to protected resources or APIs.

Refresh Token: A long-lived token used to obtain a new access token after the current one expires, without requiring the user to log in again.


In this architecture:

  1. The user signs in to the application.
  2. Upon successful authentication, the application issues an access token and a refresh token to the user. The refresh token has a longer expiry period than the access token.
  3. The user uses the access token to make requests to the application for protected resources.
  4. When the access token expires, any requests made by the user to access protected resources with the expired token will fail.
  5. The user makes a renewal request with the refresh token. The application responds with a new access token with a new expiry date.

Why Use Access and Refresh Token Architecture?

Using access and refresh tokens improves the user experience by reducing the friction of repeated login prompts. Users can continue making requests using the access token until it expires. Once the access token expires, the refresh token can be used to automatically obtain a new access token without requiring any manual action from the user.

Why Not Just Use the Access Token?

The two-token approach (access and refresh tokens) improves security by addressing the risks associated with access tokens. Since the access token is sent over the network with each request to protected resources, it is at a higher risk of being stolen by a malicious third party. To minimize potential damage, the access token is given a shorter expiry period, ensuring that even if it is compromised, the stolen token will only be valid for a short time.

In contrast, the refresh token has a longer lifespan because it is only used to renew access tokens and is not exposed to the same level of risk since it is not transmitted with every request.

JWT Authentication Flow

  • The user signs in with their credential.
  • Once signed in our application will create an access token and refresh token stores the tokens in database and return the tokens to the user.
  • The user stores the refresh token in its Cookies, and use the access token to make requests to protected resources.
  • When the access token expires, it will make a request to the refresh endpoint with the refresh token.
  • The refresh endpoint will generate a new access token, stores the new token in database and return back to the user.
  • When the user wants to logout, it calls the signout endpoint with the refresh token which will remove both tokens from database, and remove the refresh token from the user’s Cookies.

Discussion

We’ve established that JWT is a self-contained token that can be validated using the secret key and the token itself. So why do we need to store the tokens in a database and compare the stored token with the token in the request?

The reason is to eliminate stale tokens that become invalid due to actions like logging out or updating the user’s authorities. In these cases, we update the token in the database. If we only validated the token by comparing the hashed header and payload with the signature, stale tokens would still appear valid. However, by checking the token against the stored version in the database, we can ensure that only the most current and valid tokens are accepted.

You might then wonder, why not just store opaque tokens, like JSESSIONID, in a distributed cache and use those for validation? The issue with this approach is that if a malicious party floods our system with requests using fake opaque tokens, our database (or distributed cache) could become overwhelmed with validation queries.

In contrast, using JWTs allows us to verify the token’s validity on the server side without querying the database because JWTs are signed with our secret key. This means we can immediately reject invalid tokens without burdening the database.

We are making a tradeoff here, while we eliminate stale tokens by making extra queries to the database to ensure the tokens are up-to-date, the overall number of queries will likely be lower than if we were using opaque tokens, which would require database queries for every invalid token.

JWT Setup

Add the following dependencies to pom.xml

  <!-- JWT authentication -->
  <dependency>
   <groupId>io.jsonwebtoken</groupId>
   <artifactId>jjwt-api</artifactId>
   <version>0.12.6</version>
  </dependency>
  <dependency>
   <groupId>io.jsonwebtoken</groupId>
   <artifactId>jjwt-impl</artifactId>
   <version>0.12.6</version>
  </dependency>
  <dependency>
   <groupId>io.jsonwebtoken</groupId>
   <artifactId>jjwt-jackson</artifactId>
   <version>0.12.6</version>
  </dependency>        

Add the JwtUtils.java file.

@Component
public class JwtUtils {
    public static final ExpirationTime ACCESS_TOKEN_EXPIRATION_TIME = new ExpirationTime(5, 5, 1, 0);
    public static final ExpirationTime REFRESH_TOKEN_EXPIRATION_TIME = new ExpirationTime(30, 5, 5, 0);
    public static final String TOKEN_TYPE = "token_type";
    private final SecretKey accessSigningKey;
    private final SecretKey refreshSigningKey;

    private final UserService userService;

    @Autowired
    public JwtUtils(String accessSecretKey, String refreshSecretKey, UserService userService) {
        this.accessSigningKey = Keys.hmacShaKeyFor(accessSecretKey.getBytes());
        this.refreshSigningKey = Keys.hmacShaKeyFor(refreshSecretKey.getBytes());
        this.userService = userService;
    }

    /**
     * Generates a JWT token
     *
     * @param authentication UsernameEmailPasswordAuthenticationToken
     * @param tokenType      TokenType
     * @return JWT token
     */
    public String generateToken(UsernameEmailPasswordAuthenticationToken authentication, TokenType tokenType) {
        return generateToken(
                authentication.getId(),
                authentication.getName(),
                tokenType
        );
    }

    /**
     * Generates a JWT token
     *
     * @param id       User ID
     * @param userName User name
     * @param tokenType TokenType
     * @return JWT token
     */
   public String generateToken(String id, String userName, TokenType tokenType) {
        JwtBuilder jwts = Jwts.builder()
                .id(id)
                .subject(userName)
                .issuedAt(new Date(System.currentTimeMillis()));

        /**
         * If the token type is ACCESS_TOKEN, then set the expiration time to ACCESS_TOKEN_EXPIRATION_TIME and sign with the accessSigningKey
         * Otherwise, set the expiration time to REFRESH_TOKEN_EXPIRATION_TIME and sign with the refreshSigningKey
         */
        if (tokenType.equals(ACCESS_TOKEN)) {
            jwts
                    .claims(Map.of(TOKEN_TYPE, ACCESS_TOKEN))
                    .expiration(new Date(System.currentTimeMillis() + ACCESS_TOKEN_EXPIRATION_TIME.toMillis()))
                    .signWith(accessSigningKey);
        } else {
            jwts
                    .claims(Map.of(TOKEN_TYPE, REFRESH_TOKEN))
                    .expiration(new Date(System.currentTimeMillis() + REFRESH_TOKEN_EXPIRATION_TIME.toMillis()))
                    .signWith(refreshSigningKey);
        }
        return jwts.compact();
    }

    /**
     * Validates the JWT token
     *
     * @param token    JWT token
     * @param tokenType TokenType
     * @return UsernameEmailPasswordAuthenticationToken
     */
    public UsernameEmailPasswordAuthenticationToken validateToken(String token, TokenType tokenType) {

        /**
         * If the token type is ACCESS_TOKEN, then verify the token with the accessSigningKey
         * Otherwise, verify the token with the refreshSigningKey because we sign the different token types with different
         * signing keys.
         */
        Claims jwtPayload = Jwts.parser()
                .verifyWith(
                        tokenType.equals(ACCESS_TOKEN) ? accessSigningKey : refreshSigningKey
                )
                .build()
                .parseSignedClaims(token)
                .getPayload();
        /**
         * The `parseSignedClaims` method is where we validate the jwt token. It checks
         * - if encrypt(base64(header) + "." + base64(payload), secretKey) == signature
         * - if the token is expired else throw JwtException
         */

        /**
         * Since we have 2 types of tokens(access, refresh), we want to check if the token type is valid.
         * The refresh token is used for the /auth/refresh and /auth/logout endpoint,
         * and the access token is used for all other endpoints. If validateToken is called through the /auth/refresh or
         * /auth/logout endpoint the parameter TokenType will be REFRESH_TOKEN, and the token type in the payload will be REFRESH_TOKEN
         * otherwise, the parameter TokenType will be ACCESS_TOKEN, and the token type in the payload will be ACCESS_TOKEN.
         */
        if (!jwtPayload.get(TOKEN_TYPE).equals(tokenType.name())) {
            throw new IllegalArgumentException("Invalid JWT token type");
        }

        // Retrieves the user from the database
        User user = userService.getById(jwtPayload.getId()).orElseThrow(
                () ->
                        new IllegalArgumentException("User not found"));

        /**
         * If stored in database does not equal the token from request, then throw an exception.
         * Because
         * - During logout we flush both the access and refresh token from database, or when we generate a new access token
         *  during refresh
         * - The token will still be valid if
         * we use parseSignedClaims to check if the encrypted header + payload == signature, or if the token is expired.
         * - But in terms of our business logic it is invalid, so we need to check if the token from the request is equal
         * to the token stored in the database
         */
        String StoredToken = tokenType.equals(ACCESS_TOKEN) ? user.getAccessToken() : user.getRefreshToken();
        if (StoredToken == null || !StoredToken.equals(token)) {
            throw new JwtException("JWT token does not exist");
        }

        /**
         * return verified Authentication object
         */
        return new UsernameEmailPasswordAuthenticationToken(jwtPayload.getId(), jwtPayload.getSubject(), user.getAuthorities());
    }

}        

Add the following model classes.

ExpirationTime.java

@Getter
public class ExpirationTime {
    // Getters
    private final int days;
    private final int hours;
    private final int minutes;
    private final int seconds;

    public ExpirationTime(int days, int hours, int minutes, int seconds) {
        this.days = days;
        this.hours = hours;
        this.minutes = minutes;
        this.seconds = seconds;
    }

    public long toSeconds() {
        return ((days * 24L + hours) * 60 + minutes) * 60 + seconds;
    }

    public long toMillis() {
        return toSeconds() * 1000;
    }
}        

SigninRequest.java

@Data
public class SigninRequest {
    private String username;
    @Email
    private String email;
    @NotBlank
    private String password;
}        

AccessTokenResponse.java

@Data
public class AccessTokenResponse {
    private String accessToken;
    private long expiresIn;

    public AccessTokenResponse(String accessToken, long expiresIn) {
        this.accessToken = accessToken;
        this.expiresIn = expiresIn;
    }
}        

TokenType.java

public enum TokenType {
    ACCESS_TOKEN("access_token"),
    REFRESH_TOKEN("refresh_token");

    private final String value;

    TokenType(String value) {
        this.value = value;
    }

    public String getValue() {
        return value;
    }
}        

Repository Setup

Add the following fields to User.java if you haven’t

 private String accessToken;
 private String refreshToken;        

Add the following methods to CustomUserRepository.java

    void updateTokens(String id, String accessToken, String refreshToken);
    void updateAccessToken(String id, String accessToken);
    void deleteTokens(String id);        

and to CustomUserRepositoryImpl.java

    @Override
    public void updateTokens(String id, String accessToken, String refreshToken) {
        Query query = new Query(Criteria.where("id").is(id));
        Update update = new Update()
                .set("accessToken", accessToken)
                .set("refreshToken", refreshToken);
        mongoTemplate.updateFirst(query, update, User.class);
    }

    @Override
    public void updateAccessToken(String id, String accessToken) {
        Query query = new Query(Criteria.where("id").is(id));
        Update update = new Update()
                .set("accessToken", accessToken);
        mongoTemplate.updateFirst(query, update, User.class);
    }

    @Override
    public void deleteTokens(String id) {
        Query query = new Query(Criteria.where("id").is(id));
        Update update = new Update()
                .unset("accessToken")
                .unset("refreshToken");
        mongoTemplate.updateFirst(query, update, User.class);
    }        

Add the following method it UserService.java if you haven’t.

    public void updateTokens(String id, String accessToken, String refreshToken) {
        userRepository.updateTokens(id, accessToken, refreshToken);
    }

    public void updateAccessToken(String id, String accessToken) {
        userRepository.updateAccessToken(id, accessToken);
    }

    public void deleteTokens(String id) {
        userRepository.deleteTokens(id);
    }        

Security Filter Chain Setup

Add the file JwtAuthenticationFilter.java


public class JwtAuthenticationFilter extends OncePerRequestFilter {
    private static final String AUTHORIZATION = "Authorization";
    private static final String BEARER = "Bearer";
    private static final String REFRESH_COOKIE = "refresh-token";
    private static final String ACCESS_COOKIE = "access-token";
    private static final Set<String> REFRESH_COOKIE_URL = Set.of("/auth/signout", "/auth/signout");


    private final JwtUtils jwtUtils;
    public JwtAuthenticationFilter(JwtUtils jwtUtils) {
        this.jwtUtils = jwtUtils;
    }

    /**
        * Filters the request and validates the JWT token
        *
        * @param request HTTP request
        * @param response HTTP response
        * @param filterChain Filter chain
        * @throws ServletException
        * @throws IOException
     */
    @Override
    protected void doFilterInternal(@NonNull HttpServletRequest request,
                                    @NonNull HttpServletResponse response,
                                    @NonNull FilterChain filterChain) throws ServletException, IOException {

        try {
            /**
             * If the request url is whitelisted, then no need to validate the JWT token
             */
            if (isWhitelistedOrAuthenticated(request)) {
                filterChain.doFilter(request, response);
                return;
            }

            /**
             * For /refresh and /logout we expect the refresh token,
             *  for all other urls we expect the access token.
             */
            boolean isRefreshCookieUrl = REFRESH_COOKIE_URL.contains(request.getRequestURI());
            TokenType tokenType = isRefreshCookieUrl ? TokenType.REFRESH_TOKEN : TokenType.ACCESS_TOKEN;

            String token = getJwtFromRequest(request, isRefreshCookieUrl);

            UsernameEmailPasswordAuthenticationToken authentication = jwtUtils.validateToken(token, tokenType);

            /**
             * Store the authenticated authentication object in the security context
             * so userId, authorities can be used by downstream components, e.g. controllers, services, etc.
             */
            if (getContext().getAuthentication() == null) {
                getContext().setAuthentication(authentication);
            }

            filterChain.doFilter(request, response);
        } catch (AuthenticationException | JwtException failed) {
            handleUnsuccessfulAuthentication(request, response, failed);
        }
    }

    /**
        * Handles unsuccessful authentication
        *
        * @param request HTTP request
        * @param response HTTP response
        * @param failed Authentication exception
        * @throws IOException
     */
    private void handleUnsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, RuntimeException failed) throws IOException {
        ResponseEntity<String> responseEntity = ResponseEntity
                .status(HttpStatus.UNAUTHORIZED)
                .body("Error: " + failed.getMessage());

        response.setContentType("application/json");
        response.setStatus(responseEntity.getStatusCode().value());
        response.getWriter().write(new ObjectMapper().writeValueAsString(responseEntity.getBody()));
    }

        /**
            * Extracts JWT token from the request
            *
            * @param request HTTP request
            * @return JWT token
         */
    private String getJwtFromRequest(HttpServletRequest request, boolean isRefreshCookieUrl) {
        Cookie[] cookies = request.getCookies();

        /**
         * If the request URL is a refresh token URL, then extract the refresh-token from the cookie
         */
        if (isRefreshCookieUrl) {
            if (cookies != null) {
                return Arrays.stream(cookies)
                        .filter(cookie -> REFRESH_COOKIE.equals(cookie.getName()))
                        .map(Cookie::getValue)
                        .findFirst()
                        .orElseThrow(() -> new JwtException("Refresh token is missing"));
            }
        }

        /**
         * For all other url extract the access token from the Authorization header or the access-token cookie
         */
        String bearerToken = request.getHeader(AUTHORIZATION);
        if (bearerToken != null && bearerToken.startsWith(BEARER + " ")) {
            return bearerToken.substring(7);
        }

        if (cookies != null) {
            return Arrays.stream(cookies)
                    .filter(cookie -> ACCESS_COOKIE.equals(cookie.getName()))
                    .map(Cookie::getValue)
                    .findFirst()
                    .orElseThrow(() -> new JwtException("Access token is missing"));
        }

        throw new JwtException("JWT token is missing");
    }

    /**
        * Checks if the request URL is whitelisted
        *
        * @param request HTTP request
        * @return true if the request URL is whitelisted, false otherwise
     */
    private boolean isWhitelistedOrAuthenticated(HttpServletRequest request) {
        String requestURI = request.getRequestURI();
        Authentication authentication = getContext().getAuthentication();
        if (authentication != null && authentication.isAuthenticated()) {
            return true;
        }

        AntPathMatcher pathMatcher = new AntPathMatcher();
        boolean isWhitelistedURL = Stream.of(SecurityConfig.WHITE_LIST_URL)
                .anyMatch(pattern -> pathMatcher.match(pattern, requestURI));
        return isWhitelistedURL;
    }
}        

The isWhitelistedOrAuthenticated function tells us that the filter won’t be handling requests that is for a whitelist url or request that is authenticated. All other requests requires a JWT token.

Modify to SecurityConfig.java

 public static final String[] WHITE_LIST_URL = {
          "/public/**",
          "/swagger-ui/**",         // Swagger UI
          "/v3/api-docs/**",         // Swagger API docs
          "/auth/signup",            // Signup endpoint
 };
  
  @Bean  
  public JwtUtils jwtUtils(@Value("${JWT_ACCESS_SIGNING_KEY}") String accessSigningKey,
                           @Value("${JWT_REFRESH_SIGNING_KEY}") String refreshSigningKey,
                           UserService userService) {
    return new JwtUtils(accessSigningKey, refreshSigningKey, userService);
  }

  @Bean
  public JwtAuthenticationFilter jwtAuthenticationFilter(JwtUtils jwtUtils) {
    return new JwtAuthenticationFilter(jwtUtils);
  }

  @Bean
  public SecurityFilterChain securityFilterChain(HttpSecurity http,
                                                 JwtAuthenticationFilter jwtAuthenticationFilter,
                                                 UsernameEmailPasswordAuthenticationFilter usernameEmailPasswordAuthenticationFilter) throws Exception {
    http
            .csrf(csrf -> csrf.disable())  // Disable CSRF protection
            .authorizeHttpRequests(auth -> auth
                    .requestMatchers(WHITE_LIST_URL).permitAll()  // Whitelist signup endpoint
                    .anyRequest().authenticated()  // All other endpoints require authentication
            )
            .sessionManagement(session -> session
                    .sessionCreationPolicy(SessionCreationPolicy.STATELESS)  // Stateless session management
            )
            .authenticationProvider(authenticationProvider())  // Register custom authentication provider
            .addFilterAt(usernameEmailPasswordAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
            .addFilterAfter(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
    return http.build();
  }        

When we update our session creation strategy to STATELESS , our application won’t be using JSESSIONID to track sessions.

@Value(“${JWT_ACCESS_SIGNING_KEY}”) and @Value(“${JWT_REFRESH_SIGNING_KEY}”) tells our application to fetch these values from the environmental variable JWT_ACCESS_SIGNING_KEY and JWT_REFRESH_SIGNING_KEY .

.addFilterAfter tells application to add the JWTAuthenticationFilter after the UsernamePasswordAuthenticationFilter.

Updating Authentication Endpoints

Update AuthenticationController.java

    @Autowired
    private JwtUtils jwtUtils;

    /**
     * When the username/email and password are validated during signin, an access token and refresh token are returned
     * to the user. The access token is used to authenticate the user for a short period of time, so it is passed back to
     * user in the response body along with the expiry time.
     * While the refresh token are longer lived and are stored in an HTTP-only cookie on the user's browser.
     */
    @PostMapping("/auth/signin")
    public ResponseEntity<?> authenticateUser(@RequestBody SigninRequest signinRequest, HttpServletResponse response) {
        /** The reason for storing the authentication object in the SecurityContextHolder because SecurityContextHolder is thread local
         * store meaning that our authentication object is global to the thread. You can access the authentication object from
         * anywhere in the application without worrying about leaking the authentication object to other threads. Because while
         * SecurityContextHolder is global to the thread, it provides isolation because the authentication object isolated to
         * the thread. By thread, we mean the request thread that is processing the request.
         * We stored the userId, username, email, and authorities in the authentication object, which means we can
         * access the user's information from anywhere in the application.
         */
        UsernameEmailPasswordAuthenticationToken authentication = (UsernameEmailPasswordAuthenticationToken) SecurityContextHolder.getContext().getAuthentication();

        String accessToken = jwtUtils.generateToken(authentication, TokenType.ACCESS_TOKEN);
        String refreshToken = jwtUtils.generateToken(authentication, TokenType.REFRESH_TOKEN);

        userService.updateTokens(authentication.getId(), accessToken, refreshToken);

        // Set refresh token in HTTP-only cookie
        Cookie refreshTokenCookie = new Cookie("refresh-token", refreshToken);
        refreshTokenCookie.setHttpOnly(true); // prevents JavaScript from accessing the cookie
        refreshTokenCookie.setSecure(true); // should be set to true in production
        refreshTokenCookie.setPath("/");
        refreshTokenCookie.setMaxAge((int) JwtUtils.REFRESH_TOKEN_EXPIRATION_TIME.toSeconds()); // Evicts the cookie from browser when the token expires
        response.addCookie(refreshTokenCookie);

        return ResponseEntity.ok(new AccessTokenResponse(accessToken, JwtUtils.ACCESS_TOKEN_EXPIRATION_TIME.toSeconds()));
    }

    /**
     * Uses the refresh token to authenticate the user and generate a new access token. We use the refresh token for authentication
     * because the user would call this endpoint to get a new access token when the current access token expires.
     * A new access token is generated and returned to the user, and replace the old access token in the database with the new access token.
     */
    @PostMapping("/auth/refresh")
    public ResponseEntity<?> refreshToken(HttpServletResponse response) {
        UsernameEmailPasswordAuthenticationToken authentication = (UsernameEmailPasswordAuthenticationToken) SecurityContextHolder.getContext().getAuthentication();

        String accessToken = jwtUtils.generateToken(authentication, TokenType.ACCESS_TOKEN);
        userService.updateAccessToken(authentication.getId(), accessToken);

        return ResponseEntity.ok(new AccessTokenResponse(accessToken, JwtUtils.ACCESS_TOKEN_EXPIRATION_TIME.toSeconds()));
    }

    /**
     * We are using the refresh token to authenticate the user during logout.
     * When the user is successfully authenticated we remove the user's access and refresh token from the database.
     * We will also remove the refresh token from the user's browser by setting the cookie to expire immediately.
     */
    @PostMapping("/auth/signout")
    public ResponseEntity<?> logoutUser(HttpServletResponse response) {
        // Get the authenticated user's ID from security context
        String userId = ((UsernameEmailPasswordAuthenticationToken) SecurityContextHolder.getContext().getAuthentication()).getId();
        userService.deleteTokens(userId);

        Cookie refreshTokenCookie = new Cookie("refresh-token", null);
        refreshTokenCookie.setHttpOnly(true);
        refreshTokenCookie.setSecure(true);
        refreshTokenCookie.setPath("/");
        refreshTokenCookie.setMaxAge(0); // Remove the refresh token from the user's browser by setting the cookie to expire immediately.
        response.addCookie(refreshTokenCookie);

        return ResponseEntity.ok("Signout successful");
    }
        

The reason we return the refresh token as an HttpOnly, Secure cookie is due to its longer lifespan and higher security requirements. The most secure method for handling refresh tokens is through HttpOnly, Secure cookies, which prevent access to the token via client-side JavaScript, thereby reducing the risk of XSS (Cross-Site Scripting) and MITM (Man-in-the-Middle) attacks.

Meanwhile, the access token, which is shorter-lived, can be returned in the response body.

Setting Up Environment Variables

Generate two secret keys, each at least 64 characters long (as required by the HS512 algorithm). These secret keys will be used separately for the refresh and access tokens. Using two distinct secret keys makes our application more secure.

Then go to https://dashboard.render.com/, click into your Application, then the Environment tab, and add your keys under JWT_ACCESS_SIGNING_KEY and JWT_REFRESH_SIGNING_KEY .


要查看或添加评论,请登录

Yi leng Yao的更多文章