🟢 Node.js Q73 / 118

How to implement authentication and authorization in Node.js?

AI-Powered Answer ✓ Answered

Authentication confirms a user's identity, while authorization determines what actions an authenticated user can perform. Both are critical components for securing Node.js applications, ensuring only legitimate and authorized users can access specific resources or functionalities.

Authentication vs. Authorization

Authentication is the process of verifying who a user is. This typically involves users providing credentials (like username and password) which are then validated against stored records. Authorization, on the other hand, is the process of determining what an authenticated user is permitted to do. It checks if the user has the necessary rights or roles to access a specific resource or perform a particular action.

Common Authentication Strategies

In Node.js applications, the two most prevalent strategies for user authentication are token-based authentication (e.g., JSON Web Tokens or JWTs) and session-based authentication.

Token-based Authentication (JWT)

JWTs are a popular, stateless method for authentication. Upon successful login, the server generates a JWT containing user information (payload), signs it with a secret key, and sends it back to the client. The client stores this token (e.g., in localStorage or HttpOnly cookie) and includes it in the Authorization header of subsequent requests. The server then verifies the token's signature and expiration on each request without needing to check a database every time.

javascript
const jwt = require('jsonwebtoken');
const SECRET_KEY = process.env.JWT_SECRET || 'supersecretkey_for_jwt';

// --- Authentication (Login) ---
function generateAuthToken(user) {
  const payload = { userId: user.id, username: user.username, roles: user.roles };
  const token = jwt.sign(payload, SECRET_KEY, { expiresIn: '1h' });
  return token;
}

// --- Authorization (Middleware) ---
function authenticateToken(req, res, next) {
  const authHeader = req.headers['authorization'];
  const token = authHeader && authHeader.split(' ')[1];

  if (token == null) return res.sendStatus(401); // No token provided

  jwt.verify(token, SECRET_KEY, (err, user) => {
    if (err) return res.sendStatus(403); // Invalid or expired token
    req.user = user; // Attach user payload to request
    next();
  });
}

// Usage in Express example:
// app.post('/login', (req, res) => { /* ... validate user ... */ const token = generateAuthToken(user); res.json({ token }); });
// app.get('/protected', authenticateToken, (req, res) => { res.send(`Hello ${req.user.username}`); });

Session-based Authentication

Session-based authentication is a stateful approach where the server maintains a session for each authenticated user. After successful login, the server creates a session record (often stored in memory, a database, or Redis) and sends a unique session ID back to the client, usually as an HttpOnly cookie. On subsequent requests, the client sends this cookie, and the server uses the session ID to retrieve the user's session data and verify their identity.

javascript
const express = require('express');
const session = require('express-session');
const app = express();

app.use(session({
  secret: process.env.SESSION_SECRET || 'another_secret_key_for_session',
  resave: false,
  saveUninitialized: false,
  cookie: { 
    secure: process.env.NODE_ENV === 'production', // Use secure cookies in production
    httpOnly: true, // Prevent client-side JS from reading the cookie
    maxAge: 24 * 60 * 60 * 1000 // 24 hours 
  }
}));

// Login endpoint
app.post('/login', (req, res) => {
  // ... validate user credentials ...
  const userValid = true; // Placeholder
  const user = { id: 'user123', roles: ['user'] }; // Placeholder user object

  if (userValid) {
    req.session.userId = user.id;
    req.session.roles = user.roles;
    res.send('Logged in successfully!');
  } else {
    res.status(401).send('Invalid credentials');
  }
});

// Protected endpoint
app.get('/dashboard', (req, res) => {
  if (req.session.userId) {
    res.send(`Welcome to your dashboard, user ${req.session.userId}!`);
  } else {
    res.status(401).send('Please log in.');
  }
});

// Logout endpoint
app.post('/logout', (req, res) => {
  req.session.destroy(err => {
    if (err) return res.status(500).send('Could not log out.');
    res.send('Logged out successfully.');
  });
});

Implementing Authorization

Once a user is authenticated, authorization checks ensure they have the necessary permissions. This is commonly implemented using Role-Based Access Control (RBAC) or Permission-Based Access Control. In RBAC, users are assigned roles (e.g., 'admin', 'editor', 'viewer'), and each role has specific permissions. Middleware functions are ideal for checking roles or permissions before allowing access to a route.

javascript
// Example RBAC middleware
function authorizeRoles(requiredRoles) {
  return (req, res, next) => {
    // Ensure req.user is populated by an authentication middleware (e.g., authenticateToken or session logic)
    if (!req.user || !req.user.roles) {
      return res.sendStatus(403); // Forbidden - user not authenticated or roles missing
    }

    const userRoles = Array.isArray(req.user.roles) ? req.user.roles : [req.user.roles];
    const hasPermission = requiredRoles.some(role => userRoles.includes(role));

    if (hasPermission) {
      next(); // User has at least one of the required roles
    } else {
      res.status(403).send('Access Denied: You do not have the necessary permissions.');
    }
  };
}

// Usage with JWT (assuming req.user.roles is set by authenticateToken middleware):
// app.get('/admin', authenticateToken, authorizeRoles(['admin']), (req, res) => {
//   res.send('Welcome, Admin!');
// });

// Usage with session (assuming req.session.roles is set during login):
// app.get('/editor-panel', (req, res, next) => {
//   if (!req.session.userId) return res.sendStatus(401); // Not authenticated
//   // Manually populate req.user for consistency if authorizeRoles expects it
//   req.user = { id: req.session.userId, roles: req.session.roles }; 
//   authorizeRoles(['admin', 'editor'])(req, res, next);
// }, (req, res) => {
//   res.send('Welcome, Editor/Admin!');
// });

Best Practices

  • Use Strong Password Hashing: Always hash passwords with a robust, slow hashing algorithm like bcrypt. Never store plaintext passwords.
  • Securely Store Secrets: Keep sensitive information like JWT secret keys, session secrets, and database credentials in environment variables, not directly in code.
  • Implement HTTPS: Encrypt all communication between client and server using HTTPS to prevent eavesdropping and Man-in-the-Middle attacks.
  • Handle Token/Session Expiration: Implement proper expiration for JWTs and sessions. Use refresh tokens for JWTs to provide a seamless user experience without frequent re-logins.
  • Input Validation and Sanitization: Sanitize all user inputs to prevent common vulnerabilities like SQL injection and XSS attacks.
  • Rate Limiting: Implement rate limiting on authentication endpoints to mitigate brute-force attacks.
  • Clear Sessions/Tokens on Logout: Ensure sessions are destroyed on the server-side and tokens are invalidated (if using a blacklist mechanism) upon user logout.
  • Leverage Libraries: For complex scenarios, consider well-vetted libraries like Passport.js which offers a modular approach to authentication with support for many strategies (local, OAuth, etc.).