Skip to main content

Overview

The Crypto Shop Backend implements a secure JWT-based authentication system that stores tokens in HttpOnly cookies, protecting against XSS attacks. The system uses dual-token authentication with access tokens (15 minutes) and refresh tokens (7 days).

Token Generation

Tokens are generated using the jsonwebtoken library with separate secrets for access and refresh tokens.

Token Utilities

File: src/utils/tokenUtils.js:9
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 };
};

Token Verification

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

export const verifyRefreshToken = (token) => {
  try {
    return jwt.verify(token, process.env.REFRESH_TOKEN_SECRET);
  } catch (error) {
    return null;
  }
};
Cookies are configured with security-first settings to prevent common attacks. File: src/api/auth/login.js:4
const COOKIE_OPTIONS = {
  httpOnly: true,                                    // Prevents JavaScript access
  secure: process.env.NODE_ENV === 'production',    // HTTPS only in production
  sameSite: 'strict',                                // CSRF protection
  maxAge: 7 * 24 * 60 * 60 * 1000                   // 7 days
};
The httpOnly flag prevents client-side JavaScript from accessing the token, protecting against XSS attacks. The sameSite: 'strict' setting provides CSRF protection.

Login Implementation

File: src/api/auth/login.js:11
export const login = async (req, res) => {
  try {
    const { email, password } = req.body;

    const user = await User.findOne({ email }).select('+password');

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

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

    user.lastLogin = new Date();
    await user.save();

    const { accessToken, refreshToken } = generateTokens(user._id, user.role);

    // Set tokens in HttpOnly cookies
    res.cookie('accessToken', accessToken, COOKIE_OPTIONS);
    res.cookie('refreshToken', refreshToken, { 
      ...COOKIE_OPTIONS, 
      maxAge: 7 * 24 * 60 * 60 * 1000 
    });

    res.json({
      message: 'Login successful',
      user: {
        id: user._id,
        email: user.email,
        username: user.username,
        role: user.role,
        wallet: user.wallet.address
      }
    });
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
};

Authentication Middleware

The authentication middleware extracts and verifies the JWT token from cookies.

Auth Middleware

File: src/middlewares/auth.js:3
import { verifyAccessToken } from '../utils/tokenUtils.js';

export const auth = (req, res, next) => {
  try {
    // Extract token from HttpOnly cookie
    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' });
    }
    
    // Attach user data to request
    req.user = decoded;
    next();
  } catch (error) {
    res.status(401).json({ error: 'Authentication error' });
  }
};

Role-Based Authorization

Additional middleware functions enforce role-based access control.

Admin Only Middleware

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

User Only Middleware

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

Environment Variables

Never commit your JWT secrets to version control. Use strong, randomly generated secrets for production.
Required environment variables:
ACCESS_TOKEN_SECRET=your_access_token_secret_here
REFRESH_TOKEN_SECRET=your_refresh_token_secret_here
NODE_ENV=production
CLIENT_URL=https://your-frontend-domain.com

CORS Configuration

File: src/api/index.js:35
app.use(cors({
  origin: process.env.CLIENT_URL || "http://localhost:3000",
  credentials: true  // Required for cookies
}));
The credentials: true setting is essential for the browser to send and receive cookies in cross-origin requests.

Security Best Practices

Token Expiration Strategy

  • Access Token: 15 minutes (short-lived)
  • Refresh Token: 7 days (long-lived)
Short-lived access tokens minimize the window of opportunity if a token is compromised.
FlagPurpose
httpOnlyPrevents XSS attacks by blocking JavaScript access
secureEnsures cookies are only sent over HTTPS
sameSite: 'strict'Prevents CSRF attacks

Additional Security Measures

Always use HTTPS in production. The secure flag will prevent cookies from being sent over unencrypted connections.
  • Password hashing with bcryptjs (10 salt rounds)
  • User account status checking (isActive field)
  • Last login tracking for audit purposes
  • Helmet.js for HTTP header security
  • HPP (HTTP Parameter Pollution) protection

Dependencies

{
  "jsonwebtoken": "^9.0.3",
  "cookie-parser": "^1.4.6",
  "bcryptjs": "^2.4.3"
}

Usage Example

Protected Route

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

// User authentication required
app.get('/api/profile', auth, getProfile);

// Admin authentication required
app.delete('/api/users/:id', auth, adminOnly, deleteUser);

Client-Side Login Request

fetch('http://localhost:5000/api/auth/login', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json'
  },
  credentials: 'include',  // Important: sends cookies
  body: JSON.stringify({
    email: '[email protected]',
    password: 'password123'
  })
});
The credentials: 'include' option must be set on the client-side to send and receive cookies.

Build docs developers (and LLMs) love