Logo

Matviy Ivanov

< Projects View on GitHub

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

  1. Ensure you are using the Node.js version specified in the .nvmrc file.
  2. Create a .env file manually or by running cp .env.example .env.
  3. Install dependencies using npm install.
  4. Prepare the database by running npx prisma migrate dev.
  5. Generate Prisma client using npx prisma generate.
  6. Start the application using npm run start:dev or npm 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

  1. Request: The client sends a SignupDto containing an email and a raw password.
  2. Validation: The NestJS ValidationPipe ensures the email is valid and the password meets strength requirements.
  3. Existence Check: The UserService queries the database via Prisma to ensure the email is not already taken.
  4. Hashing: The raw password is hashed using Argon2.
  5. Persistence: A new user record is created in SQLite. The raw password is discarded, and only the Argon2 hash is stored.
  6. Response: The server returns a success message (201 Created).

User Login & Session Creation

  1. Verification: The UserService retrieves the user by email and uses argon2.verify to compare the incoming password against the stored hash.
  2. Token Generation: The LuciaService generates 120 bits of entropy for a sessionId and another 120 bits for a secret.
  3. Storage: The sessionId and a SHA-256 hash of the secret are stored in the Session table.
  4. 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:

  1. Extraction: The guard reads the session token from the cookie.
  2. Splitting: The token is split into the sessionId and the secret.
  3. Lookup: The database is queried for a session matching the sessionId.
  4. Integrity Check: The provided secret is hashed and compared to the stored secretHash using a constant-time equality helper to prevent timing attacks.
  5. Timeout Check: The system verifies if the time elapsed since lastVerifiedAt exceeds the inactivity timeout.
  6. Identity Attachment: The User record is fetched and attached to the request.user object, allowing easy access in controllers via the @CurrentUser() decorator.

Sliding Window Refresh

To keep active users logged in without manual re-authentication:

  1. Interval Check: If a valid request occurs and a specified amount of time has passed since the last database update, the lastVerifiedAt timestamp is updated.
  2. Cookie Sync: The AuthGuard detects the update and re-sends the session cookie with a fresh maxAge to the browser.

Logout & Invalidation


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: