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;
}
};
HttpOnly Cookie Configuration
Cookies are configured with security-first settings to prevent common attacks.
Cookie Options
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.
Cookie Security Flags
| Flag | Purpose |
|---|
httpOnly | Prevents XSS attacks by blocking JavaScript access |
secure | Ensures 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.