Skip to content

Pier228/level-1-cookie-auth

Repository files navigation

Zero Level Logo

Level 1 — JWT Authentication with Cookies (Access & Refresh Tokens)

This level introduces a more secure and user-friendly approach to JWT authentication by leveraging HTTP-only cookies to store both the access and refresh tokens. Unlike the basic token-based approach from Level 0 (where tokens are returned in JSON and stored client-side, typically in localStorage), this method mitigates several security vulnerabilities and improves developer ergonomics for session handling in modern applications.

In this level, we separate concerns between short-lived and long-lived tokens:

  • The access token is a short-lived JWT that authorizes requests to protected routes.
  • The refresh token is a long-lived uuid stored in a secure database, used solely to issue new access tokens once the original has expired.

Both tokens are stored in HTTP-only cookies, which makes them inaccessible to JavaScript (protecting against XSS attacks), and — when combined with SameSite=Lax and Secure=true — helps defend against CSRF.

This approach mimics a session-like experience for the client (especially for SPAs or SSR apps), while maintaining the stateless nature of JWTs on the server.

Table of Contents

Features

Authentication

  • Users authenticate by providing their credentials(login and password) via /auth/sign-up.

Authorization

  • Secure Routes: Access and refresh (Only for automatic access token renewal) tokens required inside the cookie for protected routes, ensuring that only authenticated users can access sensitive data or perform critical actions.

  • Secure Token Validation Middleware: Every request to a protected route checks the access_token cookie. If the access token is expired, the server automatically generates a new one using the refresh token. During this process, the refresh token is rigorously validated against the database — if valid, it’s reissued, but if expired or invalid, it’s immediately revoked (deleted from the database) and both the access_token and refresh_token cookies are cleared from the client. This approach guarantees secure, short-lived access tokens while enforcing strict refresh token validation and automatic cleanup of compromised or stale sessions.

Logout Route

  • /auth/sign-out endpoint clears the cookies and removes the refresh token from the database to ensure full logout.

Expired refresh tokens cleanup

  • Scheduled Task: CRON Job: We use a CRON job to schedule a task that periodically deletes all expired refresh tokens from the database. This ensures that only valid refresh tokens are stored in the database. The CRON job is set to run every 3 days, at 3:00 am. This frequency provides an optimal balance between ensuring security and minimizing performance impact on our database.

Database Management

  • Prisma ORM: Utilize Prisma's type-safe PostgreSQL interactions to simplify database queries and schema management.
  • Database Schema: Leverage a pre-defined database schema for efficient data storage and retrieval.

Data Validation and Integrity

  • DTO-based Request/Response Schemas: Ensure data consistency and security with request/response schemas based on Data Transfer Objects (DTOs).
  • Validation Middleware: Built-in validation middleware to catch and handle invalid input data.

Security Measures

  • Built-in Middleware: Deployed with CORS, Helmet, and rate limiting for comprehensive security protection.
  • Cross-Site Scripting (XSS) Protection: Helmet middleware ensures that user input is properly sanitized to prevent XSS attacks.
  • Rate Limiting: Mitigate brute-force attacks by enforcing reasonable request limits.
  • Tokens Security: Access & refresh tokens stored in HTTP-only, secure, signed, sameSite:'lax' cookies.

Testing Framework

  • End-to-End Test Suite: Utilize Supertest for comprehensive testing of API endpoints, ensuring seamless integration with the application.
  • Test Coverage: Robust test coverage ensures that critical functionality is thoroughly validated and reliable.

Documentation and Discovery

  • Interactive Swagger UI: Access an interactive and user-friendly documentation interface at /api, providing a clear understanding of available endpoints and parameters.

Deployment and Containerization

  • Docker Support: Leverage Docker containers for efficient deployment, isolation, and scalability.

Tech Stack

How It Works

1. User Sign-Up or Sign-In

The journey begins when a user attempts to register or log in through the application. They send their credentials (login and password) to one of two routes:

  • /auth/sign-up: For new users who want to create an account.
  • /auth/sign-in: For existing users who need to authenticate.

2. Server Verification

The server receives the user's request and verifies their credentials against the database. If everything checks out, we proceed with the next step.

3. Access Token Generation

Upon successful verification, the server generates a short-lived access token (JWT) for the user. This token contains essential information about the user, such as their ID.

4. Refresh Token Generation

The server generates the long-lived refresh token (UUID), which is stored in the database.

5. Client-Side Token Storage

The client-side application receives access and refresh tokens inside secured HTTP-only cookies from the server. These tokens will be automatically sent to the server with each request.

6. Protected Route Access

When users attempt to access a protected route, their client-side application automatically sends the stored access and refresh tokens inside cookies with each request.

7. Backend Verification and Authorization

The backend server receives the request and verifies the validity of the access token. If the access token is valid, the request will successfully pass the security middleware and get to the controller. If the access token has expired or is undefined, the refresh token will generate a new access token. During this process, the refresh token is rigorously validated against the database. If valid, it’s reissued, but if expired or invalid, it’s immediately revoked (deleted from the database) and both the access_token and refresh_token cookies are cleared from the client.

8. Sing out

/auth/sign-outendpoint clears the cookies and removes the refresh token from the database to ensure full logout.

9. Expired refresh tokens cleanup

The server will automatically start the CRON job to schedule a task that periodically(every 3 days, at 3:00 am) deletes all expired refresh tokens from the database.

Endpoints

Method Endpoint Description Required Body
POST /auth/sign-up Register a new user and set auth tokens login: string, password: string
POST /auth/sign-in Login and set access & refresh token login: string, password: string
POST /auth/sign-out Clear auth tokens and invalidate session None

For a detailed overview of the available API endpoints, request/response structures, and data models, the Swagger documentation is available at /api. This documentation provides interactive API exploration and helps developers understand and integrate with the API efficiently.

Cookie Configuration

  • httpOnly: true
  • secure: true
  • sameSite: lax
  • signed: true
  • maxAge: 15 minutes for access token and 7 days for refresh token (should be set in milliseconds)
  • path: /

Example Request

POST /auth/sign-up
{
  "login": "Pier228
  "password": "password123"
}

Response

Body

{
  "message": "User successfully registered"
}

Headers

Set-Cookie: access_token=s%...; Path=/; HttpOnly; Secure; Expires=...GMT
Set-Cookie: refresh_token=s%...; Path=/; HttpOnly; Secure; Expires=...GMT

Security Improvements Over Level 0

This level is focused on fixing the critical security issues found in basic token-based authentication. Let’s break down the key improvements:

1. HTTP-Only Cookies Instead of LocalStorage

Level 0 typically stores JWTs in localStorage, which is vulnerable to XSS attacks. In Level 1:

  • Tokens are stored in HTTP-only cookies, so they cannot be accessed by malicious scripts.
  • This offers strong protection against client-side injection vulnerabilities.

2. CSRF Mitigation

  • By using SameSite=Lax (or Strict) and Secure=true cookies, we reduce the surface area for Cross-Site Request Forgery (CSRF) attacks.
  • When needed, additional CSRF tokens can be added — though in many modern apps, SameSite alone provides sufficient protection.

3. Token Lifecycle Control

  • In Level 0, once a JWT is issued, there's no way to revoke or rotate it.
  • In Level 1, we use a refresh token system, allowing:
    • Short-lived access tokens (better control).
    • The ability to revoke refresh tokens on logout or abnormal behavior.
    • Scheduled cleanup of expired refresh tokens in the database.

4. Reduced Exposure Time

  • Access tokens can now be short-lived (e.g., 5–15 minutes), because the refresh token ensures session continuity.
  • If an access token is compromised, it has a very limited time to be misused.

5. Controlled Logout

  • Users can explicitly logout, which triggers:
    • Deletion of the refresh token from the DB.
    • Clearing of cookies on the client.
  • This ensures that sessions are fully terminated and cannot be silently reused.

Together, these improvements make Level 1 significantly more secure and production-ready compared to the raw, stateless approach in Level 0.

Security Notes

✅ Recommended for Production Use – With Conditions

This level represents a significant improvement over basic JWT implementations and is generally suitable for production environments, provided that the best practices are respected.

Level 1 introduces a secure, session-like authentication flow using HTTP-only cookies and refresh tokens, which balances security, scalability, and developer convenience.

However, its production-readiness depends on how it is configured and deployed.

Use Cases Where This Level Is a Good Fit

  • Single Page Applications (SPAs) needing a secure login flow
  • Public-facing applications that don’t require high-sensitivity data protection
  • Systems needing session-like UX without traditional server-side sessions

Use with Caution If:

  • You handle extremely sensitive data (e.g., healthcare, banking)
  • Your application faces high risk of targeted attacks
  • You require fine-grained access control or audit logging

🧠 TL;DR:
Yes — this level can be used in production, as long as you correctly configure HTTPS, cookies, rate limiting, and token storage. However, for higher-security apps, consider combining this with additional layers like email verification, refresh token rotation, and user anomaly detection in later levels.


Testing

To ensure the application is working correctly, comprehensive testing has been implemented to cover all aspects of the JWT authentication flow. The application utilizes Supertest for E2E testing, which allows making HTTP requests directly from test code and verifying expected responses. Here's a snapshot of the test results:

Test results

As you can see, all tests passed successfully. This gives confidence in the correctness of the JWT authentication implementation and ensures it works as expected in different scenarios. You can also run this test cases using npm run test:e2e command.

Installation

GitHub

$ git clone https://github.com/Pier228/level-1-cookie-auth.git
$ cd level-1-cookie-auth
$ npm install

Docker Image

The Docker image for this project is available on Docker Hub.

Docker Hub

Environment Variables

To run this application, you need to configure several environment variables.

  1. Create a .env file in the root directory of the project.
  2. Add required environment variables:
  • PORT: The port on which the server will run. This field is optional. By default will run on 3000 port.
  • DATABASE_URL: MongoDB connection URL used to connect to the database.
  • SALT_ROUNDS: Number of rounds for hashing passwords (bcrypt).
  • JWT_SECRET: Secret key for signing and verifying JWT tokens.
  • CORS_ALLOWED_ORIGIN: The URL of the domain from which it is allowed to send requests to the server (CORS settings).
  • COOKIE_SECRET: Secret key for signing and verifying cookies.

You can also refer to the .env.example file for a complete list of required environment variables.

Running the app

After setting up the .env file, you can start the application using the following commands:

# Generate prisma client
$ npx prisma generate

# Build the application
$ npm run build

# Start in development mode
$ npm run start

# Start in watch mode
$ npm run start:dev

# Start in production mode
$ npm run start:prod

License

This project is licensed under the MIT License - see the LICENSE file for details.