Haom.in!

Hi!

How to authentication?

Discuss about Session and JWT token

1692 words
8 min read
There are two Kingdom on the Authentication continents, Session and JWT. The war between them never stop, but what's the difference between them and which one to use? Should I use JWT because it is so cooool? I'll share my idea in this article
Session-and-JWT

I want to build up an application, where users could log in, like a comment, view their profile, and make an edit.

This involves three levels of accessibility:

  • View their profile: anyone should be able to do this (to see the name, and avatar), even not log in
  • Like a comment: only authed users could do this
  • Edit the profile: only the user him/herself could do this

But how could we know who is sending the request?

Session

The traditional way is using session:

  1. log in with email and password:
    1. server authenticates the request
    2. server creates a session token, which could be a random string, stores it with some user’s info in database
    3. server send back the token to client, could be in cookie or in the local storage
  2. send a request to like a comment
    1. the token is taken along with the request, in cookie, or in the header
    2. server extract the token from the request
    3. read from database, and get the user info with that token
    4. now we know who is acting, has him/her like this comment, …
    5. server send back the result after business

Problem

First,we need to read from our database for each request, this will slow down the response time, and really “hit” our database.

Moreover, it will be really hard to scale up, since we always need to ask the machine with the tokens.

JWT

The idea of JWT is to store the user’s info in the token itself, so instead of server asking the database each time, it just let the request itself to tell who they are. The token itself is “public”, decoded by Base64, and anyone can read it’s content In this way, no database for token, and no hit every request.

But what if the client lie? claiming them to be someone else? The structure of JWT avoid this:

one JWT token has three parts:

  • header: where to place the basic info of the token, like the algorithm and type
  • payload: where we store the user info, name, id, …
  • signature: server will generate the signature with their secret key or private key, based on the content, so we can determine if this token is issued by the server.

Now, let’s move back to our scenario:

  1. log in with email and password:
    1. server authenticates the request
    2. server generate a JWT token based on user’s info and the secret key
  2. send a request to like a comment
    1. the token is taken along with the request, in cookie, or in the header
    2. server extract the token from the request
    3. server validate the token, and extract the payload, which contains the user’s info
    4. now we know who is acting

This is called stateless, since the server no longer store state of the user and session.

Problem

But this method has a huge risk: since the authentication is stateless, the server will only rely on the token user provide, and as long as they have a valid token, they will be allowed to enter.

Logout doesn’t really log out!

I have built one project based on JWT before, where I store tokens in cookie, and when user logout, I should set-Cookie to clean up the cookie, but I forget, so user can still send requests without any limitation, as long as the token is not expired.

Cannot immediately block and update user

Suppose we want to lock one user, or give one user a role as ADMIN, this will not work until token expires or user throw away their token, and login again.

Best target

JWTs are often not encrypted, so anyone is able to perform a man-in-the-middle attack, and take your token. Moreover, we cannot invalid it immediately.

Is this really Stateless?

There is one solution seems can solve these problem very easily, and which is I learnt at first from many resources: we read the latest user details from database or cache after validate the token.

For example, I found an example project from bezkoder.

@Service
public class UserDetailsServiceImpl implements UserDetailsService {
  @Autowired
  UserRepository userRepository;

  @Override
  @Transactional
  public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    User user = userRepository.findByUsername(username)
        .orElseThrow(() -> new UsernameNotFoundException("User Not Found with username: " + username));

    return UserDetailsImpl.build(user);
  }

}

// and for each request, we add a filter

public class AuthTokenFilter extends OncePerRequestFilter {
  @Autowired
  private JwtUtils jwtUtils;

  @Autowired
  private UserDetailsServiceImpl userDetailsService;

  private static final Logger logger = LoggerFactory.getLogger(AuthTokenFilter.class);

  @Override
  protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
      throws ServletException, IOException {
    try {
      String jwt = parseJwt(request);
      if (jwt != null && jwtUtils.validateJwtToken(jwt)) {
        String username = jwtUtils.getUserNameFromJwtToken(jwt);

        UserDetails userDetails = userDetailsService.loadUserByUsername(username);
        UsernamePasswordAuthenticationToken authentication =
            new UsernamePasswordAuthenticationToken(
                userDetails,
                null,
                userDetails.getAuthorities());
        authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));

        SecurityContextHolder.getContext().setAuthentication(authentication);
      }
    } catch (Exception e) {
      logger.error("Cannot set user authentication: {}", e);
    }

    filterChain.doFilter(request, response);
  }

  private String parseJwt(HttpServletRequest request) {
    String headerAuth = request.getHeader("Authorization");

    if (StringUtils.hasText(headerAuth) && headerAuth.startsWith("Bearer ")) {
      return headerAuth.substring(7);
    }

    return null;
  }
}

We only need to store user id inside the token, and the latest info will be loaded inside the filter, for each request we received.

This code wors well, and in my past project, there is no big issue yet. But gradually, I notice one problem: What if I replace the token with a random String, and also store a pair token-userId in database or cache?

This turns out to be similar to the session method, except we don’t need to store the token-userId pairs, but use a JWT token to store it. Well, at least we save up one step, but first, the JWT token is usually longer than session id. But we save up one I/O, it is much better than 100~300 bytes we saved. But(×\times 3), we could save it and accounts info in the cache.

Moreover, thinking about this method, it actually inherits some cons of both methods: It needs to hit database or cache, cannot really log out, unless we store session in token, instead of id (well, this just wraps the session id with the JWT token).

Two Token and Token Refresh

Another method is using two token during the communication. After login, server issues one access token and one refresh token.

Access token is just like the JWT token we mentioned before, store users’ info in it. However, this time, it will have a much shorter expiry, like 5~15 minutes.

Refresh token can be a random string, just like the session, and it can have a long expiry just like a session (in fact, it actually is a session). When access token expired, server will return a specified code at next request, or the client set a timer, and the client should send a refresh request with their refresh token. Server will validate their refresh token, and issue new access token based on user info.

This method ensures that, token now has a much shorter life, making them not such “stateless”, and once attacker get the access token, they can only use it for a short term, while refresh token is less frequently used.

Token Refresh adds that: instead of only issuing new access token, we should also issue new refresh token, which makes this even much safer.

Moreover, what if the attacker really get the refresh token? We could check token reused on server, and revoked the session once refresh token is used twice.

For example, I will create a table refresh session, and its model like this:

/**
 * Represents a refresh session for rotating refresh tokens.
 *
 * @property id Unique identifier for the refresh session.
 * @property accountId Identifier of the account that owns the session.
 * @property currentToken Current refresh token for the session.
 * @property previousToken Previous refresh token for rotation checks (nullable).
 * @property lastUsedAt Timestamp when the refresh session was last used.
 * @property expiresAt Timestamp when the refresh session expires.
 * @property revokedAt Timestamp when the session was revoked (nullable).
 * @property revokedReason Reason for revocation (nullable).
 * @property createdAt Timestamp when the refresh session was created.
 * @property updatedAt Timestamp when the refresh session was last updated.
 */
data class RefreshSession(
    val id: UUID,
    val accountId: UUID,
    val currentToken: String,
    val previousToken: String?,
    val lastUsedAt: OffsetDateTime,
    val expiresAt: OffsetDateTime,
    val revokedAt: OffsetDateTime?,
    val revokedReason: String?,
    val createdAt: OffsetDateTime,
    val updatedAt: OffsetDateTime,
) {
    val isRevoked: Boolean = revokedAt != null
}

and use a JWT with sessionId, currentToken inside as the refresh token.

When receiving a refresh request, server extract the sessionId and currentToken from it, and match it with the session in database. If it is valid, do the refresh, otherwise, we can consider a reuse happened, and revoke the session immediately.

I also add a previousToken, this is used to avoid client send multiple refresh request, maybe caused by internet issue. If the provided token match the previousToken, and is in a grace time, server will return current refresh token instead.

Which one to choose?

We can find the relationship between JWT and session from the two token methods. If I set access token’s expiry to 0, and only use refresh token for authentication, we got a session authentication. If I set access token’s expiry to 15 days, we got a more JWT token authentication.

If you want to benefit from statelessness, then you have to suffer some disadvantages from it.

In my opinion, JWT means statelessness, and which is not the choice for most projects, since most of them has no requirement for statelessness. However, if I want to make my project horizontal scalable, I will choose to use JWT token for authentication, and build up a auth server, which does authentication and issue tokens.

Let’s think about Firebase auth, Auth0, and all the times you choose third-party login: we send password and email to the third party, which then return the JWT token, and we used it in other project. These projects do not need to ask teh auth server the for account info, but simply parse the token.