🟢 Node.js Q109 / 118

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

AI-Powered Answer ✓ Answered

Authentication and authorization are fundamental security components for any web application. In Node.js, these can be implemented using various strategies and libraries to secure routes and resources, ensuring that only legitimate users can access what they are permitted to see and do. This guide outlines common approaches for both.

Understanding Authentication vs. Authorization

Before diving into implementation, it's crucial to understand the distinction between authentication and authorization:

  • Authentication: The process of verifying who a user is. This typically involves checking credentials like username/password, a token, or a biometric scan. Successful authentication confirms the user's identity.
  • Authorization: The process of determining what an authenticated user is permitted to do. This involves checking the user's roles, permissions, or attributes against the requested resource or action. Authorization occurs after authentication.

Common Authentication Strategies

Node.js applications can leverage several authentication strategies, often with the help of libraries like Passport.js or JSON Web Tokens (JWT).

  • Local Authentication (Username/Password): The traditional method where users provide credentials directly to your application. Passwords should always be hashed and salted before storage.
  • OAuth/OAuth2: A protocol allowing users to grant third-party services access to their resources without sharing their credentials directly (e.g., "Login with Google" or "Login with Facebook").
  • JWT (JSON Web Tokens): A compact, URL-safe means of representing claims to be transferred between two parties. JWTs are often used for stateless authentication, where the server doesn't need to store session information.
  • API Keys: Simple tokens used to identify and authenticate an application or user to an API. Less secure for user authentication, more common for service-to-service or public API access.
  • Session-Based Authentication: After successful login, the server creates a session and stores a session ID (often in a cookie) on the client. The server then uses this ID to identify the user on subsequent requests.

Implementing Local Authentication with Passport.js

Passport.js is a popular authentication middleware for Node.js. It's flexible and supports various authentication strategies via plugins.

1. Installation:

bash
npm install express express-session passport passport-local bcryptjs

2. Setup Express and Session Middleware:

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

app.use(express.urlencoded({ extended: false }));
app.use(session({
  secret: 'supersecretkey',
  resave: false,
  saveUninitialized: false
}));
app.use(passport.initialize());
app.use(passport.session());

3. Configure Local Strategy:

javascript
const LocalStrategy = require('passport-local').Strategy;
const bcrypt = require('bcryptjs');
const User = require('./models/User'); // Assume a User model

passport.use(new LocalStrategy(
  async (username, password, done) => {
    try {
      const user = await User.findOne({ username: username });
      if (!user) { return done(null, false, { message: 'Incorrect username.' }); }

      const isMatch = await bcrypt.compare(password, user.password);
      if (!isMatch) { return done(null, false, { message: 'Incorrect password.' }); }

      return done(null, user);
    } catch (err) {
      return done(err);
    }
  }
));

passport.serializeUser((user, done) => {
  done(null, user.id);
});

passport.deserializeUser(async (id, done) => {
  try {
    const user = await User.findById(id);
    done(null, user);
  } catch (err) {
    done(err);
  }
});

4. Login Route:

javascript
app.post('/login',
  passport.authenticate('local', {
    successRedirect: '/dashboard',
    failureRedirect: '/login',
    failureFlash: true // requires connect-flash
  })
);

User passwords should always be hashed using a strong hashing algorithm like bcrypt before storing them in the database.

javascript
const bcrypt = require('bcryptjs');

// When saving a new user or updating password
const salt = await bcrypt.genSalt(10);
const hashedPassword = await bcrypt.hash(plainTextPassword, salt);

Implementing JWT-based Authentication

JWTs provide a stateless authentication mechanism. Once a user logs in, the server issues a token, and the client sends this token with subsequent requests to access protected resources.

1. Installation:

bash
npm install jsonwebtoken

2. Login and Token Generation:

javascript
const jwt = require('jsonwebtoken');
const SECRET_KEY = 'your_jwt_secret'; // Use environment variable

app.post('/api/login', async (req, res) => {
  // Authenticate user (e.g., compare username/password with DB)
  const user = { id: 'user123', username: 'testuser', role: 'admin' }; // Dummy user
  if (user && req.body.password === 'password123') { // Example check
    const token = jwt.sign(
      { id: user.id, username: user.username, role: user.role },
      SECRET_KEY,
      { expiresIn: '1h' } // Token expires in 1 hour
    );
    res.json({ token });
  } else {
    res.status(401).json({ message: 'Invalid credentials' });
  }
});

3. Token Verification Middleware:

javascript
const verifyToken = (req, res, next) => {
  const authHeader = req.headers['authorization'];
  const token = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN

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

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

app.get('/api/protected', verifyToken, (req, res) => {
  res.json({ message: `Welcome ${req.user.username} to the protected route!` });
});

Implementing Authorization

Authorization typically involves checking the authenticated user's roles or permissions to determine if they can access a specific resource or perform an action. This is often done using middleware.

1. Role-Based Access Control (RBAC): Assign roles (e.g., 'admin', 'editor', 'viewer') to users and define what each role can do.

javascript
const authorize = (roles = []) => {
  if (typeof roles === 'string') {
    roles = [roles];
  }

  return (req, res, next) => {
    if (!req.user || (roles.length && !roles.includes(req.user.role))) {
      return res.status(403).json({ message: 'Forbidden: Insufficient permissions' });
    }
    next();
  };
};

// Usage:
app.get('/admin-dashboard', verifyToken, authorize('admin'), (req, res) => {
  res.json({ message: 'Welcome to the admin dashboard!' });
});

app.put('/edit-post/:id', verifyToken, authorize(['admin', 'editor']), (req, res) => {
  res.json({ message: 'Post updated successfully.' });
});

2. Attribute-Based Access Control (ABAC): More granular control based on attributes of the user, resource, and environment. This can involve more complex logic than simple role checks.

Best Practices for Authentication and Authorization

  • Use HTTPS: Always transmit sensitive data (like credentials and tokens) over encrypted connections.
  • Secure Password Storage: Hash and salt passwords using strong algorithms like bcrypt. Never store plain-text passwords.
  • Input Validation: Validate all user inputs to prevent injection attacks and other vulnerabilities.
  • Rate Limiting: Implement rate limiting on login attempts to prevent brute-force attacks.
  • Environment Variables: Store secrets (e.g., JWT secret keys, database credentials) in environment variables, not directly in code.
  • Token Expiration and Refresh Tokens: For JWTs, use short-lived access tokens combined with longer-lived refresh tokens to improve security.
  • Revocation: Implement a mechanism to revoke tokens or sessions, especially after a password change or security incident.
  • Logging: Log authentication failures and authorization denials for security monitoring, but avoid logging sensitive information.