Skip to main content
Glama
jwtValidator.ts5.3 kB
/** * JWT validation middleware for OAuth 2.1 authentication * * This module validates JWT access tokens issued by the sigma-auth server * according to MCP 2025 specification and OAuth 2.1 standards. */ import { createRemoteJWKSet, jwtVerify, type JWTPayload } from "jose"; /** * Extended JWT payload with BSV-specific claims */ export interface BSVJWTPayload extends JWTPayload { sub: string; // User ID email?: string; pubkey?: string; // Bitcoin public key bitcoinAddress?: string; // Bitcoin address scope?: string; // OAuth scopes } /** * JWT validation options */ export interface JWTValidationOptions { issuer: string; // Authorization server URL audience: string; // This resource server's URL jwksUrl?: string; // JWKS endpoint (default: issuer/.well-known/jwks.json) } /** * JWT validator class * * Validates JWT access tokens from the authorization server (sigma-auth). * Performs the following checks: * 1. Signature verification using JWKS * 2. Issuer validation * 3. Audience validation * 4. Expiration check * 5. Not-before check */ export class JWTValidator { private readonly issuer: string; private readonly audience: string; private readonly jwksUrl: string; private jwks: ReturnType<typeof createRemoteJWKSet>; constructor(options: JWTValidationOptions) { this.issuer = options.issuer; this.audience = options.audience; this.jwksUrl = options.jwksUrl || `${options.issuer}/.well-known/jwks.json`; // Create JWKS fetcher with caching this.jwks = createRemoteJWKSet(new URL(this.jwksUrl), { // Cache for 1 hour by default cacheMaxAge: 3600000, // Cooldown period to prevent JWKS refetch spam cooldownDuration: 30000, }); } /** * Validate a JWT access token * * @param token - The JWT access token to validate * @returns The validated JWT payload * @throws Error if validation fails */ async validate(token: string): Promise<BSVJWTPayload> { try { // Verify JWT signature and claims const { payload } = await jwtVerify(token, this.jwks, { issuer: this.issuer, audience: this.audience, // Require the standard claims requiredClaims: ["sub", "iss", "aud", "exp"], }); // Return typed payload return payload as BSVJWTPayload; } catch (error) { if (error instanceof Error) { // Provide more specific error messages if (error.message.includes("signature")) { throw new Error("Invalid token signature"); } if (error.message.includes("expired")) { throw new Error("Token has expired"); } if (error.message.includes("issuer")) { throw new Error(`Invalid issuer. Expected: ${this.issuer}`); } if (error.message.includes("audience")) { throw new Error(`Invalid audience. Expected: ${this.audience}`); } throw new Error(`Token validation failed: ${error.message}`); } throw error; } } /** * Extract and validate token from Authorization header * * @param authHeader - The Authorization header value * @returns The validated JWT payload, or null if no token * @throws Error if token is present but invalid */ async validateFromHeader( authHeader: string | null ): Promise<BSVJWTPayload | null> { if (!authHeader) { return null; } // Check for Bearer token format if (!authHeader.startsWith("Bearer ")) { throw new Error('Authorization header must use Bearer scheme'); } // Extract token const token = authHeader.substring(7).trim(); if (!token) { throw new Error("Bearer token is empty"); } // Validate and return return await this.validate(token); } /** * Extract and validate token from Request object * * @param request - The HTTP request * @returns The validated JWT payload, or null if no token * @throws Error if token is present but invalid */ async validateFromRequest( request: Request ): Promise<BSVJWTPayload | null> { const authHeader = request.headers.get("Authorization"); return await this.validateFromHeader(authHeader); } } /** * Create a JWT validator instance for the MCP server * * @param resourceUrl - This MCP server's resource URL * @returns JWT validator instance */ export function createMCPJWTValidator(resourceUrl?: string): JWTValidator { const issuer = process.env.OAUTH_ISSUER || "https://auth.sigmaidentity.com"; const audience = resourceUrl || process.env.RESOURCE_URL || "http://localhost:3000"; return new JWTValidator({ issuer, audience, }); } /** * Generate WWW-Authenticate header for 401 responses * * Per RFC 9728 Section 5.1, the WWW-Authenticate header must include * the resource_metadata URL pointing to the Protected Resource Metadata endpoint. * * @param resourceUrl - The resource server URL * @param error - Optional error code (invalid_token, insufficient_scope, etc.) * @param errorDescription - Optional error description * @returns WWW-Authenticate header value */ export function generateWWWAuthenticate( resourceUrl: string, error?: string, errorDescription?: string ): string { let header = `Bearer realm="BSV-MCP", resource_metadata="${resourceUrl}/.well-known/oauth-protected-resource"`; if (error) { header += `, error="${error}"`; } if (errorDescription) { header += `, error_description="${errorDescription}"`; } return header; }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/b-open-io/bsv-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server