Skip to main content
The Crypto Shop Backend implements a secure, stateless authentication system using JSON Web Tokens (JWT) stored in HttpOnly cookies.

Authentication Flow

Token Generation

Two types of tokens are issued upon successful authentication:

Access Token

  • Expiry: 15 minutes
  • Purpose: Authorizes API requests
  • Payload: User ID and role

Refresh Token

  • Expiry: 7 days
  • Purpose: Issues new access tokens
  • Payload: User ID and role
// src/utils/tokenUtils.js
import jwt from 'jsonwebtoken';

const ACCESS_TOKEN_EXPIRY = '15m';
const REFRESH_TOKEN_EXPIRY = '7d';

export const generateTokens = (userId, role) => {
  const accessToken = jwt.sign(
    { id: userId, role: role },
    process.env.ACCESS_TOKEN_SECRET,
    { expiresIn: ACCESS_TOKEN_EXPIRY }
  );

  const refreshToken = jwt.sign(
    { id: userId, role: role },
    process.env.REFRESH_TOKEN_SECRET,
    { expiresIn: REFRESH_TOKEN_EXPIRY }
  );

  return { accessToken, refreshToken };
};
Reference: src/utils/tokenUtils.js:6-23
The ACCESS_TOKEN_SECRET and REFRESH_TOKEN_SECRET environment variables must be cryptographically secure random strings. Never use the same secret for both tokens.

Login Process

The login endpoint validates credentials and issues tokens:
// src/api/auth/login.js
export const login = async (req, res) => {
  const { email, password } = req.body;

  // 1. Find user with password field (normally excluded)
  const user = await User.findOne({ email }).select('+password');

  // 2. Verify password using bcrypt comparison
  if (!user || !(await user.matchPassword(password))) {
    return res.status(401).json({ error: 'Invalid email or password' });
  }

  // 3. Check if account is active
  if (!user.isActive) {
    return res.status(401).json({ error: 'User account is inactive' });
  }

  // 4. Update last login timestamp
  user.lastLogin = new Date();
  await user.save();

  // 5. Generate tokens
  const { accessToken, refreshToken } = generateTokens(user._id, user.role);

  // 6. Set HttpOnly cookies
  res.cookie('accessToken', accessToken, COOKIE_OPTIONS);
  res.cookie('refreshToken', refreshToken, COOKIE_OPTIONS);

  // 7. Return user data (no password)
  res.json({
    message: 'Login successful',
    user: {
      id: user._id,
      email: user.email,
      username: user.username,
      role: user.role,
      wallet: user.wallet.address
    }
  });
};
Reference: src/api/auth/login.js:11-51 Cookies are configured with strict security settings:
const COOKIE_OPTIONS = {
  httpOnly: true,  // Prevent JavaScript access
  secure: process.env.NODE_ENV === 'production',  // HTTPS only in production
  sameSite: 'strict',  // CSRF protection
  maxAge: 7 * 24 * 60 * 60 * 1000  // 7 days
};
Reference: src/api/auth/login.js:4-9
Why HttpOnly? Storing tokens in HttpOnly cookies prevents XSS attacks from stealing authentication credentials. The browser automatically sends cookies with requests, but JavaScript cannot access them.

Password Hashing

Passwords are hashed using bcrypt before storage:
// src/models/User.js
import bcryptjs from 'bcryptjs';

userSchema.pre('save', async function(next) {
  if (!this.isModified('password')) return next();
  
  const salt = await bcryptjs.genSalt(10);
  this.password = await bcryptjs.hash(this.password, salt);
  next();
});

userSchema.methods.matchPassword = async function(enteredPassword) {
  return await bcryptjs.compare(enteredPassword, this.password);
};
Reference: src/models/User.js:68-82

Token Refresh Flow

When the access token expires, clients can obtain a new one using the refresh token:
// src/api/auth/refreshToken.js
export const refreshToken = async (req, res) => {
  const token = req.cookies.refreshToken;

  if (!token) {
    return res.status(401).json({ error: 'No refresh token' });
  }

  const decoded = verifyRefreshToken(token);
  if (!decoded) {
    return res.status(401).json({ error: 'Invalid refresh token' });
  }

  // Issue new tokens
  const { accessToken, refreshToken: newRefreshToken } = 
    generateTokens(decoded.id, decoded.role);

  // Update cookies
  res.cookie('accessToken', accessToken, COOKIE_OPTIONS);
  res.cookie('refreshToken', newRefreshToken, COOKIE_OPTIONS);

  res.json({ message: 'Token refreshed' });
};
Reference: src/api/auth/refreshToken.js:10-32
The refresh endpoint issues a new refresh token on each call (token rotation), invalidating the previous one. This limits the window of opportunity if a refresh token is compromised.

Authentication Middleware

Protected routes use the auth middleware to verify access tokens:
// src/middlewares/auth.js
import { verifyAccessToken } from '../utils/tokenUtils.js';

export const auth = (req, res, next) => {
  const token = req.cookies.accessToken;

  if (!token) {
    return res.status(401).json({ error: 'No token, authorization denied' });
  }

  const decoded = verifyAccessToken(token);
   
  if (!decoded) {
    return res.status(401).json({ error: 'Token is invalid or expired' });
  }
  
  req.user = decoded;  // Attach user data to request
  next();
};
Reference: src/middlewares/auth.js:3-21

Token Verification

// src/utils/tokenUtils.js
export const verifyAccessToken = (token) => {
  try {
    return jwt.verify(token, process.env.ACCESS_TOKEN_SECRET);
  } catch (error) {
    return null;
  }
};
Reference: src/utils/tokenUtils.js:25-31

Role-Based Access Control

The system supports two roles: user and admin.

User Model Role Definition

// src/models/User.js
role: {
  type: String,
  enum: ['user', 'admin'],
  default: 'user'
}
Reference: src/models/User.js:24-28

Admin-Only Middleware

export const adminOnly = (req, res, next) => {
  if (req.user?.role !== 'admin') {
    return res.status(403).json({ error: 'Admin access only' });
  }
  next();
};
Reference: src/middlewares/auth.js:23-28

User-Only Middleware

export const userOnly = (req, res, next) => {
  if (req.user?.role !== 'user') {
    return res.status(403).json({ error: 'User access only' });
  }
  next();
};
Reference: src/middlewares/auth.js:30-35

Usage Example

import { auth, adminOnly } from '../middlewares/auth.js';

// Only authenticated admins can access
router.get('/admin/stats', auth, adminOnly, getAdminStats);

// Any authenticated user can access
router.get('/orders', auth, getOrders);

User Schema Fields

Relevant authentication fields in the User model:
{
  email: { type: String, required: true, unique: true },
  username: { type: String, required: true, unique: true },
  password: { type: String, required: true, select: false },
  role: { type: String, enum: ['user', 'admin'], default: 'user' },
  isActive: { type: Boolean, default: true },
  lastLogin: Date,
  twoFactorEnabled: { type: Boolean, default: false },
  twoFactorSecret: { type: String, default: null }
}
Reference: src/models/User.js:4-65
The password field has select: false, meaning it’s excluded from queries by default. Use .select('+password') to explicitly include it during login.

Security Best Practices

  1. Separate Secrets: Use different secrets for access and refresh tokens
  2. Short-Lived Access Tokens: 15-minute expiry limits exposure window
  3. Token Rotation: Refresh tokens are rotated on each use
  4. HttpOnly Cookies: Prevents XSS token theft
  5. SameSite Strict: Prevents CSRF attacks
  6. HTTPS Only: Secure flag enabled in production
  7. Password Hashing: Bcrypt with salt rounds = 10
  8. Account Status: isActive flag allows account disabling

Logout Implementation

While not shown in the provided code, logout typically clears the cookies:
export const logout = (req, res) => {
  res.clearCookie('accessToken');
  res.clearCookie('refreshToken');
  res.json({ message: 'Logged out successfully' });
};

Build docs developers (and LLMs) love