Scalable JWT Token Revokation in Spring Boot

With stateless JWT Tokens for security, short TTLs (1 min) can be used. These tokens are then refreshed during their time to live. If the server does not get to know when a user has logged out, a token of a logged-out user could continue to be refreshed. One solution for this problem will be shown here that keeps a lot of the horizontal scalability.

Architecture

Revoked Tokens DB architecture

The architecture shows the microservices each with its own DB. The revoked tokens and the users need a single source of truth. The database needs to be highly available with multi-master or hot standby or another feature of the database. The revoked token database needs only two tables: one for the revoked tokens that gets called every 90 seconds by the microservices that cache the content of the revoked tokens table, and if the user logs out, and one for the users for the login. The microservices update the revoked tokens table after each logout with a defined time to live of the row and the logins are rate limited. That architecture reduces the load on the revoked tokens database to make it scale to larger deployments. The single source of truth for the revoked tokens is needed because each user request can be processed on any microservice and the revoked tokens need to be checked there. The user table is needed to enable the microservices to log in the users. That spreads out the load for the security checks over the microservices. The JWT token is checked in memory on the microservice and adds only a little CPU and no IO load.

Implementation

The implementation of the revoked tokens can be found in the MovieManager project.

Login

To support the revoked tokens, the login checks the number of currently revoked tokens of the user and slows down the login speed to limit the amount of revoked tokens a user can generate. That is done in the UserDetailsMgmt service:

Java

 

private UserDto loginHelp(Optional entityOpt, String passwd) {
   UserDto user = new UserDto();
   Optional myRole = entityOpt.stream()
      .flatMap(myUser -> Arrays.stream(Role.values())
    .filter(role1 -> Role.USERS.equals(role1))
           .filter(role1 -> 
              role1.name().equals(myUser.getRoles()))).findAny();
   if (myRole.isPresent() && entityOpt.get().isEnabled()
    && this.passwordEncoder.matches(passwd, entityOpt.get().getPassword())) {
    Callable callableTask = () -> this.jwtTokenService
           .createToken(entityOpt.get()
              .getUsername(), Arrays.asList(myRole.get()), Optional.empty());
        try {
       String jwtToken = executorService
              .schedule(callableTask, 3, TimeUnit.SECONDS).get();
       user = this.jwtTokenService
             .userNameLogouts(entityOpt.get().getUsername()) > 2 ? 
                user : this.userMapper.convert(entityOpt.get(), 
                   jwtToken, 0L);
    } catch (InterruptedException | ExecutionException e) {
       LOG.error("Login failed.", e);
    }
   }
   return user;
}

First, the role Optional of User entity is filtered out. Then it is checked if the User entity is present and has the Users role, is enabled, and the password matches. 

Then, a callable is created to create the JWT token for the user. The token has the Username and a UUID to identify each token on logout. The callable is executed with a 3-second delay on a different thread pool to limit the number of logouts a user can do between updates of the revoked token cache. 

Next, it is checked if more than 2 revoked tokens are cached for the user. If true, the login is denied. 

These 2 checks make sure that the amount of revoked tokens a user can generate is limited and limits the load on the login.

For horizontal scalability, this table has to be moved to the RevokedToken database.

Logout

The logout is implemented in the UserDetailsMgmt service:

Java

 

public Boolean logout(String bearerStr) {
   if (!this.jwtTokenService.validateToken(
      this.jwtTokenService.resolveToken(bearerStr).orElse(""))) {
    throw new AuthenticationException("Invalid token.");
   }
   String username = this.jwtTokenService.getUsername(
      this.jwtTokenService
        .resolveToken(bearerStr).orElseThrow(() -> 
           new AuthenticationException("Invalid bearer string.")));
   String uuid = this.jwtTokenService
      .getUuid(this.jwtTokenService.resolveToken(bearerStr)
     .orElseThrow(() -> 
             new AuthenticationException("Invalid bearer string.")));
   this.userRepository.findByUsername(username).orElseThrow(() -> 
      new ResourceNotFoundException("Username not found: " + username));
   long revokedTokensForUuid = this.revokedTokenRepository.findAll().stream()
    .filter(myRevokedToken -> myRevokedToken.getUuid().equals(uuid)
       && myRevokedToken.getName().equalsIgnoreCase(username)).count();
   if (revokedTokensForUuid == 0) {
      this.revokedTokenRepository.save(new RevokedToken(username, uuid,  
         LocalDateTime.now()));
   } else {
      LOG.warn("Duplicate logout for user {}", username);
   }
   return Boolean.TRUE;
}

First, it is checked if the JWT token is valid. Next, the username and the UUID are read out of the JWT token. Then, the Users table is checked for the user with the username of the token. The revokedTokens are checked for entries with the same UUID and UserID. If an entry is found, a warning is logged about a duplicate logout try. If it is the first log out of the JWT token, a new RevokedToken entity with username, UUID, and the current time is created in the revoked token table. 

For horizontal scalability, this table has to be moved to the RevokedToken database.

Revoked Tokens Cache Updates

The revoked tokens cache is updated with the CronJobs component:

Java

 

@Scheduled(fixedRate = 90000)
public void updateLoggedOutUsers() {
   LOG.info("Update logged out users.");
   this.userService.updateLoggedOutUsers();
}

Every 90 seconds the revoked tokes are read out of the table.

The update is handled in the UserDetailsMgmt service: 

Java

 

public void updateLoggedOutUsers() {
   final List revokedTokens =
      new ArrayList(this.revokedTokenRepository.findAll());
   this.jwtTokenService.updateLoggedOutUsers(
      revokedTokens.stream().filter(myRevokedToken -> 
         myRevokedToken.getLastLogout() == null || 
         !myRevokedToken.getLastLogout()
         .isBefore(LocalDateTime.now()
         .minusSeconds(LOGOUT_TIMEOUT))).toList());
   this.revokedTokenRepository.deleteAll(
     revokedTokens.stream().filter(myRevokedToken -> 
        myRevokedToken.getLastLogout() != null && myRevokedToken
         .getLastLogout().isBefore(LocalDateTime.now()
            .minusSeconds(LOGOUT_TIMEOUT))).toList());          
}

First, all revoked tokens are read from the table. Then, the entries that are older than the LOGOUT_TIMEOUT (185 sec) are removed. The others are cached in the JwtTokenService.

The JwtTokenService manages the revoked token cache:

Java

 

public record UserNameUuid(String userName, String uuid) {}
private final List loggedOutUsers = 
   new CopyOnWriteArrayList();

public void updateLoggedOutUsers(List revokedTokens) {
   this.loggedOutUsers.clear();
   this.loggedOutUsers.addAll(revokedTokens.stream()
     .map(myRevokedToken -> new UserNameUuid(myRevokedToken.getName(), 
    myRevokedToken.getUuid())).toList());
}

The UserNameUuid record has the values to identify the tokens. The loggedOutUsers list has the UserNameUuids of the logged-out users/revoked tokens. The CopyOnWriteArrayList is thread-safe.

The updateLoggedOutUsers gets the current list of revoked tokens and clears and then updates the loggedOutUsers list. The list is used for token validation.

JWT Token Validation

The JWT tokens have a username and hash that are checked for validation. Now the JWT tokens are also checked against the loggedOutUsers list to check the logouts. This is done in the JwtTokenFilter:

Java

 

@Override
public void doFilter(ServletRequest req, ServletResponse res, 
   FilterChain filterChain) throws IOException, ServletException {
   String token = jwtTokenProvider.resolveToken((HttpServletRequest) req);
   if (token != null && jwtTokenProvider.validateToken(token)) {
      Authentication auth = token != null ?  
         jwtTokenProvider.getAuthentication(token) : null;
      SecurityContextHolder.getContext().setAuthentication(auth);
   }        
   filterChain.doFilter(req, res);
}

The JwtTokenFilter is called before the requests are processed. First, the token is read out of the HTTP header. Then it is checked if the token was found and if it is valid (validateToken(…)), then the authentication is created and set in the SecurityContextHolder.

The token validation looks like this:

Java

 

public boolean validateToken(String token) {
   try {
      Jws claimsJws = Jwts.parserBuilder()
        .setSigningKey(this.jwtTokenKey).build().parseClaimsJws(token);
      String subject = Optional.ofNullable(
        claimsJws.getBody().getSubject()).orElseThrow(() -> 
           new AuthenticationException("Invalid JWT token"));
      String uuid = Optional.ofNullable(claimsJws.getBody()
         .get(JwtUtils.UUID, String.class)).orElseThrow(() -> 
             new AuthenticationException("Invalid JWT token"));
      return this.loggedOutUsers.stream().noneMatch(myUserName -> 
         subject.equalsIgnoreCase(myUserName.userName) && 
         uuid.equals(myUserName.uuid));
   } catch (JwtException | IllegalArgumentException e) {
      throw new AuthenticationException("Expired or invalid JWT token",e);
   }
}

First, the token is parsed, the signing key is checked, and the claims are read: otherwise, an exception is thrown. Then the subject(userName) and UUID are read. Then the token is checked against the loggedOutUsers. If all the checks are ok, the token is valid, and the request gets processed.

Conclusion

The time to live for a token is 60 seconds. After a logout token is written in the revoked tokens table, the cache is updated every 90 seconds. The revoked token remains in the table for 185 seconds. That means every token will need to be refreshed during the time it is in all the caches. Then the refresh will fail and the token becomes useless. The rate limit on the logins makes sure that the number of entries a user can create in a revoked tokens table is limited. All of that limits the load on the RevokedToken database to increase the number of microservices it can handle.

With such an architecture, the risk from lost tokens can be limited while keeping most of the scalability advantages that come from the distributed security checks of JWT token-based authentication.

For the synchronized clocks in the microservices, NTP could be used. Here is a how-to. This article shows how an Angular frontend can handle the tokens.

文章来源于互联网:Scalable JWT Token Revokation in Spring Boot

发布者:小站,转转请注明出处:http://blog.gzcity.top/4275.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022年5月3日 18:08
下一篇 2022年5月3日 18:08

相关推荐

  • Apache Log4J2 远程代码执行漏洞处置手册

    1、漏洞概述 Apache Log4j2是一个基于Java的日志记录工具。该日志框架被大量用于业务系统开发,用来记录日志信息。此次爆发的0day漏洞触发条件为只要外部用户输入的数据会被日志记录,即可造成远程代码执行。 漏洞细节 漏洞PoC 漏洞EXP 在野利用已公开 已公开 已公开 存在 参考链接:https://issues.apache.org/jira…

    2022年8月3日
    645720
  • 5 Steps to Strengthen API Security

    APIs are the connective tissue of scalable websites — fundamental to functioning in today’s digital world. But much like the physical world, weaknesses in connections and associate…

    安全 2022年5月3日
    692320
  • Secure Spring REST With Spring Security and OAuth2

    In this post, we are going to demonstrate Spring Security + OAuth2 for securing REST API endpoints on an example Spring Boot project. Clients and user credentials will be stored in…

    2022年5月3日
    89300
  • Using HTTPS to Secure Your Websites: An Intro to Web Security

    More and more sites are switching to HTTPS. And with good reason! Security is essential in today’s complex web ecosystem: logins, online payment systems, and personal user in…

    2022年5月3日
    20.3K36990
  • How JSON Web Token (JWT) Secures Your API

    You’ve probably heard that JSON Web Token (JWT) is the current state-of-the-art technology for securing APIs. Like most security topics, it’s important to understand ho…

    2022年5月3日
    35650

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

评论列表(6条)

  • Κωδικ αναφορ Binance
    Κωδικ αναφορ Binance 2024年7月25日 14:38

    Thank you for your sharing. I am worried that I lack creative ideas. It is your article that makes me full of hope. Thank you. But, I have a question, can you help me?

  • binance Регистрация
    binance Регистрация 2024年8月10日 10:09

    I don’t think the title of your article matches the content lol. Just kidding, mainly because I had some doubts after reading the article.

  • binance
    binance 2024年10月5日 15:51

    Your article helped me a lot, is there any more related content? Thanks!

  • binance
    binance 2024年11月13日 06:48

    Your point of view caught my eye and was very interesting. Thanks. I have a question for you.

  • вдкрити акаунт на бнанс

    Your point of view caught my eye and was very interesting. Thanks. I have a question for you.

  • cuenta de Binance
    cuenta de Binance 2025年1月10日 03:36

    Can you be more specific about the content of your article? After reading it, I still have some doubts. Hope you can help me.