Skip to content

JWT-based authentication with cookies and email verification

License

Notifications You must be signed in to change notification settings

Pier228/level-2-email-verification

Repository files navigation

Second Level Logo

Level 2 – Email Verification

This level introduces a crucial upgrade to the authentication system: mandatory email verification. When a user signs up, they are required to confirm their email address before they can log into the application. This mechanism helps ensure that users provide valid and accessible email addresses, and it protects the system from fake account registrations.

During the registration process, users submit their email, password, name, and surname. While basic user information is immediately stored in the database, users remain inactive until they confirm their email through a 6-digit code sent to them via email.

The authentication flow is now strictly controlled β€” users cannot log in until their email address is verified. Additionally, an automatic cleanup system ensures that unverified accounts are removed after a specific time period, keeping the database clean and secure.

This level marks a significant step toward building a production-ready, secure authentication system.

Table of Contents

Description

This level implements email verification during user registration, adding an extra security layer and improving account integrity.

After successful registration:

  1. A user record is created with email, password, and emailConfirmed = false.
  2. A profile record is created with name and surname.
  3. A confirmation record is created containing a hashed 6-digit code.

An email containing the confirmation code is sent to the user’s email address.

New Features

  • Email Confirmation During Registration:
    Users must verify their email before they can log in.

  • Secure Code Generation and Storage:
    A random 6-digit code is generated for each user. The code is hashed before being stored in the database to protect against database leaks.

  • Resend Email Confirmation:
    Users can request to resend a new confirmation code if they have lost or did not receive the original email.

  • CRON Job for Cleanup:
    A scheduled task automatically deletes unconfirmed accounts (including the related profiles and confirmations) if the email is not verified within 2 days after registration.

  • Safe Relationships:
    Proper database relations ensure that when a user is deleted, their profile and confirmation entries are also automatically removed.

Features from previous levels

Authentication

  • Users authenticate by providing their credentials(email, password, name and surname) via /auth/sign-up.

Authorization

  • Authorization Route: Users sign in into application by providing their credentials(email, password) via /auth/sign-in.

  • 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 day, at 4: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

Database Structure

  • users table:
    • Stores email, password (hashed), emailConfirmed (boolean).
  • profiles table:
    • Stores name, surname, and references the associated user.
  • confirmations table:
    • Stores the hashed verification code and the type of the confirmation linked to a user.
  • tokens table:
    • Stores refresh tokens and the expiration time linked to a user.

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: email, password, and name for the sign-up route and email, password for the sign-in route:

  • /auth/sign-up: For new users who want to create an account.
  • /auth/sign-in: For existing users(with confirmed email address) 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. Records creations

Authentication route: After validating the user's data, the server will create three records in different tables: create record in the users table with email, password and confirmedEmail: false data, in the profiles table with name, surname, and reference(one-to-one) to a user table, and in the confirmations table with hashed token, type: "EMAIL_VERIFICATION", reference(one-to-many) to the user, and expires time. Then send an email letter with a 6-digit code to confirm the user's email address.

Authorization route: Find the user with the provided email address in the database, check if the user's email is confirmed, compare the password via bcrypt, and go to the access & refresh tokens generation steps if everything is correct.

4. Email confirmation (Only for the authentication route)

The user gets the verification code from the letter and sends it to the confirm-email route; the server finds the confirmation record with the confirmation ID, compares the codes via bcrypt, and if everything is correct, deletes the confirmation record and marks the user's emailConfirmed field true. After this, the user could successfully sign in to the application.

5. 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.

6. Refresh Token Generation

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

7. 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.

8. 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.

9. 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.

10. Sing out

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

11. Expired refresh tokens cleanup

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

12. Users with unconfirmed email addresses cleanup

The server will automatically start the CRON job to schedule a task that periodically(every day, at 3:00 am) deletes all users with unconfirmed email addresses, which have been created 2 days earlier from the database. The delete setting in the database models is set up as Cascade, so when the user is deleted, all references to that user will be deleted too.

Endpoints

Method Endpoint Description Required Body Response
POST /auth/sign-up Register a new user & send email confirmation email, password, name, surname: string message, confirmationId: string
POST /auth/sign-in Login and set access & refresh token email: string, password: string message: string, set auth cookies
POST /auth/sign-out Clear auth tokens and invalidate session None message: string, clear auth cookies
POST /auth/confirm-email Confirm user's email address confirmationId: string, token: string message: string, set auth cookies
POST /auth/resend-confirmation Resend email confirmation letter confirmationId: string message, confirmationId: string

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.

Example Request

POST /auth/sign-in
{
  "email": "example@gmail.com",
  "password": "examplePassword123"
}

Response

Body

{
  "message": "User successfully logged in"
}

Headers

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

Configurations

Cookie

  • 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: /

Email Confirmation

  • Verification time: 15 minutes to verify the email address. Provided in milliseconds
  • Template ID: 6922624 - email confirmation template ID - must contain token variable
  • Resend threshold: Verification time/3 - 5 minutes. Provided in milliseconds. After this time, the user can request a new email confirmation letter

Letters

  • senderName: Support Team
  • senderEmail: The email address from which emails will be sent. Must be verified on Mailjet website. Stores in environment variables(.env)

Security Improvements Over Level 1

The transition from Level 1 (cookie-based authentication) to Level 2 (email verification) introduces several critical security improvements:

1. Validation of Email Ownership

Users are now required to prove ownership of the provided email address before they can interact with the system. This ensures that accounts are tied to real, accessible email addresses and reduces the risk of fake or malicious account creation.

2. Prevention of Unauthorized Access

Until a user completes the email verification step, they cannot log in or access protected resources, even if they possess valid credentials (email and password). This measure prevents unauthorized or malicious use of unconfirmed accounts.

3. Hashed Confirmation Codes

The 6-digit confirmation codes are never stored in plaintext. Instead, they are hashed using a secure hashing algorithm before being saved to the database, similar to how passwords are handled. Even if the database is compromised, attackers cannot easily retrieve or reverse-engineer the original codes.

4. Strict Account Activation Flow

The system enforces a strict policy: users must verify their email within a limited timeframe, or their account will be automatically deleted. This helps maintain a clean database and prevents it from being flooded with inactive or incomplete registrations.

5. Automated Cleanup of Unverified Users (CRON Job)

A scheduled task periodically checks and deletes accounts that failed to verify their email address within the specified timeframe (e.g., 2 days). This minimizes the long-term storage of unverified user data and reduces security risks related to stale accounts.

6. Separated Data Structures (Users, Profiles, Confirmations)

By splitting user credentials, profile information, and confirmation codes into different tables, we minimize data exposure and make it easier to manage the deletion and lifecycle of user-related data securely.

7. Improved User Accountability and Trust

Since each active account must have a verified email, the system fosters a more trustworthy environment where users can have greater confidence in the identity of other users.

Security Notes

βœ… Recommended for Production Use – With Conditions

This level introduces a substantial improvement over basic authentication systems and is generally ready for production environments, assuming proper best practices are enforced.

Level 2 combines secure cookie-based JWT authentication with mandatory email verification, confirmation token hashing, and automatic cleanup of unverified accounts, creating a much stronger and more trustworthy user authentication flow.

However, as with any security-sensitive feature, production-readiness depends heavily on deployment configuration and operational vigilance.

Use Cases Where This Level Is a Good Fit

  • Public web applications that require verified user identities
  • SaaS products where a confirmed email is critical for business logic
  • Systems that want to minimize fake accounts and improve database hygiene
  • Applications aiming for a balance between strong security and user convenience

Use with Caution If:

  • You handle extremely sensitive data (e.g., healthcare, banking)
  • Your application faces high risk of targeted attacks
  • You require multi-factor authentication (MFA) or advanced user anomaly detection

🧠 TL;DR:
Yes β€” this level can be confidently used in production, provided that HTTPS, cookie security settings, email service hardening, and periodic system audits are properly maintained. For critical systems, consider expanding later with MFA, refresh token rotation, and advanced behavioral security features.


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

Option 1 - Direct repository access:

$ git clone https://github.com/Pier228/level-2-email-verification.git
$ cd level-2-email-verification
$ 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.
  • MAILJET_API_KEY: Public API key from Mailjet.
  • MAILJET_SECRET_KEY: Private API secret key from Mailjet.
  • MAILJET_SENDER_EMAIL: Default sender email address (must be verified in Mailjet).

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.