lucia-nest
A user authentication flow implementation using NestJS, Prisma, and better-sqlite3. This project follows the manual implementation philosophy of Lucia Auth and The Copenhagen Book.
How to Run Locally
- Ensure you are using the Node.js version specified in the
.nvmrcfile. - Create a
.envfile manually or by runningcp .env.example .env. - Install dependencies using
npm install. - Prepare the database by running
npx prisma migrate dev. - Generate Prisma client using
npx prisma generate. - Start the application using
npm run start:devornpm run start.
You can view the database using npx prisma studio.
Test the API using your favorite tool. If you use Postman, there is a collection file in the repository named lucia-nest.postman_collection.json.
Flow Overview
User Signup
- Request: The client sends a
SignupDtocontaining an email and a raw password. - Validation: The NestJS
ValidationPipeensures the email is valid and the password meets strength requirements. - Existence Check: The
UserServicequeries the database via Prisma to ensure the email is not already taken. - Hashing: The raw password is hashed using Argon2.
- Persistence: A new user record is created in SQLite. The raw password is discarded, and only the Argon2 hash is stored.
- Response: The server returns a success message (201 Created).
User Login & Session Creation
- Verification: The
UserServiceretrieves the user by email and usesargon2.verifyto compare the incoming password against the stored hash. - Token Generation: The
LuciaServicegenerates 120 bits of entropy for asessionIdand another 120 bits for asecret. - Storage: The
sessionIdand a SHA-256 hash of thesecretare stored in theSessiontable. - Token Delivery: The server constructs a token (
{sessionId}.{secret}) and sends it to the client via a Set-Cookie header.
Request Authentication (The Guard)
Every request to a protected route passes through the AuthGuard:
- Extraction: The guard reads the session token from the cookie.
- Splitting: The token is split into the
sessionIdand thesecret. - Lookup: The database is queried for a session matching the
sessionId. - Integrity Check: The provided
secretis hashed and compared to the storedsecretHashusing a constant-time equality helper to prevent timing attacks. - Timeout Check: The system verifies if the time elapsed since
lastVerifiedAtexceeds the inactivity timeout. - Identity Attachment: The
Userrecord is fetched and attached to therequest.userobject, allowing easy access in controllers via the@CurrentUser()decorator.
Sliding Window Refresh
To keep active users logged in without manual re-authentication:
- Interval Check: If a valid request occurs and a specified amount of time has passed since the last database update, the
lastVerifiedAttimestamp is updated. - Cookie Sync: The
AuthGuarddetects the update and re-sends the session cookie with a freshmaxAgeto the browser.
Logout & Invalidation
- Single Logout: The specific
sessionIdis deleted from the database, and the browser cookie is cleared. - Logout All: All session records associated with the
userIdare deleted, effectively force-logging the user out of all devices (e.g., in the event of a password change or security breach).
Extensibility & Optimization
This flow is designed to be flexible and extensible. One strategy to further optimize performance is to use a short-lived JWT token (5-minute maximum) containing the session object alongside the session token.
In this hybrid approach:
- The server verifies the JWT locally for every request.
- The database session check is performed only when the JWT is invalid or expired.
- This significantly reduces database load while maintaining the ability to revoke sessions instantly via the database-backed session token.