make client side client_id generation configurable in the oauth router (#734)

This commit is contained in:
Christian Daguerre 2025-07-14 18:07:50 +02:00 • committed by GitHub
commit cc9ea7fd21
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 36 additions and 6 deletions

View file

@ -16,5 +16,5 @@ export interface OAuthRegisteredClientsStore {
*
* If unimplemented, dynamic client registration is unsupported.
*/
registerClient?(client: OAuthClientInformationFull): OAuthClientInformationFull | Promise<OAuthClientInformationFull>;
registerClient?(client: Omit<OAuthClientInformationFull, "client_id">): OAuthClientInformationFull | Promise<OAuthClientInformationFull>;
}

View file

@ -218,6 +218,26 @@ describe('Client Registration Handler', () => {
expect(response.body.client_secret_expires_at).toBe(0);
});
it('sets no client_id when clientIdGeneration=false', async () => {
// Create handler with no expiry
const customApp = express();
const options: ClientRegistrationHandlerOptions = {
clientsStore: mockClientStoreWithRegistration,
clientIdGeneration: false
};
customApp.use('/register', clientRegistrationHandler(options));
const response = await supertest(customApp)
.post('/register')
.send({
redirect_uris: ['https://example.com/callback']
});
expect(response.status).toBe(201);
expect(response.body.client_id).toBeUndefined();
});
it('handles client with all metadata fields', async () => {
const fullClientMetadata: OAuthClientMetadata = {
redirect_uris: ['https://example.com/callback'],

View file

@ -31,6 +31,13 @@ export type ClientRegistrationHandlerOptions = {
* Registration endpoints are particularly sensitive to abuse and should be rate limited.
*/
rateLimit?: Partial<RateLimitOptions> | false;
/**
* Whether to generate a client ID before calling the client registration endpoint.
*
* If not set, defaults to true.
*/
clientIdGeneration?: boolean;
};
const DEFAULT_CLIENT_SECRET_EXPIRY_SECONDS = 30 * 24 * 60 * 60; // 30 days
@ -38,7 +45,8 @@ const DEFAULT_CLIENT_SECRET_EXPIRY_SECONDS = 30 * 24 * 60 * 60; // 30 days
export function clientRegistrationHandler({
clientsStore,
clientSecretExpirySeconds = DEFAULT_CLIENT_SECRET_EXPIRY_SECONDS,
rateLimit: rateLimitConfig
rateLimit: rateLimitConfig,
clientIdGeneration = true,
}: ClientRegistrationHandlerOptions): RequestHandler {
if (!clientsStore.registerClient) {
throw new Error("Client registration store does not support registering clients");
@ -78,7 +86,6 @@ export function clientRegistrationHandler({
const isPublicClient = clientMetadata.token_endpoint_auth_method === 'none'
// Generate client credentials
const clientId = crypto.randomUUID();
const clientSecret = isPublicClient
? undefined
: crypto.randomBytes(32).toString('hex');
@ -89,14 +96,17 @@ export function clientRegistrationHandler({
const secretExpiryTime = clientsDoExpire ? clientIdIssuedAt + clientSecretExpirySeconds : 0
const clientSecretExpiresAt = isPublicClient ? undefined : secretExpiryTime
let clientInfo: OAuthClientInformationFull = {
let clientInfo: Omit<OAuthClientInformationFull, "client_id"> & { client_id?: string } = {
...clientMetadata,
client_id: clientId,
client_secret: clientSecret,
client_id_issued_at: clientIdIssuedAt,
client_secret_expires_at: clientSecretExpiresAt,
};
if (clientIdGeneration) {
clientInfo.client_id = crypto.randomUUID();
}
clientInfo = await clientsStore.registerClient!(clientInfo);
res.status(201).json(clientInfo);
} catch (error) {

View file

@ -142,7 +142,7 @@ export function mcpAuthRouter(options: AuthRouterOptions): RequestHandler {
new URL(oauthMetadata.registration_endpoint).pathname,
clientRegistrationHandler({
clientsStore: options.provider.clientsStore,
...options,
...options.clientRegistrationOptions,
})
);
}