How can I refresh tokens in Spring security
Asked Answered
K

2

9

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);
        }
    } 
Kapoor answered 15/8, 2020 at 13:2 Comment(4)
This might help : please read this : JWTs_Refresh_tokensFabiano
Do I need a middle ware in my front end app to implement something like this?Kapoor
There are several ways to do it. How are you generating "the initial" JWT one?Crt
I generate the token when the user logs in. I edited the answer so you can see the code where I generate the token.Kapoor
C
12

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()
      ..
   }
}
Crt answered 16/8, 2020 at 10:38 Comment(16)
What do you mean that I will manage the security by myself? When you say excluded from required Spring authenticated endpoints, do you mean making it a route that is not protected? If not, could you please elaborate? Thank you for your answer.Kapoor
By the way, I had decided to do in another way, but haven't yet fully implemented it. I made it so the jwt token lasts for about 7 days. But, every 5 days, assuming the users logs in to my site, the front end will request a new token be generated. If the user has not logged in for 7 days, they will have to re log in by entering the credentials. Is that a good way of handling jwt tokens? I feel like it is not a very "standard" way...Kapoor
I marked your answer as the one that solved it but I will have to look into it a little bit more and I might make an answer to my question myself later on.Kapoor
What do you mean that I will manage the security by myself? in the same way you manage the security in your login endpoint, taking into account the provided AuthenticationRequest object, you have to do the same with the new endpoint verifying the provided Jwt token: is a valid one? is the expiration time "too old"?Crt
When you say excluded from required Spring authenticated endpoints, do you mean making it a route that is not protected? yes, in the same way, you have done it with your login endpoint.Crt
...AuthenticationRequest object, you have to do the same with the new endpoint verifying the provided Jwt token: is a valid one? is the expiration time "too old"? I did that but every time but I could not set a new date for the token because it would throw an exception. I also tried to make a new one after the previous token was expired but still it always threw an exception. Because of the exception I can't "interact" with the token. I can't extract the claims and make a new one... I think the only thing I can do is extend the expiration date but BEFORE it has expired.Kapoor
I guess the whole point of an expired jwt token is to not work anymore. Therefore, you either make a new, which requires the credentials of the user (at least their username). Or, you handle the token before it expires. Is this correct?Kapoor
...I made it so the jwt token lasts for about 7 days... the front end will request a new token be generated... I feel like it is not a very "standard" way... , no because you are "hardcoding" in frontend the interval on which a new token should be requested. The approach I propose you is more suitable because it will work without take into account the time of your users are logged. You can increase the expiration time of your Jwt token if that is suitable in your case.Crt
I did that but every time but I could not set a new date for the token because it would throw an exception, not sure how you have configured the Spring security in your project but, if you include the "new proposed endpoint" in the allowed request, you will be able to manage is the provided Jwt token "is allowed to extend or not". In this case, depending of the Jwt library you are using probably some different use cases have to be taking into account: what is the exception I have to manage when the token is valid but it has expired, for example.Crt
So to recap, when the user's token expires, the front will make a request for a new token at (i.e.) "/auth/token/refresh/", where I will pass the user's username (I think I need to pass the username to build the jwt) and the previous expired token? (what do I need the expired token for?); to check if it truly is invalid? That's what I did before and it would throw and expiredjwtexception... Then, I return the token and the front end stores it local storage. Is there a batter way to store it? Did I make any mistakes? Are there any security drawbacks to this way of handling tokens?Kapoor
Or, you handle the token before it expires. Is this correct? => with this approach you are developing a "ping endpoint", it means frontend will send every "X days" a really not required request to "refresh" the Jwt token. If you prefer that approach, ok, is a developer decision. Personally I prefer to request only the "required ones" when it necessary.Crt
I prefer your method too.Kapoor
So to recap, when the user's token expires, the front will make a request ..., where I will pass the user's username, include the username is up to you, I mean, if the Jwt contains that information, you can extract from it (that is a safer way to do it). The Jwt library you are using should be able distinguish between: 1. The provided token was not generated by my application (or has been modified) => returns 401 directly. 2. The token is valid (generated by me), but it has expired => if it is not too old (check it or not is up to you), generate a new one and return itCrt
Sorry for the spam... Is it a good idea to catch the ExpiredJwtException and make a new one in the request filter... or is that stupid in terms of security? } catch (ExpiredJwtException ex) { UserDetails userDetails = userService.loadUserByUsername(this.username); String newjwt = jwtUtil.generateToken(userDetails); resolver.resolveException(request, response, null, ex); }Kapoor
I would include such method in your JwtUtil class, I mean for example, isTokenValid(token, tooOldLimit) returning an enum value: VALID (generated by my application and not expired), NOT_VALID (not generated by my application), EXPIRED_BUT_NOT_TOO_OLD (generated by my application, expired but not too old), EXPIRED_TOO_OLD (generated by my application, expired and too old). In that way, you will be able to manage "know use cases" in your service layer without work with "internal exceptions"Crt
I don't think I am able to check to see if the token is expired. Look at this: github.com/jwtk/jjwt/issues/440 and this: tools.ietf.org/html/rfc7519#section-4.1.4 ... If it is expired it throws an exception. Therefore, I can only return to the user the 401 and they will have to log in again. I am not sure how to implement the other solution with my current spring boo implementation and code base. I will keep you updated when I finally solve this problem.Kapoor
I
2

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.

Intermediate answered 16/8, 2020 at 11:18 Comment(7)
Thank you for your answer. I will let you know If I successfully implement it.Kapoor
That only works if you have included in your project an Oauth integration: spring-security-oauth2, spring-cloud-starter-oauth2 etc that is not your case because, taking into account the information and code provided, if you have included any of them, you are not really using it.Crt
yes absolutely @AlexandrosKourtis. In the case we do not use Spring Security OAuth2, we can build our own endpoint for refresh. Such as /refresh/token and do the same with the login by verifying the refresh token. Almost the process is the same. Cheers.Intermediate
Thanks for the help @QuocTruong ... In the end, I did it with refresh and access token because the method with only the access token is unsupported with the library I chose. It was much easier than I thoughtKapoor
Thank you very much, I am searching this response by hours and found here in your tutorial...Yttria
The Authorization header needs to be 'Basic' and not 'Bearer' in the refresh request.Priorate
ah oh yes. Thanks @GojaGattisIntermediate

© 2022 - 2024 — McMap. All rights reserved.