13

I implemented JWT authentication for my Spring boot app. Overally, it works like this:

  1. Client sends username, password to the login endpoint.
  2. Server checks if the provided credentials are valid.
  3. If no, it would return an error
  4. If yes, it would return a token, that token actually includes
  5. Client sends that token with every future request

The question is, how should we implement logout ?

import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import java.util.Date;

class TokenAuthenticationService {
    static final long EXPIRATIONTIME = 864_000_000; // 10 days
    static final String SECRET = "ThisIsASecret";
    static final String TOKEN_PREFIX = "Bearer";
    static final String HEADER_STRING = "Authorization";

    static void addAuthentication(HttpServletResponse res, String username) {
        String JWT = Jwts
                .builder()
                .setSubject(username)
                .setExpiration(
                        new Date(System.currentTimeMillis() + EXPIRATIONTIME))
                .signWith(SignatureAlgorithm.HS512, SECRET).compact();
        res.addHeader(HEADER_STRING, TOKEN_PREFIX + " " + JWT);
    }

    static Authentication getAuthentication(HttpServletRequest request, UserDetailsService customUserDetailsService) {
        String token = request.getHeader(HEADER_STRING);
        if (token != null) {
            // parse the token.
            Claims claims = Jwts.parser().setSigningKey(SECRET)
                    .parseClaimsJws(token.replace(TOKEN_PREFIX, "")).getBody();
            String userName = claims.getSubject();
            Date expirationTime = claims.getExpiration();
            if (expirationTime.compareTo(new Date()) < 0) {
                return null;
            }
            UserDetails user = customUserDetailsService.loadUserByUsername(userName);
            return user != null ? new UsernamePasswordAuthenticationToken(user.getUsername(),
                    user.getPassword(), user.getAuthorities()) : null;
        }
        return null;
    }
}

addAuthentication is used by the JWTLoginFilter class to send the authentication code when logging in, 'getAuthenticationis used by theJWTAuthenticationFilter` that filter all requests to the end points.

What is the best practice here ?

Arian
  • 7,397
  • 21
  • 89
  • 177

3 Answers3

40

I don't think there is a best practice here. I guess it depends on the application you're building and it's requirements.

The benefit of JWT is that they're stateless. You don't need to query the database to validate the token. This is good when you wish to reduce the load on your database but bad when you want to invalidate an existing non-expired token.

Possible solutions:

  • Store JWT in the database. You can check which tokens are valid and which ones are revoked but this defeats the purpose of using JWT at all in my opinion.
  • Delete token from the client. This would stop the client from being able to make authenticated requests but if the token is still valid and somebody else has access to it, the token could still be used. This leads me to my next point.
  • Short token lifetime. Let the tokens expire quickly. Depending on the application, it could be several minutes or half an hour. When the client deletes its token, there's a short window of time where it can still be used. Deleting the token from the client and having short token lifetimes would not require major modifications on the back-end. But short token lifetimes would mean that the user is constantly being logged out because the token has expired.
  • Rotate tokens. Maybe introduce a concept of refresh tokens. When the user logs in, provide them with a JWT and a refresh token. Store the refresh token in a database. For authenticated requests, the client can use the JWT but when the token expires (or is about to expire), let the client make a request with the refresh token in exchange for a new JWT. This way you would only have to hit the database when a user logs in or asks for a new JWT. When the user logs out, you would need to invalidate the stored refresh token. Otherwise somebody listening in on the connection could still get new JWTs even though the user had logged out.
  • Create a JWT blacklist. Depending on the expiration time, when the client deletes its token, it might still be valid for some time. If the token lifetime is short, it might not be an issue, but if you still wish that the token is invalidated immediately, you could create a token blacklist. When the back-end receives a logout request, take the JWT from the request and store it in an in-memory database. For each authenticated request you would need to check your in-memory database to see if the token has been invalidated. To keep the search space small, you could remove tokens from the blacklist which have already expired.
Indrek Ots
  • 3,701
  • 1
  • 19
  • 24
  • Very nice list of all the possible (feasible) solutions, valid for any general JWT implementation independent of the actual system it is implemented on / in. – Larzan Jul 18 '19 at 07:31
3

I don't know what's best practice, but in a system whose internals I have seen, there is a central authentication manager which knows all the currently valid authentication tokens, so logging out would simply consist of removing the token from the collection of valid tokens.

So, next time the authentication manager is asked whether the token is valid, it would respond with a "no".

Mike Nakis
  • 56,297
  • 11
  • 110
  • 142
  • There would a problem here. Look at how `addAuthentication` is implemented. The authentication code depends on two factors: 1.username 2.expiration time. username never changes, therefore the authentication code would be valid until the expiration time comes. – Arian Apr 23 '17 at 10:44
  • Unless it is only considered valid if it can be found in the authentication manager's collection of currently valid tokens. Which is precisely what my answer suggests. – Mike Nakis Apr 23 '17 at 10:48
  • In any case, if that's what's bothering you, add one more unique term to the token, like a counter. – Mike Nakis Apr 23 '17 at 10:49
  • Does the spring authentication manager implement the functionality you mentioned by default ? shouldn't we define a database table for that? is it kept in memory ? – Arian Apr 23 '17 at 18:19
  • 1
    I don't know whether spring implements that. If it does, then it is most probably in-memory, with the possibility of adding your own handlers (somewhere, somehow) to persist to the database. A database solution would be better because a) it would allow you to never actually delete entries, only "retire" them, so that you can have a history of what happened, and b) it would (theoretically) allow you to restart your server without (necessarily) logging everyone out. – Mike Nakis Apr 24 '17 at 09:19
  • I think you are talking about opaque tokens, whereas the question is about JWT. – abbas Jan 05 '23 at 01:18
1

1) Simply remove the token from the client

2) Create a token blacklist

3) Just keep token expiry times short and rotate them often

Please have a look at Invalidating JSON Web Tokens Invalidating JSON Web Tokens

Ziaullhaq Savanur
  • 1,848
  • 2
  • 17
  • 20