Building a user management microservice (Part 6): Adding remember me authentication with persistent JWT tokens

Last time I added username and password based authentication with using Spring Security. Should you have missed the that, I notice here that JWT tokens were issued upon a successful login and validated for subsequent requests. Creating long-lived JWTs isn’t practical, as they’re self contained and there’s no way to revoke them. If tokens are stolen all bets are off. For that reason, I wanted to add the classic remember-me style authentication with persistent tokens. Remember-me tokens are stored in cookies as JWTs as the first line of defense, however they are also persisted to the database and their lifecycle is being tracked.

This time I’d like to start with demonstrating how the running user management app works and later dive into the details.

Authentication flow

Basically what happens in that users authenticate with a username / password pair and they might indicate their intention that they want the app to remember them (persistent session). Most of the time there’s an additional checkbox on the UI to make that happen. As the app hasn’t had a UI developed yet, we do everything with cURL.

  1. Login

     curl -D- -c cookies.txt -b cookies.txt \
     -XPOST http://localhost:5000/auth/login \
     -d '{ "username":"test", "password": "test", "rememberMe": true }'
    
     HTTP/1.1 200
     ...
     Set-Cookie: remember-me=eyJhbGciOiJIUzUxMiJ9...;Max-Age=1209600;path=/;HttpOnly
     X-Set-Authorization-Bearer: eyJhbGciOiJIUzUxMiJ9...

    Upon successful authentication the PersistentJwtTokenBasedRememberMeServices creates a persistent Session, saves it to the database and converts it into a JWT token. It takes care of storing this persistent Session to a cookie on the client’s side (Set-Cookie) and it also sends the newly created transient token. The latter is meant to be used through the lifetime of the single page front-end and sent with a non-standard HTTP header (X-Set-Authorization-Bearer).

    When the rememberMe flag is false, just a stateless JWT token is created and the remember-me infrastructure is completely bypassed.

  2. Using only the transient token while the the app is running

    While the app is open in the browser, it sends the transient JWT token in the Authorization header with every XHR request. When the application gets reloaded however, the transient token gets lost.

    For the sake of simplicity normal GET /users/{id} is used here to demonstrate a normal request.

    curl -D- -H 'Authorization: Bearer eyJhbGciOiJIUzUxMiJ9...' \
      -XGET http://localhost:5000/users/524201457797040
    
    HTTP/1.1 200
    ...
    {
      "id" : 524201457797040,
      "screenName" : "test",
      "contactData" : {
      "email" : "test@springuni.com",
      "addresses" : [ ]
      },
      "timezone" : "AMERICA_LOS_ANGELES",
      "locale" : "en_US"
    }
  3. Using the transient token in conjunction with the persistent token

    This happens when the user has selected remember-me authentication in the first scenario.

    curl -D- -c cookies.txt -b cookies.txt \
      -H 'Authorization: Bearer  eyJhbGciOiJIUzUxMiJ9...' \
      -XGET http://localhost:5000/users/524201457797040
    
    HTTP/1.1 200
    ...
    {
      "id" : 524201457797040,
      "screenName" : "test",
      "contactData" : {
        "email" : "test@springuni.com",
        "addresses" : [ ]
      },
      "timezone" : "AMERICA_LOS_ANGELES",
      "locale" : "en_US"
    }
    

    In this scenario both the transient JWT token and a valid remember-me cookie are sent at the same time. As long as the single page application is running, the transient token is used.

  4. Using the persistent token upon initialization

    When the front-end gets loaded in the browser, it doesn’t know about the existence of any transient JWT tokens. All it can do is testing the persisted remember-me cookie by trying to execute a normal request.

    curl -D- -c cookies.txt -b cookies.txt \
      -XGET http://localhost:5000/users/524201457797040
    
    HTTP/1.1 200
    ...
    Set-Cookie: remember-me=eyJhbGciOiJIUzUxMiJ9...;Max-Age=1209600;path=/;HttpOnly
    X-Set-Authorization-Bearer: eyJhbGciOiJIUzUxMiJ9...
    
    {
      "id" : 524201457797040,
      "screenName" : "test",
      "contactData" : {
        "email" : "test@springuni.com",
        "addresses" : [ ]
      },
      "timezone" : "AMERICA_LOS_ANGELES",
      "locale" : "en_US"
    }

    If the persistent token (cookie) is still valid it gets updated in the database keeping on record the last time it was used and it also gets updated in the browser. Another important step is also performed, the user gets authenticated automatically again without having to give their username / password pair and a new transient token is created. From now on, the app uses the transient token as long as it’s running.

  5. Logging out

    Altought logging out seems simple, there are a few details we need to be aware of. The front-end still send the stateless JWT token, as long as the user is authenticated, otherwise the logout button on UI wouldn’t even be offered and the back-end wouldn’t know how logs out.

    curl -D- -c cookies.txt -b cookies.txt \
      -H 'Authorization: Bearer eyJhbGciOiJIUzUxMiJ9...' \
      -XPOST http://localhost:5000/auth/logout
    
    HTTP/1.1 302 
    Set-Cookie: remember-me=;Max-Age=0;path=/
    Location: http://localhost:5000/login?logout

    After this request the remember-me cookie get reset and also the persistent session in the database flagged as deleted.

Implementing Remember-me authentication

As I mentioned in the summary, we’re going to use persistent tokens for added security in order to be able to revoke them any time we wish. There are three steps we need to perform to enable proper remember-me handling with Spring Security.

    1. Implement UserDetailsService

      In the first post, I decided that the model will be developed with DDD, thus it couldn’t depend on any framework specific class. Actually, it doesn’t even depend on any 3rd party framework or library. Most tutorials usually just implement UserDetailsService directly and there’s no extra layer between the business logic and the framework used to build the application.

      UserServices was added to the project long ago in the second part, thus our task is quite simple, because all we need now is a framework specific component which delegates the responsibility of a UserDetailsService to the existing logic.

       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      public class DelegatingUserService implements UserDetailsService {
      
        private final UserService userService;
      
        public DelegatingUserService(UserService userService) {
          this.userService = userService;
        }
      
        @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
          Long userId = Long.valueOf(username);
          UsernameNotFoundException usernameNotFoundException = new UsernameNotFoundException(username);
          return userService.findUser(userId)
              .map(DelegatingUser::new)
              .orElseThrow(() -> usernameNotFoundException);
        }
      
      }
      

      Is just a simple wrapper around UserService which eventually converts the returned User model object to a framework specific UserDetails instance. Other than that, in this project we don’t use the user’s login name (email address or screen name) directly. Instead their users’ ID is passed around everywhere.

    2. Implement PersistentTokenRepository

      Fortunately we have an equally easy job in adding a proper PersistentTokenRepository implementation, as the domain model already contains SessionService and Session.

       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      public class DelegatingPersistentTokenRepository implements PersistentTokenRepository {
      
        private static final Logger LOGGER =
            LoggerFactory.getLogger(DelegatingPersistentTokenRepository.class);
      
        private final SessionService sessionService;
      
        public DelegatingPersistentTokenRepository(SessionService sessionService) {
          this.sessionService = sessionService;
        }
      
        @Override
        public void createNewToken(PersistentRememberMeToken token) {
          Long sessionId = Long.valueOf(token.getSeries());
          Long userId = Long.valueOf(token.getUsername());
          sessionService.createSession(sessionId, userId, token.getTokenValue());
        }
      
        @Override
        public void updateToken(String series, String tokenValue, Date lastUsed) {
          Long sessionId = Long.valueOf(series);
          try {
            sessionService.useSession(sessionId, tokenValue, toLocalDateTime(lastUsed));
          } catch (NoSuchSessionException e) {
            LOGGER.warn("Session {} doesn't exists.", sessionId);
          }
        }
      
        @Override
        public PersistentRememberMeToken getTokenForSeries(String seriesId) {
          Long sessionId = Long.valueOf(seriesId);
          return sessionService
              .findSession(sessionId)
              .map(this::toPersistentRememberMeToken)
              .orElse(null);
        }
      
        @Override
        public void removeUserTokens(String username) {
          Long userId = Long.valueOf(username);
          sessionService.logoutUser(userId);
        }
      
        private PersistentRememberMeToken toPersistentRememberMeToken(Session session) {
          String username = String.valueOf(session.getUserId());
          String series = String.valueOf(session.getId());
          LocalDateTime lastUsedAt =
              Optional.ofNullable(session.getLastUsedAt()).orElseGet(session::getIssuedAt);
          return new PersistentRememberMeToken(
              username, series, session.getToken(), toDate(lastUsedAt));
        }
      
      }
      

      The situation is roughly the same as with UserDetailsService, the wrapper converts between PersistentRememberMeToken and Session. The only thing which was taken extra care of is the date field in PersistentRememberMeToken. In Session, I separated that two date fields (ie. issuedAt and lastUsedAt) and the latter gets its first value when the user first logs in with the help of a remember-me token. Hence there’s chance that it’s null and when it is, the value of issuedAt is used instead.

    3. Implement RememberMeServices

      At this point we re-use PersistentTokenBasedRememberMeServices and customize for the task at hand, it depends on both UserDetailsService and PersistentTokenRepository and those were already taken care of.

       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      public class PersistentJwtTokenBasedRememberMeServices extends
          PersistentTokenBasedRememberMeServices {
      
        private static final Logger LOGGER =
            LoggerFactory.getLogger(PersistentJwtTokenBasedRememberMeServices.class);
      
        public static final int DEFAULT_TOKEN_LENGTH = 16;
      
        public PersistentJwtTokenBasedRememberMeServices(
            String key, UserDetailsService userDetailsService,
            PersistentTokenRepository tokenRepository) {
      
          super(key, userDetailsService, tokenRepository);
        }
      
        @Override
        protected String[] decodeCookie(String cookieValue) throws InvalidCookieException {
          try {
            Claims claims = Jwts.parser()
                .setSigningKey(getKey())
                .parseClaimsJws(cookieValue)
                .getBody();
      
            return new String[] { claims.getId(), claims.getSubject() };
          } catch (JwtException e) {
            LOGGER.warn(e.getMessage());
            throw new InvalidCookieException(e.getMessage());
          }
        }
      
        @Override
        protected String encodeCookie(String[] cookieTokens) {
          Claims claims = Jwts.claims()
              .setId(cookieTokens[0])
              .setSubject(cookieTokens[1])
              .setExpiration(new Date(currentTimeMillis() + getTokenValiditySeconds() * 1000L))
              .setIssuedAt(new Date());
      
          return Jwts.builder()
              .setClaims(claims)
              .signWith(HS512, getKey())
              .compact();
        }
      
        @Override
        protected String generateSeriesData() {
          long seriesId = IdentityGenerator.generate();
          return String.valueOf(seriesId);
        }
      
        @Override
        protected String generateTokenData() {
          return RandomUtil.ints(DEFAULT_TOKEN_LENGTH)
              .mapToObj(i -> String.format("%04x", i))
              .collect(Collectors.joining());
        }
      
        @Override
        protected boolean rememberMeRequested(HttpServletRequest request, String parameter) {
          return Optional.ofNullable((Boolean)request.getAttribute(REMEMBER_ME_ATTRIBUTE)).orElse(false);
        }
      
      }
      

      This particular implementation uses JWT tokens as the materialized form for storing remember-me tokens in cookies. Spring Security’s default form could have been just fine as well, but JWT add an extra layer of security. The default implementation doesn’t have a signature and every request end up being a query in the database for checking upon the remember-me token.

      JWT prevents that, although parsing it and validating its signature need some more CPU cycles.

    4. Wire all of these together

       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      @Configuration
      public class AuthSecurityConfiguration extends SecurityConfigurationSupport {
      
        ...
      
        @Bean
        public UserDetailsService userDetailsService(UserService userService) {
          return new DelegatingUserService(userService);
        }
      
        @Bean
        public PersistentTokenRepository persistentTokenRepository(SessionService sessionService) {
          return new DelegatingPersistentTokenRepository(sessionService);
        }
      
        @Bean
        public RememberMeAuthenticationFilter rememberMeAuthenticationFilter(
            AuthenticationManager authenticationManager, RememberMeServices rememberMeServices,
            AuthenticationSuccessHandler authenticationSuccessHandler) {
      
          RememberMeAuthenticationFilter rememberMeAuthenticationFilter =
              new ProceedingRememberMeAuthenticationFilter(authenticationManager, rememberMeServices);
      
          rememberMeAuthenticationFilter.setAuthenticationSuccessHandler(authenticationSuccessHandler);
      
          return rememberMeAuthenticationFilter;
        }
      
        @Bean
        public RememberMeServices rememberMeServices(
            UserDetailsService userDetailsService, PersistentTokenRepository persistentTokenRepository) {
      
          String secretKey = getRememberMeTokenSecretKey().orElseThrow(IllegalStateException::new);
      
          return new PersistentJwtTokenBasedRememberMeServices(
              secretKey, userDetailsService, persistentTokenRepository);
        }
      
        ...
      
        @Override
        protected void customizeRememberMe(HttpSecurity http) throws Exception {
          UserDetailsService userDetailsService = lookup("userDetailsService");
          PersistentTokenRepository persistentTokenRepository = lookup("persistentTokenRepository");
          AbstractRememberMeServices rememberMeServices = lookup("rememberMeServices");
          RememberMeAuthenticationFilter rememberMeAuthenticationFilter =
              lookup("rememberMeAuthenticationFilter");
      
          http.rememberMe()
              .userDetailsService(userDetailsService)
              .tokenRepository(persistentTokenRepository)
              .rememberMeServices(rememberMeServices)
              .key(rememberMeServices.getKey())
              .and()
              .logout()
              .logoutUrl(LOGOUT_ENDPOINT)
              .and()
              .addFilterAt(rememberMeAuthenticationFilter, RememberMeAuthenticationFilter.class);
        }
      
        ...
      
      }
      

      The magic is in the last part obviously. Basically, it’s about registering components with Spring Security and enable remember-me services. What’s interesting though is that we need a key (a string) which used by AbstractRememberMeServices internally. AbstractRememberMeServices  is also the default logout handler in this setup and takes care of marking tokens in the database as deleted upon logout.

Gotchas

  1. Receiving users’ credentials and the remember-me flag as JSON data in the body of POST request

    By default UsernamePasswordAuthenticationFilter expects credentials as HTTP request parameters of a POST request, however we want to send a JSON document instead. Further down the pipeline, AbstractRememberMeServices also checks upon the existence of the remember-me flag as a request parameter.In order to fix that, LoginFilter set the remember-me flag as a request attribute and delegates the decision to PersistentTokenBasedRememberMeServices if remember-me authentication needs to be initiated or not.

  2. Handling login success with RememberMeServices

    RememberMeAuthenticationFilter doesn’t proceed to next filters in the filter chain, but it stops its execution if an AuthenticationSuccessHandler is set.

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    public class ProceedingRememberMeAuthenticationFilter extends RememberMeAuthenticationFilter {
    
      private static final Logger LOGGER =
          LoggerFactory.getLogger(ProceedingRememberMeAuthenticationFilter.class);
    
      private AuthenticationSuccessHandler successHandler;
    
      public ProceedingRememberMeAuthenticationFilter(
          AuthenticationManager authenticationManager, RememberMeServices rememberMeServices) {
    
        super(authenticationManager, rememberMeServices);
      }
    
      @Override
      public void setAuthenticationSuccessHandler(AuthenticationSuccessHandler successHandler) {
        this.successHandler = successHandler;
      }
    
      @Override
      protected void onSuccessfulAuthentication(
          HttpServletRequest request, HttpServletResponse response, Authentication authResult) {
    
        if (successHandler == null) {
          return;
        }
    
        try {
          successHandler.onAuthenticationSuccess(request, response, authResult);
        } catch (Exception e) {
          LOGGER.error(e.getMessage(), e);
        }
      }
    
    }
    

    ProceedingRememberMeAuthenticationFilter is a customized version of the original filter which doesn’t stop when authentication succeeds.

Next in this series

Building a user management microservice (Part 7): Putting it together

About the Author László Csontos

I’m László Csontos (@springunidotcom) and my focus area has been Java development in the last 12 years. During past projects I had almost always worked on the back-end. Later I specialized in developing applications with the Spring Framework and got acquainted with its internals. I’d like to share and pass that knowledge on what I learned as a software engineer and help others to boost their carriers by learning Spring which is the most popular Java framework out there for creating enterprise software.