[ Web Proxy ]
URL:
Viewing: https://raw.githubusercontent.com/bourgeoa/JavaScriptSolidServer/patchAppend/src/auth/token.js [Back]  [Original]

/**
 * Token-based authentication
 *
 * Supports multiple modes:
 * 1. Simple tokens (for local/dev use): base64(JSON({webId, iat, exp})) + HMAC signature
 * 2. Solid-OIDC DPoP tokens (for federation): verified via external IdP JWKS
 * 3. Nostr NIP-98 tokens: Schnorr signatures, returns did:nostr identity
 */

import crypto from 'crypto';
import { verifySolidOidc, hasSolidOidcAuth } from './solid-oidc.js';
import { verifyNostrAuth, hasNostrAuth } from './nostr.js';
import { webIdTlsAuth, hasClientCertificate } from './webid-tls.js';
import { resolveTokenSecret } from './token-secret.js';

// Initialize secret once at module load. See token-secret.js for the
// resolution order (env  ~/.jss/token.secret  exit-or-ephemeral).
const SECRET = resolveTokenSecret();

/**
 * Create a simple token for a WebID
 * @param {string} webId - The WebID to create token for
 * @param {number} [expiresIn] - Expiration time in seconds (default: no expiry)
 * @returns {string} Token string
 */
export function createToken(webId, expiresIn) {
  const payload = {
    webId,
    iat: Math.floor(Date.now() / 1000),
  };
  if (expiresIn !== undefined && expiresIn > 0) {
    payload.exp = Math.floor(Date.now() / 1000) + expiresIn;
  }

  const data = Buffer.from(JSON.stringify(payload)).toString('base64url');
  const signature = crypto
    .createHmac('sha256', SECRET)
    .update(data)
    .digest('base64url');

  return `${data}.${signature}`;
}

/**
 * Verify and decode a simple token (2-part HMAC-signed)
 *
 * SECURITY: Only accepts 2-part simple tokens signed with HMAC.
 * JWT tokens (3-part) require async verification via verifyTokenAsync().
 *
 * @param {string} token - The token to verify
 * @returns {{webId: string, iat: number, exp?: number} | null} Decoded payload or null
 */
export function verifyToken(token) {
  if (!token || typeof token !== 'string') {
    return null;
  }

  const parts = token.split('.');

  // JWT tokens (3 parts) require async verification - reject in sync function
  if (parts.length === 3) {
    return null;
  }

  if (parts.length !== 2) {
    return null;
  }

  const [data, signature] = parts;

  // Verify HMAC signature
  const expectedSig = crypto
    .createHmac('sha256', SECRET)
    .update(data)
    .digest('base64url');

  // Constant-time comparison to prevent timing attacks
  try {
    if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expectedSig))) {
      return null;
    }
  } catch {
    // If lengths don't match, timingSafeEqual throws
    return null;
  }

  // Decode payload
  try {
    const payload = JSON.parse(Buffer.from(data, 'base64url').toString());

    // Check expiration
    if (payload.exp && payload.exp < Math.floor(Date.now() / 1000)) {
      return null;
    }

    return payload;
  } catch {
    return null;
  }
}

/**
 * Verify a JWT token from the credentials endpoint
 * Properly verifies signature against IdP's JWKS
 *
 * @param {string} token - JWT token
 * @returns {Promise}
 */
async function verifyJwtFromIdp(token) {
  try {
    // Dynamically import to avoid circular dependencies
    const { getPublicJwks } = await import('../idp/keys.js');
    const jose = await import('jose');

    const jwks = await getPublicJwks();
    if (!jwks || !jwks.keys || jwks.keys.length === 0) {
      return null;
    }

    // Create JWKS for verification
    const keySet = jose.createLocalJWKSet(jwks);

    // Verify the token
    const { payload } = await jose.jwtVerify(token, keySet, {
      // Allow some clock skew
      clockTolerance: 60,
    });

    // Extract webid claim
    const webId = payload.webid || payload.webId || payload.sub;
    if (!webId) {
      return null;
    }

    return {
      webId,
      iat: payload.iat,
      exp: payload.exp
    };
  } catch (err) {
    // Verification failed - invalid signature, expired, etc.
    return null;
  }
}

/**
 * Extract token from Authorization header
 * @param {string} authHeader - Authorization header value
 * @returns {string | null} Token or null
 */
export function extractToken(authHeader) {
  if (!authHeader || typeof authHeader !== 'string') {
    return null;
  }

  // Support "Bearer " format
  if (authHeader.startsWith('Bearer ')) {
    return authHeader.slice(7);
  }

  // Also support raw token
  return authHeader;
}

/**
 * Extract WebID from request (sync version for simple tokens only)
 * @param {object} request - Fastify request object
 * @returns {string | null} WebID or null if not authenticated
 */
export function getWebIdFromRequest(request) {
  const authHeader = request.headers.authorization;

  // Skip DPoP tokens - use async version for those
  if (authHeader && authHeader.startsWith('DPoP ')) {
    return null;
  }

  // Skip Nostr tokens - use async version for those
  if (authHeader && authHeader.startsWith('Nostr ')) {
    return null;
  }

  const token = extractToken(authHeader);

  if (!token) {
    return null;
  }

  const payload = verifyToken(token);
  return payload?.webId || null;
}

/**
 * Extract WebID from request (async version supporting Solid-OIDC)
 * @param {object} request - Fastify request object
 * @returns {Promise}
 */
export async function getWebIdFromRequestAsync(request) {
  const authHeader = request.headers.authorization;

  // Try Authorization header methods first
  if (authHeader) {
    // Try Solid-OIDC first (DPoP tokens)
    if (hasSolidOidcAuth(request)) {
      return verifySolidOidc(request);
    }

    // Try Nostr NIP-98 (Schnorr signatures)
    if (hasNostrAuth(request)) {
      return verifyNostrAuth(request);
    }

    // Fall back to Bearer tokens
    const token = extractToken(authHeader);
    if (token) {
      // Try simple 2-part token first
      const payload = verifyToken(token);
      if (payload?.webId) {
        return { webId: payload.webId, error: null };
      }

      // If 3-part JWT, verify against IdP's JWKS
      const parts = token.split('.');
      if (parts.length === 3) {
        const jwtPayload = await verifyJwtFromIdp(token);
        if (jwtPayload?.webId) {
          return { webId: jwtPayload.webId, error: null };
        }
        return { webId: null, error: 'Invalid or unverifiable JWT token' };
      }

      return { webId: null, error: 'Invalid token' };
    }
  }

  // Try WebID-TLS (client certificate authentication)
  // This works even without Authorization header
  if (hasClientCertificate(request)) {
    try {
      const webId = await webIdTlsAuth(request);
      if (webId) {
        return { webId, error: null };
      }
      // Certificate present but verification failed
      return { webId: null, error: 'WebID-TLS certificate verification failed' };
    } catch (err) {
      return { webId: null, error: `WebID-TLS error: ${err.message}` };
    }
  }

  return { webId: null, error: null };
}

Web Proxy Viewer  |  New URL  |  Original Page