Skip to main content

Authentication Guide

The two‑token JWT architecture – secure, stateless, and frontend‑friendly.

JWT Cookies Redis


📖 Table of Contents


Token Overview

Reshma‑Core uses a two‑token JWT architecture that is both stateless and secure against XSS and CSRF attacks.

TokenStorageLifetimePurposeWhere to keep
Access TokenReact memory (Zustand/Context)15 minutes (JWT_ACCESS_EXPIRES_IN)Authorise API requests via Authorization: Bearer headerIn‑memory (never localStorage)
Refresh TokenHttpOnly cookie7 days (JWT_REFRESH_EXPIRES_IN)Obtain new access tokens without re‑loginAutomatically managed by browser

Why this design?

  • Access token short lifespan limits damage if leaked.
  • Refresh token in HttpOnly cookie is invisible to JavaScript → XSS cannot steal it.
  • SameSite=Strict prevents CSRF on the refresh endpoint.
  • Redis blacklist ensures logout cannot be replayed.

Authentication Flow Diagram

Step‑by‑Step Guide

1. Registration

  • Endpoint: POST /auth/register
  • Rate limit: 10 requests per 15 minutes per IP

Request body:

{
"firstname": "Test",
"lastname": "User",
"email": "test@example.com",
"password": "SecurePassword123!"
}

Validation rules (Zod):

  • firstname: string, 2‑50 characters
  • lastname: string, 2‑50 characters
  • email: valid email format
  • password: min 8 chars, at least one uppercase, one number, one special character

Response (201 Created):

{
"success": true,
"statusCode": 201,
"message": "Registration successful. OTP sent to your email.",
"data": {
"user": {
"_id": "...",
"email": "test@example.com",
"firstname": "Test",
"lastname": "User",
"role": "USER",
"isEmailVerified": false
}
},
"timestamp": "2026-05-22T10:30:00.000Z"
}

Note: The user is created with isEmailVerified: false. They cannot log in until OTP is verified.


2. OTP Verification (First Login)

  • Endpoint: POST /auth/verify-otp
  • Rate limit: 10 requests per 15 minutes per IP

Request body:

{
"email": "test@example.com",
"otp": "123456"
}

Response (200 OK):

{
"success": true,
"statusCode": 200,
"message": "Email verified successfully",
"data": {
"user": {
"_id": "...",
"email": "test@example.com",
"firstname": "Test",
"lastname": "User",
"role": "USER",
"isEmailVerified": true
},
"accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
},
"timestamp": "2026-05-22T10:32:00.000Z"
}

Cookie automatically set (invisible to JavaScript):

refreshToken: <jwt>; HttpOnly; Secure; SameSite=Strict; Max-Age=604800

Important: The refresh token cookie is only set during OTP verification and login. It is not returned in JSON.


3. Standard Login

  • Endpoint: POST /auth/login
  • Rate limit: 10 requests per 15 minutes per IP

Request body:

{
"email": "test@example.com",
"password": "SecurePassword123!"
}

Response: Exactly the same as /verify-otp – returns accessToken and sets the refreshToken cookie.


4. Using the Access Token

For every protected API request, include the access token in the Authorization header:

Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

Example (JavaScript fetch):

fetch('http://localhost:5000/api/v1/users/profile', {
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json'
}
});

Never store the access token in localStorage or sessionStorage – they are vulnerable to XSS. Keep it in memory (React state, Zustand, Redux).


5. Refreshing an Expired Token

When an API request returns 401 Unauthorized (and you know the token was valid before), call the refresh endpoint.

  • Endpoint: POST /auth/refresh
  • No body, no auth header – the browser automatically attaches the refreshToken cookie.

Response (200 OK):

{
"success": true,
"statusCode": 200,
"message": "Access token refreshed",
"data": {
"accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
},
"timestamp": "2026-05-22T10:45:00.000Z"
}

Error (401 Unauthorized):

  • If the refresh token cookie is missing, expired, or blacklisted → user must login again.

Implementation pattern (axios interceptor): see Frontend Integration Example.


6. Logout

  • Endpoint: POST /auth/refresh
  • Headers: Authorization: Bearer <valid_access_token>

Response (200 OK):

{
"success": true,
"statusCode": 200,
"message": "Logged out successfully",
"data": null,
"timestamp": "2026-05-22T10:50:00.000Z"
}

What happens server‑side:

  • The refresh token cookie is immediately expired (overwritten).
  • The refresh token is added to a Redis blacklist – cannot be reused even if stolen.
  • The access token is not blacklisted (short lifespan anyway).

After logout: The frontend should clear the in‑memory access token and redirect to login.


Password Reset Flow

Users who forget their password can reset it without being logged in.

Step 1: Request Reset Token

Endpoint: POST /auth/forgot-password

{
"email": "test@example.com"
}

Response (200 OK): A reset token is sent to the email (the API does not return it).

Step 2: Reset Password

Endpoint: POST /auth/reset-password

{
"email": "test@example.com",
"token": "reset-token-from-email",
"newPassword": "NewSecurePassword456!"
}

Response (200 OK): Password updated. User can now login with the new password.

Both endpoints are rate‑limited (10 requests per 15 minutes per IP).


Google OAuth Login

Endpoint: POST /auth/google

This endpoint accepts a Google idToken (obtained from the frontend Google Sign-In library) and either logs in an existing user or creates a new one.

Request body:

{
"idToken": "google-oauth-id-token-string"
}

Response: Same as /login – returns accessToken and sets refreshToken cookie.

Google OAuth users do not have a password. They cannot use the password reset or change password endpoints.


Error Responses

HTTP Codemessage exampleWhen it happensFrontend action
400Validation Failed: email: Invalid email formatZod validation failsDisplay the message; fix input
401Invalid or expired token. Please login again.Access token missing/malformed/expired (refresh also failed)Redirect to login
403Please verify your email address before logging in.User tries to login without OTP verificationShow verification UI
403This account has been deactivated.Admin banned the user (isActive: false)Show support contact
409Email already registered. Please login instead.Duplicate email on registrationSuggest login or password reset
429Too many requests, please try again later.Rate limit exceededWait 15 minutes or implement backoff

Frontend Integration Example (Axios)

Here is a complete interceptor that handles token refresh automatically.

import axios from 'axios';

const api = axios.create({
baseURL: process.env.REACT_APP_API_URL,
});

// Store access token in memory (Zustand / React context)
let accessToken = null;

export const setAccessToken = (token) => {
accessToken = token;
};

// Request interceptor: add Authorization header
api.interceptors.request.use((config) => {
if (accessToken) {
config.headers.Authorization = `Bearer ${accessToken}`;
}
return config;
});

// Response interceptor: handle 401 and refresh
api.interceptors.response.use(
(response) => response,
async (error) => {
const originalRequest = error.config;

if (error.response?.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;

try {
const { data } = await axios.post(`${api.defaults.baseURL}/auth/refresh`);
setAccessToken(data.data.accessToken);
originalRequest.headers.Authorization = `Bearer ${data.data.accessToken}`;
return api(originalRequest);
} catch (refreshError) {
// Refresh failed – logout user
setAccessToken(null);
window.location.href = '/login';
return Promise.reject(refreshError);
}
}

return Promise.reject(error);
}
);

export default api;

Usage in React component:

import api, { setAccessToken } from './api';

// After login / OTP verification
setAccessToken(response.data.accessToken);

// Now all api calls will include the token automatically
const { data } = await api.get('/users/profile');

Security Best Practices (Frontend)

DoDon't
Store access token in memory (React state, Zustand, Redux)❌ Store access token in localStorage or sessionStorage
Use axios interceptors to handle refresh automatically❌ Manually refresh on every 401 without retry logic
Clear access token on logout and redirect to login❌ Keep token after logout
Use HTTPS in production (cookies require Secure flag)❌ Test without HTTPS – cookies may be rejected


Stateless, secure, and production‑proven – the Reshma‑Core authentication system.