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 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 | 219x 219x 219x 219x 219x 219x 219x 219x 128x 128x 37x 219x 135x 135x 1x 1x 134x 134x 133x 133x 1x 1x 132x 3x 3x 129x 130x 130x 1x 1x 128x 128x 128x 128x 1x 1x 1x 1x 127x 127x 1x 127x 126x 126x 126x 1x 126x 2x 2x 219x 43x 43x 6x 37x 219x 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 { createHmac, randomUUID } from 'node:crypto';
import { logInfo, logError, logWarn } from '../../logger';
import { loadOAuthConfig } from '../config';
import { sessionStore } from '../session-store';
import type { RegisteredOAuthClient } from '../types';
import { TOKEN_ENDPOINT_AUTH_METHODS } from '../resource';
/**
* Registration is anonymous and durable (RFC 7591 section 3 allows an open endpoint), so
* what one source can occupy is bounded: at most this many registrations per source in
* any hour, and never-used ones expire. Over the limit new registrations are refused
* (RFC 6585 429); none is removed for it, because sources behind one proxy or NAT share
* an address and an earlier client may still be signing in.
*/
export const REGISTRATIONS_PER_SOURCE_PER_HOUR = 100;
const REGISTRATION_WINDOW_MS = 60 * 60 * 1000;
/** A registration that never completes an authorization is removed after this long. */
const UNUSED_REGISTRATION_TTL_MS = 24 * 60 * 60 * 1000;
/** Keyed hash of the registering address: groups registrations without storing the IP. */
function registrationSource(req: Request, secret: string): string {
const address = req.ip ?? req.socket?.remoteAddress ?? 'unknown';
return createHmac('sha256', secret).update(address).digest('base64url').slice(0, 22);
}
/** 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;
}
function toRegisteredClient(client: RegisteredOAuthClient): RegisteredClient {
return {
client_id: client.clientId,
client_secret: client.clientSecret,
redirect_uris: client.redirectUris,
client_name: client.clientName,
token_endpoint_auth_method: client.tokenEndpointAuthMethod,
grant_types: client.grantTypes,
response_types: client.responseTypes,
created_at: client.createdAt,
};
}
/**
* 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> {
const config = loadOAuthConfig();
if (!config) {
res.status(500).json({ error: 'server_error', error_description: 'OAuth not configured' });
return;
}
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;
// RFC 7591 section 3.2.2: only methods the token endpoint authenticates are registered.
if (!TOKEN_ENDPOINT_AUTH_METHODS.includes(token_endpoint_auth_method)) {
res.status(400).json({
error: 'invalid_client_metadata',
error_description: `token_endpoint_auth_method must be one of ${TOKEN_ENDPOINT_AUTH_METHODS.join(', ')}`,
});
return;
}
// 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;
}
}
// Concurrent registrations of one source may each pass the count: the limit is a
// bound on storage, not an exact quota.
const createdAt = Date.now();
const registeredFrom = registrationSource(req, config.sessionSecret);
const recent = await sessionStore.countClientsRegisteredSince(
registeredFrom,
createdAt - REGISTRATION_WINDOW_MS,
);
if (recent >= REGISTRATIONS_PER_SOURCE_PER_HOUR) {
logWarn('Client registration refused: hourly limit of the source reached', { recent });
res.set('Retry-After', String(REGISTRATION_WINDOW_MS / 1000));
res.status(429).json({
error: 'temporarily_unavailable',
error_description: 'Too many client registrations from this address; retry later',
});
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 in the shared storage backend, so every replica and every
// restart knows the client; the response is sent only once the registration is stored.
await sessionStore.storeClient({
clientId: client_id,
clientSecret: client_secret,
redirectUris: redirect_uris,
clientName: client_name,
tokenEndpointAuthMethod: token_endpoint_auth_method,
grantTypes: grant_types,
responseTypes: response_types,
createdAt,
registeredFrom,
expiresAt: createdAt + UNUSED_REGISTRATION_TTL_MS,
});
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 async function getRegisteredClient(clientId: string): Promise<RegisteredClient | undefined> {
const client = await sessionStore.getClient(clientId);
// An unused registration past its expiry is gone even before cleanup removes it.
if (!client || (client.expiresAt !== undefined && client.expiresAt < Date.now())) {
return undefined;
}
return toRegisteredClient(client);
}
/**
* Validate a client's redirect URI
*/
export async function isValidRedirectUri(clientId: string, redirectUri: string): Promise<boolean> {
const client = await getRegisteredClient(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);
}
|