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

(2) Overview of JWT Principles

(3) bcrypt Password Hashing



3. Technical Details

(1) The Structure and Working Principles of JWT

A JWT consists of three parts connected by .:

TEXT 📖 Display only
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:

  1. User logs in; the server verifies the credentials
  2. If authentication is successful, issue a JWT and return it to the client
  3. The client stores the token in localStorage or a cookie
  4. Subsequent requests include the following in the Authorization: Bearer <token> header:
  5. Server-side middleware verifies the signature and expiration time

▶ Example: Issuing and Verifying a JWT

JAVASCRIPT
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);
▶ Try it Yourself

▶ Example: Decoding a JWT to View the Payload (Without Verifying the Signature)

JAVASCRIPT
const decoded = jwt.decode(token, { complete: true });
console.log('Header:', decoded.header);
console.log('Payload:', decoded.payload);
▶ Try it Yourself

(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

JAVASCRIPT
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);
})();
▶ Try it Yourself

(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

JAVASCRIPT
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);
▶ Try it Yourself

(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)

JAVASCRIPT
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 });
});
▶ Try it Yourself

▶ Example: Preventing XSS (Escaping Output)

JAVASCRIPT
function escapeHtml(str) {
  return str
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#x27;');
}

const userInput = '<script>alert("xss")</script>';
console.log(escapeHtml(userInput));
▶ Try it Yourself

▶ Example: CSRF Protection (csurf middleware)

JAVASCRIPT
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' });
});
▶ Try it Yourself

4. Practical Exercises

(1) Registration API

▶ Example: User Registration API

JAVASCRIPT
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 });
});
▶ Try it Yourself

(2) Login API

▶ Example: User Login API

JAVASCRIPT
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
  });
});
▶ Try it Yourself

(3) JWT Authentication Middleware

▶ Example: The authenticate middleware

JAVASCRIPT
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' });
  }
}
▶ Try it Yourself

(4) Role-Based Access Control Middleware

▶ Example: "authorize" role middleware

JAVASCRIPT
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' });
});
▶ Try it Yourself

5. Comprehensive Example: A Complete Authentication System

TEXT 📖 Display only
project/
├── server.js
├── middleware/
│   ├── auth.js
│   └── role.js
├── routes/
│   ├── auth.js
│   └── users.js
└── package.json

middleware/auth.js

JAVASCRIPT
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

JAVASCRIPT
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

JAVASCRIPT
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

JAVASCRIPT
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

JAVASCRIPT
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:

BASH
# 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>"
TEXT 📖 Display only
# 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

100%
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

Q Where is the JWT stored?
A It is recommended to store it in an 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.
Q What should I do if a token expires?
A A common two-token approach is to use a short-lived access token and a long-lived refresh token. When the access token expires, use the refresh token to obtain a new one. The refresh token is stored in the database so it can be revoked proactively.
Q Why is bcrypt so slow?
A bcrypt’s cost factor controls the number of hashing rounds; each additional round doubles the processing time. This “slowness” is by design, ensuring that brute-force attacks incur a significant computational cost with every password guess.
Q Is HTTPS required?
A It is required in a production environment. With HTTP, tokens and passwords are transmitted in plain text and can be intercepted by any intermediary; encrypted transmission via HTTPS is the minimum security requirement.
Q How can brute-force attacks be prevented?
A Multi-layered defense—bcrypt increases the computational cost per attempt with higher rounds + express-rate-limit for rate limiting + account locking mechanism + delayed response after failed login attempts.
Q Can a JWT be revoked proactively?
A Since JWTs are stateless by nature, they cannot be revoked proactively. Common solutions include: maintaining a blacklist (storing the JTI of revoked tokens in Redis until they expire), shortening the validity period in conjunction with a refresh token, and requiring secondary authentication for sensitive operations.
Q What should I keep in mind when configuring CSP for Helmet?
A If CSP rules are too strict, they may block legitimate resources from loading. We recommend using Content-Security-Policy-Report-Only during development to monitor violation reports and gradually tighten the policy.

📖 Summary

📝 Exercises

  1. 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.
  2. Implement the login API to issue a JWT with a 1-hour validity period after verifying the password; the payload must include userId and role.
  3. Write the authenticate middleware to extract and validate the JWT from Authorization: Bearer <token>
  4. Write the authorize('admin') middleware to restrict access to DELETE /api/users/:id to users with the "admin" role only.
  5. 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

Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏