Remember-me or persistent-login authentication refers to websites being able to remember the identity of a principal between sessions. Spring Security provides the necessary hooks for these operations for servlet based applications but not for reactive type application (see github issue). This project demonstrates how to implement persistent Remember-me authentication for a reactive Spring boot app with Spring Security.
The application utilizes GitHub REST API for user authentication. When a login request with the GitHub Personal Access token is received by this application, it in turn calls the GitHub REST API to retrieve the user details associated with the token. If the API token is valid, the spring security context is populated with the fetched user information and two tokens (JWT and Remember-me) are generated and sent back to the client as cookies. The client can use the JWT (Json Web Token) cookie as a bearer token for subsequent requests, and the Remember-me cookie is stored in the database. The Remember-me cookie is used to authenticate the user for subsequent request when the JWT token is expired.
- Create a GitHub Personal Access Token
- Java 21
- Docker to run the app and PostgresSQL database
-
From the root of the project, run the docker-compose.yml file to start the application and PostgresSQL database
docker compose up -d
The following output is displayed when the containers are started successfully.
[+] Running 3/3 ✔ Network boot-reactive-jwt-security-rememberme_app_nw Created 0.0s ✔ Container postgres Healthy 10.7s ✔ Container app Started
During startup the database is initialized with a single table (schema.sql) to store the Remember-me cookie details.
-
Run the
curl
command to authenticate with GitHub REST API by providing the correct API Token and therememberMe
flag set to truecurl -v -X POST http://localhost:8080/auth/login \ -H 'Content-Type: application/json' \ -d '{"personalAccessToken":"ghp_hrshOO2323N86Xu7csfscDlT688Y10Mv0esdH2","rememberMe":true}'
The following response is returned containing two cookies:
jwt_token
andremember_me
.* Connected to localhost (::1) port 8080 > POST /auth/login HTTP/1.1 > Host: localhost:8080 > User-Agent: curl/8.4.0 > Accept: */* > Content-Type: application/json > Content-Length: 65 > < HTTP/1.1 200 OK < Set-Cookie: jwt_token=eyJhbGciOiJIUzI1NiJ9.eyJzdWiOiJUB2bXdhcmUuY29tIiwicm9sZXMiOiJST0xFX1VTRVIiLCJpYXQiOjE3MDU1MTMzMDIsImV4cCI6MTcwNTUxkwMn0.Yezpx634-eeO2rTjtfGa5JVSYbHPkZiF3WxrcX-5HSc; Path=/; Max-Age=600; Expires=Wed, 17 Jan 2024 17:51:42 GMT; Secure; HttpOnly; SameSite=STRICT < Set-Cookie: remember_me=MjMwOTU2OGEt2VmNy00MmQ2LWFmZmUtYTYxZTNmNTQ5YzY3OnNEeTgyeFA5UVV6JTJGR2NMVmZzNkpYUSUzRCUzRA; Path=/; Max-Age=604800; Expires=Wed, 24 Jan 2024 17:41:42 GMT; Secure; HttpOnly; SameSite=STRICT < Cache-Control: no-cache, no-store, max-age=0, must-revalidate < Pragma: no-cache < Expires: 0 < X-Content-Type-Options: nosniff < X-Frame-Options: DENY < X-XSS-Protection: 0 < Referrer-Policy: no-referrer < content-length: 0
The
jwt_token
is a JWT token that can be used for subsequent requests and has an expiry time of 10 minutes from the issued time. Theremember_me
cookie is a base64 encoded string that contains a unique series id and a token id. The expiry date is set to 7 days from the current date. Theremember_me
cookie is stored in the database. -
Run the
curl
command to retrieve the user details by providing thejwt_token
as an Authorization Bearer headercurl -v -H 'Authorization:Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWiOiJhbXVzaHRhcUB2bXdhcmUuY29tIiwicm9sZXMiOiJST0xFX1VTRVIiLCJ0b2tlbiI6IjczNTJlNTE0N2QwNWU0NDQ3OWJkZDZlNTM0NWU4MmYwIiwiaWF0IjoxNzA1NTcxMjQxLCJleHAiOjE3MDU1NzQ4NDF9.eGnUfTNOAzEXLAskj5amWKKv4PaZEvZc70Od_6Bb0Go' http://localhost:8080/auth/user
The user details are returned as JSON response:
{"id":30720533,"login":"ethan","name":"Ethan Hunt"}
-
Run the
curl
command to retrieve the user details by providing only theremember_me
cookiecurl -v --cookie 'remember_me=Mzk3YzBjNGYtN2M2NS00OTUxLTkyMjUtN2UyNjRmZWNlMjU2Ok1UbUxtdm9LdnZIOUNpYjJPZEVJamclM0QlM0Q' http://localhost:8080/auth/user
The below is the output of the above command.
* Trying [::1]:8080... * Connected to localhost (::1) port 8080 > GET /auth/user HTTP/1.1 > Host: localhost:8080 > User-Agent: curl/8.4.0 > Accept: */* > Cookie: remember_me=NzkwNWZlMWMtMTBmYy00NDRjLWFkMTUtN2UzNGFlNzhkZTkzOnhZWmNxd0ZZYUFDYiUyRnNvMWJyc1VCUSUzRCUzRA > < HTTP/1.1 200 OK < Content-Type: application/json < Content-Length: 56 < Cache-Control: no-cache, no-store, max-age=0, must-revalidate < Pragma: no-cache < Expires: 0 < X-Content-Type-Options: nosniff < X-Frame-Options: DENY < X-XSS-Protection: 0 < Referrer-Policy: no-referrer < Set-Cookie: remember_me=NzkwNWZlMWMtMTBmYy00NDRjLWFkMTUtN2UzNGFlNzhkZTkzOkhKNzNnNiUyRm50TjNXVDFPQXVtNU8xQSUzRCUzRA; Path=/; Max-Age=604800; Expires=Fri, 26 Jan 2024 16:08:31 GMT; Secure; HttpOnly; SameSite=STRICT < Set-Cookie: jwt_token=eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhaG1lZG1xIiwicm9sZXMiOiJST0xFX1VTRVIiLCJ0b2tlbiI6ImdocF9ocnNoT09Td2lOODZYdTdjQ0djRGxUNjg4WTEwTXYwZUpGSDIiLCJpYXQiOjE3MDU2ODA1MTEsImV4cCI6MTcwNTY4NDExMX0.rm4Au5rdfnNhPgnntKFyd50wXMYbIPxNoVJXbs2P3J0; Path=/; Max-Age=600; Expires=Fri, 19 Jan 2024 16:18:31 GMT; Secure; HttpOnly; SameSite=STRICT < * Connection #0 to host localhost left intact {"id":30720533,"login":"ethan","name":"Ethan Hunt"}
The
remember_me
cookie is used to authenticate the user even if the JWT cookie is not expired. Notice a newjwt_token
is sent back as cookie which the client can use for subsequent requests.
- The main class for the reactive persistent remember-me authentication is PersistentRememberMeService. The implementation of this class closely resembles the servlet implementation of persistent remember-me authentication in PersistentTokenBasedRememberMeServices.java
- The GitHub Personal Access token is currently stored in the JWT. This is not a good practice and is only implemented here for demonstration purposes.