JWT Cookie authorisation
326
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:
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.
/auth/sign-up.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.
/auth/sign-out endpoint clears the cookies and removes the refresh token from the database to ensure full logout.HTTP-only, secure, signed, sameSite:'lax' cookies./api, providing a clear understanding of available endpoints and parameters.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.The server receives the user's request and verifies their credentials against the database. If everything checks out, we proceed with the next step.
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.
The server generates the long-lived refresh token (UUID), which is stored in the database.
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.
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.
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.
/auth/sign-outendpoint clears the cookies and removes the refresh token from the database to ensure full logout.
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.
| 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.
truetruelaxtrue15 minutes for access token and 7 days for refresh token (should be set in milliseconds)/POST /auth/sign-up
{
"login": "Pier228
"password": "password123"
}
{
"message": "User successfully registered"
}
Set-Cookie: access_token=s%...; Path=/; HttpOnly; Secure; Expires=...GMT
Set-Cookie: refresh_token=s%...; Path=/; HttpOnly; Secure; Expires=...GMT
This level is focused on fixing the critical security issues found in basic token-based authentication. Let’s break down the key improvements:
Level 0 typically stores JWTs in localStorage, which is vulnerable to XSS attacks. In Level 1:
SameSite=Lax (or Strict) and Secure=true cookies, we reduce the surface area for Cross-Site Request Forgery (CSRF) attacks.SameSite alone provides sufficient protection.Together, these improvements make Level 1 significantly more secure and production-ready compared to the raw, stateless approach in Level 0.
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.
🧠 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.
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:
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.
$ git clone https://github.com/Pier228/ultimate-auth-guide.git
$ cd jwt-based-auth
$ cd level-1-cookie-auth
$ npm install
The Docker image for this project is available on Docker Hub.
To run this application, you need to configure several 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.
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
This project is licensed under the MIT License.
Content type
Image
Digest
sha256:a7af383fa…
Size
274.4 MB
Last updated
over 1 year ago
docker pull pier276/level-1-cookie-auth