Node.js: Auth and Security
Last updated: 2026-08-26
1. Story: An Unprotected API
Alice spent two weeks deploying the user management API, only to discover the next day that the database had been deleted—her API had no authentication, so anyone could call it directly DELETE /api/users. Having learned her lesson, she implemented JWT authentication: users must first log in to obtain a token, and subsequent requests must include the token to access protected routes. At the same time, she added role-based middleware for administrator operations, ensuring that even users with a token cannot perform dangerous actions. A week later, she used Helmet to add security measures to the response headers, and the API was finally as secure as a fortress.
2. Core Concepts
(1) The Difference Between Authentication and Authorization
- Authentication: Verifying "who you are," such as username and password verification
- Authorization: Verifies "what you can do," such as determining role-based permissions
- The two are often used together: first, identity is verified; then, the operation is authorized.
(2) Overview of JWT Principles
- A JWT (JSON Web Token) is a stateless token consisting of three Base64-encoded segments
- Once issued by the server, it does not need to be stored; the client saves it and includes it in the request.
- Suitable for distributed systems, avoiding session sharing issues
(3) bcrypt Password Hashing
- bcrypt is a hash algorithm designed specifically for passwords, with a built-in salt
- You can adjust the computation time using
cost factorto resist brute-force attacks. - The same plaintext produces a different hash result each time, making it far more secure than MD5/SHA
3. Technical Details
(1) The Structure and Working Principles of JWT
A JWT consists of three parts connected by .:
Header.Payload.Signature
| Section | Content | Description |
|---|---|---|
| Header | { "alg": "HS256", "typ": "JWT" } |
Algorithms and Data Structures |
| Payload | { "userId": 1, "role": "admin", "exp": ... } |
Custom Declaration + Standard Declaration |
| Signature | HMACSHA256(base64(header) + "." + base64(payload), secret) |
Tamper-proof signature |
Workflow:
- User logs in; the server verifies the credentials
- If authentication is successful, issue a JWT and return it to the client
- The client stores the token in localStorage or a cookie
- Subsequent requests include the following in the
Authorization: Bearer <token>header: - Server-side middleware verifies the signature and expiration time
▶ Example: Issuing and Verifying a JWT
const jwt = require('jsonwebtoken');
const SECRET = 'my_super_secret_key';
const token = jwt.sign(
{ userId: 42, role: 'admin' },
SECRET,
{ expiresIn: '2h' }
);
console.log('Token:', token);
const decoded = jwt.verify(token, SECRET);
console.log('Decoded:', decoded);
▶ Example: Decoding a JWT to View the Payload (Without Verifying the Signature)
const decoded = jwt.decode(token, { complete: true });
console.log('Header:', decoded.header);
console.log('Payload:', decoded.payload);
(2) bcrypt Parameters and Usage
| Parameter | Recommended Value | Description |
|---|---|---|
| saltRounds | 10-12 | Number of rounds; the higher the number, the more secure but slower |
| Password Length | ≥8 characters | Validated by the front end |
| Algorithms | Blowfish | bcrypt Underlying Algorithms |
▶ Example: Password Hashing and Verification
const bcrypt = require('bcrypt');
async function hashPassword(plainPassword) {
const saltRounds = 10;
const hash = await bcrypt.hash(plainPassword, saltRounds);
console.log('Hash:', hash);
return hash;
}
async function verifyPassword(plainPassword, hash) {
const match = await bcrypt.compare(plainPassword, hash);
console.log('Match:', match);
return match;
}
(async () => {
const hash = await hashPassword('MyPassword123');
await verifyPassword('MyPassword123', hash);
await verifyPassword('WrongPassword', hash);
})();
(3) HTTP Security Headers and Helmet
Helmet enhances security by setting HTTP response headers:
| Safety Header | Purpose | Enabled by Default |
|---|---|---|
| Content-Security-Policy | Prevents XSS and restricts the sources from which resources can be loaded | No (requires manual configuration) |
| X-Frame-Options | Prevent clickjacking | Yes |
| X-Content-Type-Options | Prevent MIME sniffing | Yes |
| Strict-Transport-Security | Enforce HTTPS | Yes |
| X-XSS-Protection | Browser XSS filtering | Deprecated (Helmet no longer enables this by default) |
▶ Example: Integrating Helmet
const express = require('express');
const helmet = require('helmet');
const app = express();
app.use(helmet());
app.use(helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
}
}));
app.get('/', (req, res) => {
res.send('Secured with helmet!');
});
app.listen(3000);
(4) Common Security Threats and Countermeasures
| Threat | Mechanism | Countermeasures |
|---|---|---|
| SQL Injection | Concatenating User Input to Construct Malicious SQL | Parameterized Queries / ORM |
| XSS (Cross-Site Scripting) | Injecting malicious scripts into pages | Escaping output / CSP / Helmet |
| CSRF (Cross-Site Request Forgery) | Sending requests while impersonating an authenticated user | CSRF Token / SameSite Cookie |
| Brute-force attacks | Repeated password attempts | Rate limiting / High bcrypt rounds / Account lockout |
| Man-in-the-Middle Attacks | Intercepting Communication Data | HTTPS / HSTS |
▶ Example: Preventing SQL Injection (Parameterized Queries)
const { Pool } = require('pg');
const pool = new Pool();
app.post('/login', async (req, res) => {
const { username, password } = req.body;
const result = await pool.query(
'SELECT * FROM users WHERE username = $1',
[username]
);
if (result.rows.length === 0) {
return res.status(401).json({ error: 'Invalid credentials' });
}
const user = result.rows[0];
const match = await bcrypt.compare(password, user.password_hash);
if (!match) {
return res.status(401).json({ error: 'Invalid credentials' });
}
const token = jwt.sign({ userId: user.id, role: user.role }, SECRET, { expiresIn: '2h' });
res.json({ token });
});
▶ Example: Preventing XSS (Escaping Output)
function escapeHtml(str) {
return str
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
const userInput = '<script>alert("xss")</script>';
console.log(escapeHtml(userInput));
▶ Example: CSRF Protection (csurf middleware)
const csrf = require('csurf');
const csrfProtection = csrf({ cookie: true });
app.get('/form', csrfProtection, (req, res) => {
res.json({ csrfToken: req.csrfToken() });
});
app.post('/submit', csrfProtection, (req, res) => {
res.json({ message: 'Form submitted successfully' });
});
4. Practical Exercises
(1) Registration API
▶ Example: User Registration API
app.post('/api/register', async (req, res) => {
const { username, password, role } = req.body;
if (!username || !password) {
return res.status(400).json({ error: 'Username and password are required' });
}
if (password.length < 8) {
return res.status(400).json({ error: 'Password must be at least 8 characters' });
}
const existingUser = await pool.query(
'SELECT id FROM users WHERE username = $1',
[username]
);
if (existingUser.rows.length > 0) {
return res.status(409).json({ error: 'Username already exists' });
}
const saltRounds = 10;
const passwordHash = await bcrypt.hash(password, saltRounds);
const result = await pool.query(
'INSERT INTO users (username, password_hash, role) VALUES ($1, $2, $3) RETURNING id, username, role',
[username, passwordHash, role || 'user']
);
const user = result.rows[0];
const token = jwt.sign(
{ userId: user.id, role: user.role },
SECRET,
{ expiresIn: '2h' }
);
res.status(201).json({ user: { id: user.id, username: user.username, role: user.role }, token });
});
(2) Login API
▶ Example: User Login API
app.post('/api/login', async (req, res) => {
const { username, password } = req.body;
if (!username || !password) {
return res.status(400).json({ error: 'Username and password are required' });
}
const result = await pool.query(
'SELECT id, username, password_hash, role FROM users WHERE username = $1',
[username]
);
if (result.rows.length === 0) {
return res.status(401).json({ error: 'Invalid credentials' });
}
const user = result.rows[0];
const match = await bcrypt.compare(password, user.password_hash);
if (!match) {
return res.status(401).json({ error: 'Invalid credentials' });
}
const token = jwt.sign(
{ userId: user.id, username: user.username, role: user.role },
SECRET,
{ expiresIn: '2h' }
);
res.json({
user: { id: user.id, username: user.username, role: user.role },
token
});
});
(3) JWT Authentication Middleware
▶ Example: The authenticate middleware
function authenticate(req, res, next) {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ error: 'Access denied. No token provided.' });
}
const token = authHeader.split(' ')[1];
try {
const decoded = jwt.verify(token, SECRET);
req.user = decoded;
next();
} catch (err) {
if (err.name === 'TokenExpiredError') {
return res.status(401).json({ error: 'Token expired' });
}
return res.status(403).json({ error: 'Invalid token' });
}
}
(4) Role-Based Access Control Middleware
▶ Example: "authorize" role middleware
function authorize(...roles) {
return (req, res, next) => {
if (!req.user) {
return res.status(401).json({ error: 'Authentication required' });
}
if (!roles.includes(req.user.role)) {
return res.status(403).json({ error: 'Insufficient permissions' });
}
next();
};
}
app.get('/api/profile', authenticate, (req, res) => {
res.json({ user: req.user });
});
app.delete('/api/users/:id', authenticate, authorize('admin'), async (req, res) => {
await pool.query('DELETE FROM users WHERE id = $1', [req.params.id]);
res.json({ message: 'User deleted' });
});
app.get('/api/admin/dashboard', authenticate, authorize('admin'), (req, res) => {
res.json({ message: 'Welcome to admin dashboard' });
});
5. Comprehensive Example: A Complete Authentication System
project/
├── server.js
├── middleware/
│ ├── auth.js
│ └── role.js
├── routes/
│ ├── auth.js
│ └── users.js
└── package.json
middleware/auth.js:
const jwt = require('jsonwebtoken');
const SECRET = process.env.JWT_SECRET || 'fallback_dev_secret';
function authenticate(req, res, next) {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ error: 'Access denied. No token provided.' });
}
const token = authHeader.split(' ')[1];
try {
const decoded = jwt.verify(token, SECRET);
req.user = decoded;
next();
} catch (err) {
if (err.name === 'TokenExpiredError') {
return res.status(401).json({ error: 'Token expired. Please login again.' });
}
return res.status(403).json({ error: 'Invalid token.' });
}
}
module.exports = { authenticate, SECRET };
middleware/role.js:
function authorize(...roles) {
return (req, res, next) => {
if (!req.user) {
return res.status(401).json({ error: 'Authentication required.' });
}
if (!roles.includes(req.user.role)) {
return res.status(403).json({ error: 'Forbidden. Insufficient permissions.' });
}
next();
};
}
module.exports = { authorize };
routes/auth.js:
const express = require('express');
const bcrypt = require('bcrypt');
const jwt = require('jsonwebtoken');
const { authenticate, SECRET } = require('../middleware/auth');
const router = express.Router();
const users = [];
router.post('/register', async (req, res) => {
const { username, password, role } = req.body;
if (!username || !password) {
return res.status(400).json({ error: 'Username and password required.' });
}
if (password.length < 8) {
return res.status(400).json({ error: 'Password must be at least 8 characters.' });
}
const exists = users.find(u => u.username === username);
if (exists) {
return res.status(409).json({ error: 'Username already exists.' });
}
const saltRounds = 10;
const passwordHash = await bcrypt.hash(password, saltRounds);
const newUser = {
id: users.length + 1,
username,
passwordHash,
role: role || 'user'
};
users.push(newUser);
const token = jwt.sign(
{ userId: newUser.id, username: newUser.username, role: newUser.role },
SECRET,
{ expiresIn: '2h' }
);
res.status(201).json({
user: { id: newUser.id, username: newUser.username, role: newUser.role },
token
});
});
router.post('/login', async (req, res) => {
const { username, password } = req.body;
if (!username || !password) {
return res.status(400).json({ error: 'Username and password required.' });
}
const user = users.find(u => u.username === username);
if (!user) {
return res.status(401).json({ error: 'Invalid credentials.' });
}
const match = await bcrypt.compare(password, user.passwordHash);
if (!match) {
return res.status(401).json({ error: 'Invalid credentials.' });
}
const token = jwt.sign(
{ userId: user.id, username: user.username, role: user.role },
SECRET,
{ expiresIn: '2h' }
);
res.json({
user: { id: user.id, username: user.username, role: user.role },
token
});
});
router.get('/profile', authenticate, (req, res) => {
const user = users.find(u => u.id === req.user.userId);
if (!user) {
return res.status(404).json({ error: 'User not found.' });
}
res.json({ id: user.id, username: user.username, role: user.role });
});
module.exports = router;
routes/users.js:
const express = require('express');
const { authenticate } = require('../middleware/auth');
const { authorize } = require('../middleware/role');
const router = express.Router();
const users = [];
router.get('/', authenticate, authorize('admin'), (req, res) => {
const safeList = users.map(u => ({ id: u.id, username: u.username, role: u.role }));
res.json(safeList);
});
router.delete('/:id', authenticate, authorize('admin'), (req, res) => {
const index = users.findIndex(u => u.id === parseInt(req.params.id));
if (index === -1) {
return res.status(404).json({ error: 'User not found.' });
}
users.splice(index, 1);
res.json({ message: 'User deleted.' });
});
module.exports = router;
server.js:
const express = require('express');
const helmet = require('helmet');
const authRoutes = require('./routes/auth');
const userRoutes = require('./routes/users');
const app = express();
app.use(helmet());
app.use(express.json());
app.use('/api/auth', authRoutes);
app.use('/api/users', userRoutes);
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).json({ error: 'Internal server error.' });
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
Testing Process:
# Register
curl -X POST http://localhost:3000/api/auth/register \
-H "Content-Type: application/json" \
-d '{"username":"alice","password":"SecurePass123","role":"admin"}'
# Log In
curl -X POST http://localhost:3000/api/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"alice","password":"SecurePass123"}'
# Accessing Protected Routes
curl http://localhost:3000/api/auth/profile \
-H "Authorization: Bearer <your_token>"
# Administrator Actions
curl -X DELETE http://localhost:3000/api/users/2 \
-H "Authorization: Bearer <admin_token>"
# Registration Response
{
"user": { "id": 1, "username": "alice", "role": "admin" },
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
# Login Response
{
"user": { "id": 1, "username": "alice", "role": "admin" },
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
# Insufficient Permissions Response
{ "error": "Forbidden. Insufficient permissions." }
6. JWT Authentication Flowchart
sequenceDiagram
participant Client as Client
participant Server as Server-side
participant DB as Database
Client->>Server: POST /api/login {username, password}
Server->>DB: Query User Records
DB-->>Server: Return User Data
Server->>Server: bcrypt.compare() Confirm Password
alt The password is correct
Server->>Server: jwt.sign() Issued Token
Server-->>Client: Back { token }
Client->>Client: Storage Token
Client->>Server: GET /api/profile<br/>Authorization: Bearer <token>
Server->>Server: jwt.verify() Verification Token
alt Token Valid
Server-->>Client: 200 Return User Data
else Token Invalid or Expired
Server-->>Client: 401/403 Access Denied
end
else Incorrect password
Server-->>Client: 401 Invalid credentials
end
7. Comparisons and References
(1) JWT vs Session vs OAuth
| Dimension | JWT | Session | OAuth 2.0 |
|---|---|---|---|
| Storage Location | Client | Server | Server + Client |
| Stateless | Yes | No | No |
| Scalability | Natively supports distributed systems | Requires shared session storage | Requires an authorization server |
| Use Cases | API Authentication, Microservices | Traditional Web Applications | Third-Party Login |
| Security Risks | Token Leaks That Cannot Be Revoked | Session Hijacking | Replay Attacks |
| Complexity | Low | Low | High |
(2) Selecting bcrypt Parameters
| saltRounds | Approximate Duration | Use Cases |
|---|---|---|
| 8 | ~40 ms | Development and Testing |
| 10 | ~160 ms | Recommended for production environments |
| 12 | ~640 ms | High safety requirements |
| 14 | ~2.5s | Extreme safety scenarios |
(3) Common Security Threats and Countermeasures
| Threat | Example Attack | Defense Method | Tools/Libraries |
|---|---|---|---|
| SQL Injection | ' OR 1=1 -- |
Parameterized Queries | pg/mysql2 |
| XSS | <script>document.cookie</script> |
Escape + CSP | helmet/xss |
| CSRF | Form Submission Forgery | CSRF Token + SameSite | csurf |
| Brute-force attack | Dictionary attack on passwords | Rate limiting + high-round bcrypt | express-rate-limit |
| Man-in-the-Middle Attacks | Sniffing HTTP Traffic | HTTPS + HSTS | helmet/Let's Encrypt |
(4) Common Fields in a JWT Payload
| Field | Full Name | Description |
|---|---|---|
| iss | Issuer | Issuer Identifier |
| sub | Subject | Subject (usually the user ID) |
| aud | Audience | Recipient ID |
| exp | Expiration | Expiration Time (Unix timestamp) |
| iat | Issued At | Issuance Date |
| jti | JWT ID | Unique Identifier (Anti-Replay) |
| userId | Custom | Business User ID |
| role | Custom | User Role |
❓ FAQ
httpOnly cookie to prevent XSS attacks from reading it; localStorage is convenient but vulnerable to XSS theft, so it must be used in conjunction with CSP.Content-Security-Policy-Report-Only during development to monitor violation reports and gradually tighten the policy.📖 Summary
- Story: Core Concepts and Usage of Unprotected APIs
- Core Concepts: Definitions and Applications
- Key Concepts and Usage Methods in Technical Explanations
- Core Concepts and Usage of Practical Exercises
- Comprehensive Example: Core Concepts and Usage of a Complete Authentication System
- Key Concepts and Usage of the JWT Authentication Flowchart
- Key Concepts and Usage of Comparison and Reference
📝 Exercises
- Implement the registration interface. The password must be at least 8 characters long and contain at least one digit. It must be encrypted using bcrypt and stored in the database.
- Implement the login API to issue a JWT with a 1-hour validity period after verifying the password; the payload must include
userIdandrole. - Write the
authenticatemiddleware to extract and validate the JWT fromAuthorization: Bearer <token> - Write the
authorize('admin')middleware to restrict access toDELETE /api/users/:idto users with the "admin" role only. - Integrate Helmet into the project, configure CSP to allow only same-origin scripts and styles to be loaded, and use curl to verify changes in response headers