MEDIUM insufficient loggingexpressbearer tokens

Insufficient Logging in Express with Bearer Tokens

Insufficient Logging in Express with Bearer Tokens — how this specific combination creates or exposes the vulnerability

Insufficient logging in an Express API that uses Bearer tokens weakens auditability, incident response, and detection of abuse. When endpoints rely on bearer token authentication but do not log key events, an attacker can probe authorization flaws (BOLA/IDOR) or privilege escalation (BFLA) without leaving a trace, making it difficult to link a token to a malicious request.

For example, consider an endpoint that accepts a Bearer token in the Authorization header and accesses a user resource by an :id parameter. If the server does not log the token, the user ID, and the outcome of the authorization decision, an attacker can iterate over IDs or swap tokens without visibility for defenders. This lack of observability complements broken object level authorization because there is no log evidence to help detect or reconstruct an access path that should have been denied.

In practice, insufficient logging becomes critical when tokens are passed but not tied to a per-request audit record. Without logging the token (or a token identifier), the timestamp, the endpoint path, the HTTP method, the response status, and the authorization result, you cannot reliably answer: which token accessed which resource, when, and with what outcome. That gap increases the risk of data exposure and complicates forensic analysis after a breach, such as following an OWASP API Top 10 scenario where an attacker iterates over IDs to exfiltrate other users’ data.

To address this securely while still handling Bearer tokens, you should log enough context to reconstruct requests without logging the raw token in plaintext where avoidable. A balanced approach logs a token hash or a redacted representation alongside request metadata. The following Express example shows how to structure logging for a Bearer-protected route, including token hashing, user and endpoint context, and structured output suitable for ingestion by monitoring tools.

const express = require('express');
const crypto = require('crypto');
const app = express();

app.use(express.json());

function hashToken(token) {
  return crypto.createHash('sha256').update(token).digest('hex');
}

app.get('/users/:id', (req, res) => {
  const authHeader = req.headers.authorization || '';
  const match = authHeader.match(/^Bearer\s(\S+)$/);
  const token = match ? match[1] : null;
  const tokenHash = token ? hashToken(token) : null;

  // Example user resolution from token validation (pseudo)
  const requestingUserId = req.user ? req.user.id : null;
  const requestedUserId = req.params.id;

  // Log structured audit record
  console.info(JSON.stringify({
    timestamp: new Date().toISOString(),
    method: req.method,
    path: req.path,
    token_hash: tokenHash,
    requesting_user_id: requestingUserId,
    target_user_id: requestedUserId,
    status_code: null, // set after response
    error: null        // set on failure
  }));

  // Authorization check (simplified)
  if (!token) {
    console.warn(JSON.stringify({ error: 'missing_bearer_token', timestamp: new Date().toISOString() }));
    return res.status(401).json({ error: 'unauthorized' });
  }

  // Simulate token validation and user lookup
  if (requestingUserId !== requestedUserId) {
    console.info(JSON.stringify({
      timestamp: new Date().toISOString(),
      method: req.method,
      path: req.path,
      token_hash: tokenHash,
      requesting_user_id: requestingUserId,
      target_user_id: requestedUserId,
      status_code: 403,
      error: 'forbidden_bola_idor'
    }));
    return res.status(403).json({ error: 'forbidden' });
  }

  // On success, update status code in a follow-up log or use a response finish hook
  res.on('finish', () => {
    console.info(JSON.stringify({
      timestamp: new Date().toISOString(),
      method: req.method,
      path: req.path,
      token_hash: tokenHash,
      requesting_user_id: requestingUserId,
      target_user_id: requestedUserId,
      status_code: res.statusCode,
      error: null
    }));
  });

  res.json({ id: requestedUserId, name: 'Alice' });
});

app.listen(3000, () => console.log('Server running on port 3000'));

This pattern demonstrates how to log method, path, token hash, user IDs, status code, and errors in a structured, machine-readable format. By hashing the Bearer token rather than storing it in plaintext, you reduce exposure while still enabling correlation across requests. Such logging supports detection of BOLA/IDOR and BFLA patterns and aligns with findings that insufficient logging impedes timely detection and remediation.

Bearer Tokens-Specific Remediation in Express — concrete code fixes

Remediation focuses on consistent authentication, precise authorization, and structured audit logging for Bearer tokens in Express. Below are concrete code fixes that demonstrate secure handling, including token validation, user-context logging, and middleware patterns that reduce the risk of insufficient logging and related authorization issues.

First, use a small authentication middleware that extracts and validates the Bearer token, attaches a normalized user context, and ensures every request has an audit entry. The example includes token hashing to avoid storing secrets in logs, status tracking, and error handling that still produces useful log lines.

const express = require('express');
const crypto = require('crypto');
const app = express();

app.use(express.json());

function hashToken(token) {
  return crypto.createHash('sha256').update(token).digest('hex');
}

// Authentication middleware for Bearer tokens
function authenticate(req, res, next) {
  const authHeader = req.headers.authorization || '';
  const match = authHeader.match(/^Bearer\s(\S+)$/);
  const token = match ? match[1] : null;
  const tokenHash = token ? hashToken(token) : null;

  // Replace with real token validation against your identity provider or store
  const fakeValidToken = 'valid_token_123';
  if (!token || token !== fakeValidToken) {
    console.info(JSON.stringify({
      timestamp: new Date().toISOString(),
      event: 'auth_failure',
      token_hash: tokenHash,
      message: 'invalid_or_missing_bearer_token'
    }));
    return res.status(401).json({ error: 'invalid_token' });
  }

  // Simulate user attached after validation
  req.user = { id: 'user-123', role: 'user' };
  req.tokenHash = tokenHash;
  next();
}

// Authorization helper to log and enforce access
function ensureOwnership(req, res, next) {
  const requestingUserId = req.user.id;
  const requestedUserId = req.params.id;

  console.info(JSON.stringify({
    timestamp: new Date().toISOString(),
    event: 'authz_check',
    token_hash: req.tokenHash,
    requesting_user_id: requestingUserId,
    target_user_id: requestedUserId
  }));

  if (requestingUserId !== requestedUserId) {
    console.info(JSON.stringify({
      timestamp: new Date().toISOString(),
      event: 'authz_failure',
      token_hash: req.tokenHash,
      requesting_user_id: requestingUserId,
      target_user_id: requestedUserId,
      error: 'forbidden_bola_idor'
    }));
    return res.status(403).json({ error: 'forbidden' });
  }
  next();
}

app.get('/users/:id', authenticate, ensureOwnership, (req, res) => {
  // On success, log outcome with status
  console.info(JSON.stringify({
    timestamp: new Date().toISOString(),
    event: 'request_complete',
    token_hash: req.tokenHash,
    requesting_user_id: req.user.id,
    target_user_id: req.params.id,
    status_code: res.statusCode
  }));
  res.json({ id: req.params.id, name: 'Alice' });
});

app.use((err, req, res, next) => {
  console.error(JSON.stringify({
    timestamp: new Date().toISOString(),
    level: 'error',
    token_hash: req ? req.tokenHash : null,
    message: err.message,
    stack: err.stack
  }));
  res.status(500).json({ error: 'internal_server_error' });
});

app.listen(3000, () => console.log('Server running on port 3000'));

Key remediation points:

  • Centralize Bearer extraction and validation to avoid duplicated logic.
  • Hash sensitive token material before logging to preserve privacy while enabling correlation.
  • Log structured events for authentication success/failure and authorization checks, including token hash, user IDs, path, method, and status code.
  • Use response finish hooks or explicit calls to ensure final status codes are captured in audit logs.
  • Return generic error messages to clients while recording detailed reasons server-side for investigation.

These practices help ensure that Bearer token usage in Express is accompanied by sufficient logging to detect abuse, support incident response, and map findings to frameworks such as OWASP API Top 10 and compliance regimes.

Frequently Asked Questions

Should I log the full Bearer token for debugging?
No. Log a hashed or redacted representation of the token instead. This allows correlation without exposing secrets in logs and reduces the risk of token leakage in log storage.
What minimal fields should I include in authorization-related logs?
Include timestamp, HTTP method, path, token hash, requesting user ID, target resource ID, outcome (allow/deny), response status code, and error context where applicable. Structured JSON logs are ideal for automated analysis.