Alexandros Kourtis
Alexandros Kourtis

Reputation: 629

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

Upvotes: 9

Views: 35810

Answers (2)

turong
turong

Reputation: 1622

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.

Upvotes: 2

user731136
user731136

Reputation:

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()
      ..
   }
}

Upvotes: 13

Related Questions