Skip to content

Commit 6bfbd20

Browse files
committed
Client registration using a "store" interface
1 parent 76f367b commit 6bfbd20

File tree

3 files changed

+84
-41
lines changed

3 files changed

+84
-41
lines changed

src/server/auth/clients.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { OAuthClientInformationFull } from "../../shared/auth.js";
2+
3+
/**
4+
* Stores information about registered OAuth clients for this server.
5+
*/
6+
export interface OAuthRegisteredClientsStore {
7+
/**
8+
* Returns information about a registered client, based on its ID.
9+
*/
10+
getClient(clientId: string): OAuthClientInformationFull | undefined | Promise<OAuthClientInformationFull | undefined>;
11+
12+
/**
13+
* 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.
14+
*
15+
* NOTE: Implements must ensure that client secrets, if present, are expired in accordance with the `client_secret_expires_at` field.
16+
*
17+
* If unimplemented, dynamic client registration is unsupported.
18+
*/
19+
registerClient?(client: OAuthClientInformationFull): OAuthClientInformationFull | Promise<OAuthClientInformationFull>;
20+
}

src/server/auth/handlers/clientRegistration.ts

Lines changed: 62 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -2,46 +2,69 @@ import { Request, Response } from "express";
22
import { OAuthClientInformationFull, OAuthClientMetadataSchema, OAuthClientRegistrationError } from "../../../shared/auth.js";
33
import crypto from 'node:crypto';
44
import bodyParser from 'body-parser';
5+
import { OAuthRegisteredClientsStore } from "../clients.js";
56

6-
async function handler(requestBody: unknown): Promise<OAuthClientInformationFull | OAuthClientRegistrationError> {
7-
let clientMetadata;
8-
try {
9-
clientMetadata = OAuthClientMetadataSchema.parse(requestBody);
10-
} catch (error) {
11-
return { error: "invalid_client_metadata", error_description: String(error) };
7+
export type ClientRegistrationHandlerOptions = {
8+
/**
9+
* A store used to save information about dynamically registered OAuth clients.
10+
*/
11+
store: OAuthRegisteredClientsStore;
12+
13+
/**
14+
* The number of seconds after which to expire issued client secrets, or 0 to prevent expiration of client secrets (not recommended).
15+
*
16+
* If not set, defaults to 30 days.
17+
*/
18+
clientSecretExpirySeconds?: number;
19+
};
20+
21+
const DEFAULT_CLIENT_SECRET_EXPIRY_SECONDS = 30 * 24 * 60 * 60; // 30 days
22+
23+
export function clientRegistrationHandler({ store, clientSecretExpirySeconds = DEFAULT_CLIENT_SECRET_EXPIRY_SECONDS }: ClientRegistrationHandlerOptions) {
24+
if (!store.registerClient) {
25+
throw new Error("Client registration store does not support registering clients");
1226
}
1327

14-
// Implement RFC 7591 dynamic client registration
15-
const clientId = crypto.randomUUID();
16-
const clientSecret = clientMetadata.token_endpoint_auth_method !== 'none'
17-
? crypto.randomBytes(32).toString('hex')
18-
: undefined;
19-
const clientIdIssuedAt = Math.floor(Date.now() / 1000);
20-
21-
const clientInfo: OAuthClientInformationFull = {
22-
...clientMetadata,
23-
client_id: clientId,
24-
client_secret: clientSecret,
25-
client_id_issued_at: clientIdIssuedAt,
26-
client_secret_expires_at: 0 // Set to 0 for non-expiring secret
27-
};
28-
29-
// TODO: Store client information securely
30-
31-
return clientInfo;
32-
}
33-
34-
export const clientRegistrationHandler = (req: Request, res: Response) => bodyParser.json()(req, res, (err) => {
35-
if (err === undefined) {
36-
handler(req.body).then((result) => {
37-
if ("error" in result) {
38-
res.status(400).json(result);
39-
} else {
40-
res.status(201).json(result);
41-
}
42-
}, (error) => {
43-
console.error("Uncaught error in client registration handler:", error);
44-
res.status(500).end("Internal Server Error");
45-
});
28+
async function register(requestBody: unknown): Promise<OAuthClientInformationFull | OAuthClientRegistrationError> {
29+
let clientMetadata;
30+
try {
31+
clientMetadata = OAuthClientMetadataSchema.parse(requestBody);
32+
} catch (error) {
33+
return { error: "invalid_client_metadata", error_description: String(error) };
34+
}
35+
36+
// Implement RFC 7591 dynamic client registration
37+
const clientId = crypto.randomUUID();
38+
const clientSecret = clientMetadata.token_endpoint_auth_method !== 'none'
39+
? crypto.randomBytes(32).toString('hex')
40+
: undefined;
41+
const clientIdIssuedAt = Math.floor(Date.now() / 1000);
42+
43+
let clientInfo: OAuthClientInformationFull = {
44+
...clientMetadata,
45+
client_id: clientId,
46+
client_secret: clientSecret,
47+
client_id_issued_at: clientIdIssuedAt,
48+
client_secret_expires_at: clientSecretExpirySeconds > 0 ? clientIdIssuedAt + clientSecretExpirySeconds : 0
49+
};
50+
51+
clientInfo = await store.registerClient!(clientInfo);
52+
return clientInfo;
4653
}
47-
});
54+
55+
// Actual request handler
56+
return (req: Request, res: Response) => bodyParser.json()(req, res, (err) => {
57+
if (err === undefined) {
58+
register(req.body).then((result) => {
59+
if ("error" in result) {
60+
res.status(400).json(result);
61+
} else {
62+
res.status(201).json(result);
63+
}
64+
}, (error) => {
65+
console.error("Uncaught error in client registration handler:", error);
66+
res.status(500).end("Internal Server Error");
67+
});
68+
}
69+
});
70+
}

src/shared/auth.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ export const OAuthClientMetadataSchema = z.object({
7676
jwks: z.any().optional(),
7777
software_id: z.string().optional(),
7878
software_version: z.string().optional(),
79-
}).passthrough();
79+
}).strip();
8080

8181
/**
8282
* RFC 7591 OAuth 2.0 Dynamic Client Registration client information
@@ -86,7 +86,7 @@ export const OAuthClientInformationSchema = z.object({
8686
client_secret: z.string().optional(),
8787
client_id_issued_at: z.number().optional(),
8888
client_secret_expires_at: z.number().optional(),
89-
}).passthrough();
89+
}).strip();
9090

9191
/**
9292
* RFC 7591 OAuth 2.0 Dynamic Client Registration full response (client information plus metadata)

0 commit comments

Comments
 (0)