All files / src/oauth/endpoints register.ts

100% Statements 34/34
100% Branches 14/14
100% Functions 3/3
100% Lines 34/34

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152                82x 82x                                               82x                   82x 12x 12x             11x     11x 3x       3x       8x 9x 9x   1x       1x         7x         7x 1x       7x                     7x   7x               7x                   7x 1x     7x   1x 1x                   82x 2x           82x 3x 3x     1x   2x    
/**
 * OAuth Dynamic Client Registration Endpoint (RFC 7591)
 *
 * Required by Claude.ai custom connectors.
 * Allows MCP clients to dynamically register themselves.
 */
 
import { Request, Response } from "express";
import { randomUUID } from "crypto";
import { logInfo, logError } from "../../logger";
 
/** Client registration request body */
interface ClientRegistrationRequest {
  redirect_uris?: string[];
  client_name?: string;
  token_endpoint_auth_method?: string;
  grant_types?: string[];
  response_types?: string[];
}
 
/** Registered client data */
interface RegisteredClient {
  client_id: string;
  client_secret?: string;
  redirect_uris: string[];
  client_name?: string;
  token_endpoint_auth_method: string;
  grant_types: string[];
  response_types: string[];
  created_at: number;
}
 
// In-memory store for registered clients (in production, use persistent storage)
const registeredClients: Map<string, RegisteredClient> = new Map();
 
/**
 * Dynamic Client Registration endpoint handler
 *
 * POST /register
 *
 * Accepts client metadata and returns client credentials.
 * Supports public clients (no client_secret) for Claude.ai.
 */
export async function registerHandler(req: Request, res: Response): Promise<void> {
  try {
    const body = req.body as ClientRegistrationRequest;
    const {
      redirect_uris,
      client_name,
      token_endpoint_auth_method = "none",
      grant_types = ["authorization_code", "refresh_token"],
      response_types = ["code"],
    } = body;
 
    // Validate required fields
    if (!redirect_uris || !Array.isArray(redirect_uris) || redirect_uris.length === 0) {
      res.status(400).json({
        error: "invalid_client_metadata",
        error_description: "redirect_uris is required and must be a non-empty array",
      });
      return;
    }
 
    // Validate redirect URIs (must be valid URLs)
    for (const uri of redirect_uris) {
      try {
        new URL(uri);
      } catch {
        res.status(400).json({
          error: "invalid_redirect_uri",
          error_description: `Invalid redirect URI: ${uri}`,
        });
        return;
      }
    }
 
    // Generate client credentials
    const client_id = randomUUID();
 
    // For public clients (token_endpoint_auth_method: "none"), no secret is issued
    // For confidential clients, generate a secret
    let client_secret: string | undefined;
    if (token_endpoint_auth_method !== "none") {
      client_secret = randomUUID() + randomUUID(); // Long random secret
    }
 
    // Store client registration
    const clientData: RegisteredClient = {
      client_id,
      client_secret,
      redirect_uris,
      client_name,
      token_endpoint_auth_method,
      grant_types,
      response_types,
      created_at: Date.now(),
    };
 
    registeredClients.set(client_id, clientData);
 
    logInfo("New OAuth client registered via DCR", {
      client_id,
      client_name,
      redirect_uris,
      token_endpoint_auth_method,
    });
 
    // Return client credentials per RFC 7591
    const response: Record<string, unknown> = {
      client_id,
      redirect_uris,
      client_name,
      token_endpoint_auth_method,
      grant_types,
      response_types,
    };
 
    // Only include client_secret for confidential clients
    if (client_secret) {
      response.client_secret = client_secret;
    }
 
    res.status(201).json(response);
  } catch (error: unknown) {
    logError("Error in dynamic client registration", { err: error as Error });
    res.status(500).json({
      error: "server_error",
      error_description: "Failed to register client",
    });
  }
}
 
/**
 * Get a registered client by ID
 */
export function getRegisteredClient(clientId: string) {
  return registeredClients.get(clientId);
}
 
/**
 * Validate a client's redirect URI
 */
export function isValidRedirectUri(clientId: string, redirectUri: string): boolean {
  const client = registeredClients.get(clientId);
  if (!client) {
    // If client is not registered via DCR, allow any redirect URI
    // (for backwards compatibility with static client_id configuration)
    return true;
  }
  return client.redirect_uris.includes(redirectUri);
}