Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I refresh tokens in Spring security

This line:

Jwts.parser().setSigningKey(SECRET_KEY).parseClaimsJws(token).getBody();

Throws an error like this when my jwt token expires:

JWT expired at 2020-05-13T07:50:39Z. Current time: 2020-05-16T21:29:41Z.

More specifically, it is this function that throws the "ExpiredJwtException" exception : parseClaimsJws() function

How do I go about handling these exceptions? Should I catch them and send back to the client an error message and force them to re-login?

How can I implement a refresh tokens feature? I'm using Spring and mysql in the backend and vuejs in the front end.

I generate the initial token like this:

   @Override
        public JSONObject login(AuthenticationRequest authreq) {
            JSONObject json = new JSONObject();
    
            try {
                Authentication authentication = authenticationManager.authenticate(
                        new UsernamePasswordAuthenticationToken(authreq.getUsername(), authreq.getPassword()));
    
                UserDetailsImpl userDetails = (UserDetailsImpl) authentication.getPrincipal();
                List<String> roles = userDetails.getAuthorities().stream().map(item -> item.getAuthority())
                        .collect(Collectors.toList());
    
                if (userDetails != null) {
    
                    final String jwt = jwtTokenUtil.generateToken(userDetails);
    
    
                    JwtResponse jwtres = new JwtResponse(jwt, userDetails.getId(), userDetails.getUsername(),
                            userDetails.getEmail(), roles, jwtTokenUtil.extractExpiration(jwt).toString());
    
                    return json.put("jwtresponse", jwtres);
                }
            } catch (BadCredentialsException ex) {
                json.put("status", "badcredentials");
            } catch (LockedException ex) {
                json.put("status", "LockedException");
            } catch (DisabledException ex) {
                json.put("status", "DisabledException");
            }
    
            return json;
        }

And then in the JwtUtil class:

   public String generateToken(UserDetails userDetails) {
            Map<String, Object> claims = new HashMap<>();
            return createToken(claims, userDetails.getUsername());
        }
    
   private String createToken(Map<String, Object> claims, String subject) {
            return Jwts.builder().setClaims(claims).setSubject(subject).setIssuedAt(new Date(System.currentTimeMillis()))
                    .setExpiration(new Date(System.currentTimeMillis() + EXPIRESIN))
                    .signWith(SignatureAlgorithm.HS256, SECRET_KEY).compact();
        }

For more info, here is my doFilterInternal function that filters every request:

   @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws ServletException, IOException, ExpiredJwtException, MalformedJwtException {

        try {

            final String authorizationHeader = request.getHeader("Authorization");

            String username = null;
            String jwt = null;

            if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
                jwt = authorizationHeader.substring(7);
                username = jwtUtil.extractUsername(jwt);
            }

            if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
                UserDetails userDetails = userService.loadUserByUsername(username);

                boolean correct = jwtUtil.validateToken(jwt, userDetails);

                if (correct) {
                    UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(
                            userDetails, null, userDetails.getAuthorities());

                    usernamePasswordAuthenticationToken
                            .setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                    SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);

                }
            }

            chain.doFilter(request, response);
        } catch (ExpiredJwtException ex) {
            resolver.resolveException(request, response, null, ex);
        }
    } 
like image 630
Alexandros Kourtis Avatar asked Aug 15 '20 13:08

Alexandros Kourtis


People also ask

How does Spring Security implement refresh token?

Flow for Spring Boot Refresh Token with JWT The diagram shows flow of how we implement Authentication process with Access Token and Refresh Token. – A legal JWT must be added to HTTP Authorization Header if Client accesses protected resources. – A refreshToken will be provided at the time user signs in.

How do I refresh refresh tokens?

To refresh your access token as well as an ID token, you send a token request with a grant_type of refresh_token . Be sure to include the openid scope when you want to refresh the ID token. If the refresh token is valid, then you get back a new access and the refresh token.

How do I refresh JWT tokens?

In the URL field enter the address to the refresh token route of your local API - http://localhost:4000/users/refresh-token . Click the Send button, you should receive a "200 OK" response containing the user details and a JWT token, and a cookie containing a new refresh token.

Can I use refresh token multiple times?

So the answer is, Yes you can (and probably should) wait until your access token expires, and then refresh it.


2 Answers

There are 2 main approaches to deal with such situations:


Manage access and refresh tokens

In this case, the flow is the following one:

  1. User logins into the application (including username and password)

  2. Your backend application returns any required credentials information and:

    2.1 Access JWT token with an expired time usually "low" (15, 30 minutes, etc).

    2.2 Refresh JWT token with an expired time greater than access one.

  3. From now, your frontend application will use access token in the Authorization header for every request.

When backend returns 401, the frontend application will try to use refresh token (using an specific endpoint) to get new credentials, without forcing the user to login again.

Refresh token flow (This is only an example, usually only the refresh token is sent)

If there is no problem, then the user will be able to continue using the application. If backend returns a new 401 => frontend should redirect to login page.


Manage only one Jwt token

In this case, the flow is similar to the previous one and you can create your own endpoint to deal with such situations: /auth/token/extend (for example), including the expired Jwt as parameter of the request.

Now it's up to you manage:

  • How much time an expired Jwt token will be "valid" to extend it?

The new endpoint will have a similar behaviour of refresh one in the previous section, I mean, will return a new Jwt token or 401 so, from the point of view of frontend the flow will be the same.


One important thing, independently of the approach you want to follow, the "new endpoint" should be excluded from the required Spring authenticated endpoints, because you will manage the security by yourself:

public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
  ..

  @Override
  protected void configure(HttpSecurity http) throws Exception {
    http.
      ..
      .authorizeRequests()
      // List of services do not require authentication
      .antMatchers(Rest Operator, "MyEndpointToRefreshOrExtendToken").permitAll()
      // Any other request must be authenticated
      .anyRequest().authenticated()
      ..
   }
}
like image 175
doctore Avatar answered Oct 19 '22 08:10

doctore


You can call the API for getting the refresh token as below

POST https://yourdomain.com/oauth/token 

Header
  "Authorization": "Basic [base64encode(clientId:clientSecret)]" 

Parameters
  "grant_type": "refresh_token"
  "refresh_token": "[yourRefreshToken]"

Please be noticed that, the

  • base64encode is the method to encrypt the client authorization. You can use online at https://www.base64encode.org/
  • the refresh_token is the String value of the grant_type
  • yourRefreshToken is the refresh token received with JWT access token

The result can be seen as

{
    "token_type":"bearer",
    "access_token":"eyJ0eXAiOiJK.iLCJpYXQiO.Dww7TC9xu_2s",
    "expires_in":20,
    "refresh_token":"7fd15938c823cf58e78019bea2af142f9449696a"
}

Good luck.

like image 23
turong Avatar answered Oct 19 '22 09:10

turong