2

I am designing out an app that would have an Angular frontend and Spring Boot (Java) backend.

I was considering (but not married to) the prospect of JWT-based authentication:

  1. User logs in with username and password and submits a form; this form POSTS to, say, /auth/sign-in
  2. A Spring Security filter checks to see if the username and password are valid, and if so, generates a JWT with a reasonable (say, 15 minute) expiry and returns the JWT as a response header
  3. Angular client then reuses this JWT until it expires, at which point, Spring Security will send back a specific 400-level error code which will prompt the Angular frontend to redirect the user to the login page and log back in

This is pretty straightforward to implement, but will be a bad UX for my users (if they have to log back in every 15 minutes).

Instead I'm thinking of something along these lines:

  1. Maintain an in-memory cache of some kind mapping each unique user ID to the last activity timestamp (a timestamp representing the last activity they were doing; basically the last time that user did anything in the app that resulted with a call to the backend being made) --> userActivityMap : Map<User,Date>
  2. Every time the user (via the Angular client) does anything in the app that prompts Angular to make a call to the backend, this user activity map is updated with the current timestamp
  3. User logs in with username and password and submits a form; this form POSTS to, say, /auth/sign-in
  4. A Spring Security filter checks to see if the username and password are valid, and if so, generates a JWT with a reasonable (say, 15 minute) expiry and returns the JWT as a response header
  5. Angular client then reuses this JWT until it expires, at which point, the Spring Security filter that detected it as being expired subsequently consults the user activity map. If the user has been active within the last, say, 15 minutes, then a new JWT "refresh token" is generated for the user and sent back to the Angular client with a specific 400-level error code
  6. The Angular client sees this specific error code and knows to pluck the refresh token off the error response, use the value of that token as the main access token/JWT moving forward, and retries the same command with the new (refreshed) access token

I think I could get this to work, but in my Google searches to find what others have done I was surprised to see a million beginner-level "how to" articles but nothing really concrete for this use case. What are typical solutions for this given stack and the specific scenario at hand? Is my solution close to generally-accepted best practices or did I miss the mark considerably?

2

It sounds like you're reinventing the wheel a bit here.

JWT authentication has a well-documented "protocol" already defined for this. When the user initially logs in, you provide both a bearer token and a refresh token. The bearer token is the short-living token you've already mentioned, the refresh token is a longer-living token used to get a new bearer token when the current one expires.

How you handle the refresh is up to you; you can go for a "better to ask forgiveness than permission approach" or a "look before you leap" approach. The former being waiting for a request to fail with an unauthorised status code, and then requesting a new bearer token and retrying the request. The latter being checking the exp claim of the bearer token either on an interval or before each request, and requesting a new token when you're within a specified threshold of the expiration time.

JWT authentication is a fairly loose concept, you can implement the client and server sides in a way that meets your requirements, but sticking to a bearer + refresh token approach is definitely recommended.

Whether you cache the refresh token or save it in a database is up to you. Keep in mind, caching introduces a lot of complexity when it comes to blacklisting refresh tokens for recently blocked users.

There are third-party solutions available such as AWS Cognito that you could use. They're far from perfect, but I would consider all options available if I were you.

4
  • Thanks for this @Dom (+1) - just so I understand your first (forgiveness > permission) model, it sounds like you're saying the flow would be: (1) user logs in, (2) server generates access token (the JWT) and the refresh token and sends them both back, (3) client stores both tokens but only uses the access token to make requests with, (4) eventually the server realizes the access token is expired and sends back a specific error that the client know to use to request a new access token, and (5) client receives this error and then finally uses the refresh token to request a new access token. – hotmeatballsoup Feb 18 at 13:31
  • ^^^ Is this a fair summary of the typical flow? Thanks again! – hotmeatballsoup Feb 18 at 13:31
  • @hotmeatballsoup yep that sums it up, although to keep it simple you should start with retrying on a 401, and then if the system gets more complex and you need to use 401's for more than expired tokens, then introduce more customised error handling. Additionally, when they logout or an admin revokes their access, you would simply delete or invalidate their refresh token so that when they try to get a new bearer token it fails; that also means it would take however long the bearer token lives for access to be revoked which is why I stick to a 5 minute lifetime when it's up to me. – Dom Feb 18 at 14:58
  • 1
    Alongside just revoking access, you could blacklist their bearer token to have immediate effect, but that gets into complicated, hard-to-horizontally-scale caching scenarios. – Dom Feb 18 at 14:59

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.