From 6bfbd2092b036977454b92c99f5fd5cbb8c21e7e Mon Sep 17 00:00:00 2001 From: Justin Spahr-Summers Date: Mon, 17 Feb 2025 13:29:51 +0000 Subject: [PATCH] Client registration using a "store" interface --- src/server/auth/clients.ts | 20 ++++ .../auth/handlers/clientRegistration.ts | 95 ++++++++++++------- src/shared/auth.ts | 4 +- 3 files changed, 81 insertions(+), 38 deletions(-) create mode 100644 src/server/auth/clients.ts diff --git a/src/server/auth/clients.ts b/src/server/auth/clients.ts new file mode 100644 index 00000000..a42935aa --- /dev/null +++ b/src/server/auth/clients.ts @@ -0,0 +1,20 @@ +import { OAuthClientInformationFull } from "../../shared/auth.js"; + +/** + * Stores information about registered OAuth clients for this server. + */ +export interface OAuthRegisteredClientsStore { + /** + * Returns information about a registered client, based on its ID. + */ + getClient(clientId: string): OAuthClientInformationFull | undefined | Promise; + + /** + * Registers a new client with the server. The client ID and secret will be automatically generated by the library. A modified version of the client information can be returned to reflect specific values enforced by the server. + * + * NOTE: Implements must ensure that client secrets, if present, are expired in accordance with the `client_secret_expires_at` field. + * + * If unimplemented, dynamic client registration is unsupported. + */ + registerClient?(client: OAuthClientInformationFull): OAuthClientInformationFull | Promise; +} \ No newline at end of file diff --git a/src/server/auth/handlers/clientRegistration.ts b/src/server/auth/handlers/clientRegistration.ts index cdbe35aa..3819c50c 100644 --- a/src/server/auth/handlers/clientRegistration.ts +++ b/src/server/auth/handlers/clientRegistration.ts @@ -2,46 +2,69 @@ import { Request, Response } from "express"; import { OAuthClientInformationFull, OAuthClientMetadataSchema, OAuthClientRegistrationError } from "../../../shared/auth.js"; import crypto from 'node:crypto'; import bodyParser from 'body-parser'; +import { OAuthRegisteredClientsStore } from "../clients.js"; -async function handler(requestBody: unknown): Promise { - let clientMetadata; - try { - clientMetadata = OAuthClientMetadataSchema.parse(requestBody); - } catch (error) { - return { error: "invalid_client_metadata", error_description: String(error) }; +export type ClientRegistrationHandlerOptions = { + /** + * A store used to save information about dynamically registered OAuth clients. + */ + store: OAuthRegisteredClientsStore; + + /** + * The number of seconds after which to expire issued client secrets, or 0 to prevent expiration of client secrets (not recommended). + * + * If not set, defaults to 30 days. + */ + clientSecretExpirySeconds?: number; +}; + +const DEFAULT_CLIENT_SECRET_EXPIRY_SECONDS = 30 * 24 * 60 * 60; // 30 days + +export function clientRegistrationHandler({ store, clientSecretExpirySeconds = DEFAULT_CLIENT_SECRET_EXPIRY_SECONDS }: ClientRegistrationHandlerOptions) { + if (!store.registerClient) { + throw new Error("Client registration store does not support registering clients"); } - // Implement RFC 7591 dynamic client registration - const clientId = crypto.randomUUID(); - const clientSecret = clientMetadata.token_endpoint_auth_method !== 'none' - ? crypto.randomBytes(32).toString('hex') - : undefined; - const clientIdIssuedAt = Math.floor(Date.now() / 1000); + async function register(requestBody: unknown): Promise { + let clientMetadata; + try { + clientMetadata = OAuthClientMetadataSchema.parse(requestBody); + } catch (error) { + return { error: "invalid_client_metadata", error_description: String(error) }; + } - const clientInfo: OAuthClientInformationFull = { - ...clientMetadata, - client_id: clientId, - client_secret: clientSecret, - client_id_issued_at: clientIdIssuedAt, - client_secret_expires_at: 0 // Set to 0 for non-expiring secret - }; + // Implement RFC 7591 dynamic client registration + const clientId = crypto.randomUUID(); + const clientSecret = clientMetadata.token_endpoint_auth_method !== 'none' + ? crypto.randomBytes(32).toString('hex') + : undefined; + const clientIdIssuedAt = Math.floor(Date.now() / 1000); - // TODO: Store client information securely + let clientInfo: OAuthClientInformationFull = { + ...clientMetadata, + client_id: clientId, + client_secret: clientSecret, + client_id_issued_at: clientIdIssuedAt, + client_secret_expires_at: clientSecretExpirySeconds > 0 ? clientIdIssuedAt + clientSecretExpirySeconds : 0 + }; - return clientInfo; -} - -export const clientRegistrationHandler = (req: Request, res: Response) => bodyParser.json()(req, res, (err) => { - if (err === undefined) { - handler(req.body).then((result) => { - if ("error" in result) { - res.status(400).json(result); - } else { - res.status(201).json(result); - } - }, (error) => { - console.error("Uncaught error in client registration handler:", error); - res.status(500).end("Internal Server Error"); - }); + clientInfo = await store.registerClient!(clientInfo); + return clientInfo; } -}); \ No newline at end of file + + // Actual request handler + return (req: Request, res: Response) => bodyParser.json()(req, res, (err) => { + if (err === undefined) { + register(req.body).then((result) => { + if ("error" in result) { + res.status(400).json(result); + } else { + res.status(201).json(result); + } + }, (error) => { + console.error("Uncaught error in client registration handler:", error); + res.status(500).end("Internal Server Error"); + }); + } + }); +} \ No newline at end of file diff --git a/src/shared/auth.ts b/src/shared/auth.ts index d1b0b3fe..fd4a9daa 100644 --- a/src/shared/auth.ts +++ b/src/shared/auth.ts @@ -76,7 +76,7 @@ export const OAuthClientMetadataSchema = z.object({ jwks: z.any().optional(), software_id: z.string().optional(), software_version: z.string().optional(), -}).passthrough(); +}).strip(); /** * RFC 7591 OAuth 2.0 Dynamic Client Registration client information @@ -86,7 +86,7 @@ export const OAuthClientInformationSchema = z.object({ client_secret: z.string().optional(), client_id_issued_at: z.number().optional(), client_secret_expires_at: z.number().optional(), -}).passthrough(); +}).strip(); /** * RFC 7591 OAuth 2.0 Dynamic Client Registration full response (client information plus metadata)