authentication – Refresh tokens by example using Angular and Spring Boot

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?