All files / src/oauth/endpoints register.ts

17.64% Statements 6/34
0% Branches 0/14
0% Functions 0/3
17.64% Lines 6/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 153 154 155                68x 68x                                               68x                   68x                                                                                                                                                                                             68x             68x                  
/**
 * 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 { logger } 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);
 
    logger.info(
      {
        client_id,
        client_name,
        redirect_uris,
        token_endpoint_auth_method,
      },
      "New OAuth client registered via DCR"
    );
 
    // 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) {
    logger.error({ err: error as Error }, "Error in dynamic client registration");
    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);
}