JWT-based authentication with email verification
360
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.
This level implements email verification during user registration, adding an extra security layer and improving account integrity.
After successful registration:
email, password, and emailConfirmed = false.name and surname.An email containing the confirmation code is sent to the user’s email address.
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.
/auth/sign-up.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.
/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.users table:
email, password (hashed), emailConfirmed (boolean).profiles table:
name, surname, and references the associated user.confirmations table:
tokens table:
tokens and the expiration time linked to a user.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.The server receives the user's request and verifies their credentials against the database. If everything checks out, we proceed with the next step.
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.
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.
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 day, at 4:00 am) deletes all expired refresh tokens from the database.
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.
| 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.
POST /auth/sign-in
{
"email": "[email protected]",
"password": "examplePassword123"
}
{
"message": "User successfully logged in"
}
Set-Cookie: access_token=s%...; Path=/; HttpOnly; Secure; Expires=...GMT
Set-Cookie: refresh_token=s%...; Path=/; HttpOnly; Secure; Expires=...GMT
truetruelaxtrue15 minutes for access token and 7 days for refresh token (should be set in milliseconds)/15 minutes to verify the email address. Provided in milliseconds6922624 - email confirmation template ID - must contain token variableVerification time/3 - 5 minutes. Provided in milliseconds. After this time, the user can request a new email confirmation letterSupport TeamThe email address from which emails will be sent. Must be verified on Mailjet website. Stores in environment variables(.env)The transition from Level 1 (cookie-based authentication) to Level 2 (email verification) introduces several critical security improvements:
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.
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.
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.
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.
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.
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.
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.
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.
🧠 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.
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.
Option 1 - Direct repository access:
$ git clone https://github.com/Pier228/level-2-email-verification.git
$ cd level-2-email-verification
$ npm install
Option 2 - From ultimate auth guide repository:
$ git clone https://github.com/Pier228/ultimate-auth-guide.git
$ cd jwt-based-auth/level-2-email-verification
$ 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.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.
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 - see the LICENSE file for details.
Content type
Image
Digest
sha256:7fc0eb7b3…
Size
276.7 MB
Last updated
over 1 year ago
docker pull pier276/level-2-email-verification