🟢 Node.js Q110 / 118

How do you implement authentication and authorization in a Node.js application?

AI-Powered Answer ✓ Answered

Authentication (AuthN) verifies who a user is, while authorization (AuthZ) determines what they are allowed to do. Implementing both securely is crucial for any Node.js application to protect user data and control access to resources.

Authentication (AuthN)

Authentication is the process of verifying the identity of a user, service, or device. In a Node.js application, this typically involves users providing credentials (like a username and password) which are then validated against stored information.

Common Authentication Strategies

  • Local Authentication: Users sign in with a username/email and password stored in your application's database.
  • Token-Based Authentication (JWTs): After successful login, the server issues a JSON Web Token (JWT) to the client, which is then sent with subsequent requests for verification.
  • OAuth2 / OpenID Connect: Delegates authentication to a third-party service (e.g., Google, Facebook), allowing users to sign in without creating new credentials for your app.

Implementing Local Authentication (Username/Password)

This method often involves a combination of password hashing (for security) and session management (to maintain user state).

  • User Registration & Hashing: When a user registers, hash their password using a robust library like bcrypt.js before saving it to the database.
  • Login & Session Management: On login, compare the provided password with the stored hash. If they match, establish a session (e.g., using express-session and passport.js's session strategy) and store user information in the session.
  • Middleware for Protected Routes: Use middleware to check if an active session exists before allowing access to protected routes.
javascript
const bcrypt = require('bcryptjs');
const saltRounds = 10;

// Hashing a password
bcrypt.hash('myPlaintextPassword', saltRounds, function(err, hash) {
  // Store hash in your password DB.
});

// Comparing a password
bcrypt.compare('myPlaintextPassword', hash, function(err, result) {
  // result == true
});
javascript
const express = require('express');
const session = require('express-session');
const app = express();

app.use(session({
  secret: 'your_secret_key',
  resave: false,
  saveUninitialized: true,
  cookie: { secure: false } // Set to true in production with HTTPS
}));

// Example login route
app.post('/login', (req, res) => {
  // Validate credentials (e.g., from req.body.username, req.body.password)
  // ...
  if (isValidUser) {
    req.session.userId = user.id; // Store user ID in session
    req.session.isLoggedIn = true;
    res.send('Logged in successfully!');
  } else {
    res.status(401).send('Invalid credentials');
  }
});

// Middleware to protect routes
function isAuthenticated(req, res, next) {
  if (req.session.isLoggedIn) {
    next(); // User is authenticated, proceed
  } else {
    res.status(403).send('Access denied. Please log in.');
  }
}

app.get('/dashboard', isAuthenticated, (req, res) => {
  res.send(`Welcome, user ${req.session.userId}! This is your dashboard.`);
});

Implementing Token-Based Authentication (JWTs)

JWTs are a popular choice for stateless APIs. The token contains encoded information about the user, signed by the server's secret key, ensuring its integrity.

  • User Login & JWT Generation: Upon successful authentication (e.g., username/password check), generate a JWT using a library like jsonwebtoken. This token typically includes the user's ID and any other relevant payload.
  • Sending JWT with Requests: The client stores the JWT (e.g., in local storage or a cookie) and sends it in the Authorization header (Bearer <token>) with every subsequent request to protected routes.
  • JWT Verification Middleware: On the server, create middleware to extract the JWT from the header, verify it using the same secret key, and if valid, attach the user's information to the request object (e.g., req.user).
javascript
const jwt = require('jsonwebtoken');
const secretKey = 'your_jwt_secret_key'; // Keep this secure!

// On user login
app.post('/api/login', (req, res) => {
  // ... validate user credentials ...
  if (userIsValid) {
    const token = jwt.sign({ userId: user.id, role: user.role }, secretKey, { expiresIn: '1h' });
    res.json({ token });
  } else {
    res.status(401).send('Invalid credentials');
  }
});

// Middleware for JWT verification
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, secretKey, (err, user) => {
    if (err) return res.sendStatus(403); // Invalid token
    req.user = user; // Attach user payload to request
    next();
  });
}

app.get('/api/protected', authenticateToken, (req, res) => {
  res.send(`Welcome, ${req.user.userId}! You accessed a protected route.`);
});

Authorization (AuthZ)

Authorization determines if an authenticated user has permission to access a specific resource or perform a particular action. This typically happens *after* authentication.

Common Authorization Strategies

  • Role-Based Access Control (RBAC): Assigns roles (e.g., 'admin', 'editor', 'viewer') to users, and permissions are then granted to these roles.
  • Attribute-Based Access Control (ABAC): More granular, making access decisions based on attributes of the user, resource, and environment (e.g., 'user.department == resource.department' and 'time.ofDay < 17:00').

Implementing Authorization (RBAC Example)

RBAC is a common and effective strategy. You'll typically store user roles in your database or directly within the JWT payload.

  • Assign Roles to Users: Each user record in your database should include a role field (or an array of roles). For JWTs, include this role in the token payload.
  • Create Authorization Middleware: After authentication, create another middleware that checks the user's role(s) against the required role for the specific route or action.
javascript
function authorizeRoles(requiredRoles) {
  return (req, res, next) => {
    // Assuming req.user is populated by an authentication middleware (e.g., JWT)
    if (!req.user || !req.user.role) {
      return res.status(401).send('User not authenticated or role missing.');
    }

    const userRole = req.user.role; // Or req.user.roles if multiple

    if (requiredRoles.includes(userRole)) {
      next(); // User has the required role, proceed
    } else {
      res.status(403).send('Access denied. Insufficient permissions.');
    }
  };
}

// Example usage:
app.get('/admin-dashboard', authenticateToken, authorizeRoles(['admin']), (req, res) => {
  res.send('Welcome to the admin dashboard!');
});

app.post('/create-article', authenticateToken, authorizeRoles(['admin', 'editor']), (req, res) => {
  res.send('Article created successfully!');
});

Best Practices for AuthN/AuthZ

  • Always Hash Passwords: Never store plaintext passwords. Use strong, adaptive hashing functions like bcrypt or argon2.
  • Use HTTPS: Ensure all communication between client and server is encrypted using HTTPS to prevent man-in-the-middle attacks.
  • Strong JWT Secrets/Keys: Use long, complex, randomly generated strings for JWT secrets and keep them absolutely private.
  • Implement Rate Limiting: Protect against brute-force attacks on login endpoints.
  • Validate All Inputs: Sanitize and validate all user inputs, especially credentials.
  • Handle Errors Gracefully: Avoid revealing too much information in error messages (e.g., 'Username not found' vs. 'Invalid credentials').
  • Keep Dependencies Updated: Regularly update authentication and authorization libraries to patch known vulnerabilities.
  • Refresh Tokens for JWTs: For long-lived sessions with JWTs, implement refresh tokens to minimize the exposure window of access tokens.