@dwk/oauth 0.1.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +175 -0
  3. package/dist/encoding.d.ts +16 -0
  4. package/dist/encoding.d.ts.map +1 -0
  5. package/dist/encoding.js +26 -0
  6. package/dist/encoding.js.map +1 -0
  7. package/dist/errors.d.ts +54 -0
  8. package/dist/errors.d.ts.map +1 -0
  9. package/dist/errors.js +66 -0
  10. package/dist/errors.js.map +1 -0
  11. package/dist/http.d.ts +19 -0
  12. package/dist/http.d.ts.map +1 -0
  13. package/dist/http.js +50 -0
  14. package/dist/http.js.map +1 -0
  15. package/dist/index.d.ts +42 -0
  16. package/dist/index.d.ts.map +1 -0
  17. package/dist/index.js +39 -0
  18. package/dist/index.js.map +1 -0
  19. package/dist/introspection.d.ts +83 -0
  20. package/dist/introspection.d.ts.map +1 -0
  21. package/dist/introspection.js +118 -0
  22. package/dist/introspection.js.map +1 -0
  23. package/dist/log.d.ts +42 -0
  24. package/dist/log.d.ts.map +1 -0
  25. package/dist/log.js +40 -0
  26. package/dist/log.js.map +1 -0
  27. package/dist/metadata.d.ts +79 -0
  28. package/dist/metadata.d.ts.map +1 -0
  29. package/dist/metadata.js +67 -0
  30. package/dist/metadata.js.map +1 -0
  31. package/dist/observability.d.ts +37 -0
  32. package/dist/observability.d.ts.map +1 -0
  33. package/dist/observability.js +25 -0
  34. package/dist/observability.js.map +1 -0
  35. package/dist/par.d.ts +67 -0
  36. package/dist/par.d.ts.map +1 -0
  37. package/dist/par.js +132 -0
  38. package/dist/par.js.map +1 -0
  39. package/dist/registration.d.ts +71 -0
  40. package/dist/registration.d.ts.map +1 -0
  41. package/dist/registration.js +258 -0
  42. package/dist/registration.js.map +1 -0
  43. package/dist/revocation.d.ts +35 -0
  44. package/dist/revocation.d.ts.map +1 -0
  45. package/dist/revocation.js +50 -0
  46. package/dist/revocation.js.map +1 -0
  47. package/dist/store.d.ts +90 -0
  48. package/dist/store.d.ts.map +1 -0
  49. package/dist/store.js +13 -0
  50. package/dist/store.js.map +1 -0
  51. package/package.json +53 -0
  52. package/src/encoding.ts +26 -0
  53. package/src/errors.ts +80 -0
  54. package/src/http.ts +51 -0
  55. package/src/index.ts +75 -0
  56. package/src/introspection.ts +185 -0
  57. package/src/log.ts +43 -0
  58. package/src/metadata.ts +133 -0
  59. package/src/observability.ts +56 -0
  60. package/src/par.ts +205 -0
  61. package/src/registration.ts +336 -0
  62. package/src/revocation.ts +92 -0
  63. package/src/store.ts +93 -0
package/dist/par.js ADDED
@@ -0,0 +1,132 @@
1
+ /**
2
+ * Pushed Authorization Requests (RFC 9126).
3
+ *
4
+ * The client POSTs its authorization-request parameters directly to this
5
+ * endpoint; the server validates and stores them and hands back a short-lived,
6
+ * single-use `request_uri`. The client then starts the normal authorization
7
+ * flow with just `client_id` + `request_uri`, so the request parameters never
8
+ * travel through the browser/redirect and cannot be tampered with.
9
+ *
10
+ * This lib owns the *push* side (validate → store → mint `request_uri`). The
11
+ * *consume* side lives at the authorization endpoint in the consuming package:
12
+ * use {@link parseRequestUri} to recover the reference and the store's
13
+ * single-use `consume` ({@link PushedAuthorizationStore}). When DPoP binding is
14
+ * enabled and the push carries a `DPoP` header, the proof is verified via
15
+ * `@dwk/dpop` and its `jkt` recorded so the eventual token is key-bound
16
+ * (RFC 9449 §10).
17
+ *
18
+ * @see https://www.rfc-editor.org/rfc/rfc9126
19
+ */
20
+ import { verifyDpopProof } from "@dwk/dpop";
21
+ import { hostFromUrl } from "@dwk/log";
22
+ import { randomIdentifier } from "./encoding";
23
+ import { OAuthError, oauthErrorResponse } from "./errors";
24
+ import { json, methodNotAllowed, readForm } from "./http";
25
+ import { OAuthLogEvent } from "./log";
26
+ import { emit, resolveObservability, } from "./observability";
27
+ /** The URN prefix RFC 9126 §2.2 mandates for a PAR `request_uri`. */
28
+ export const PUSHED_REQUEST_URI_PREFIX = "urn:ietf:params:oauth:request_uri:";
29
+ const DEFAULT_LIFETIME_SECONDS = 60;
30
+ /** Reference entropy in bytes: 256 bits of unguessable `request_uri`. */
31
+ const REFERENCE_BYTES = 32;
32
+ /** Build the `request_uri` URN for a stored reference. */
33
+ export function requestUriFor(reference) {
34
+ return `${PUSHED_REQUEST_URI_PREFIX}${reference}`;
35
+ }
36
+ /**
37
+ * Recover the opaque reference from a PAR `request_uri`, or `null` if `uri` is
38
+ * not a `urn:ietf:params:oauth:request_uri:` value. Use this at the
39
+ * authorization endpoint before calling the store's single-use `consume`.
40
+ */
41
+ export function parseRequestUri(uri) {
42
+ if (!uri.startsWith(PUSHED_REQUEST_URI_PREFIX))
43
+ return null;
44
+ const reference = uri.slice(PUSHED_REQUEST_URI_PREFIX.length);
45
+ return reference.length > 0 ? reference : null;
46
+ }
47
+ /**
48
+ * Create the pushed-authorization-request endpoint handler. On success it
49
+ * returns `201` with `{ request_uri, expires_in }` (RFC 9126 §2.2).
50
+ */
51
+ export function createPushedAuthorizationRequestHandler(config) {
52
+ const obs = resolveObservability(config);
53
+ const clock = config.now ?? (() => Math.floor(Date.now() / 1000));
54
+ const lifetime = config.lifetimeSeconds ?? DEFAULT_LIFETIME_SECONDS;
55
+ return async (request) => {
56
+ if (request.method.toUpperCase() !== "POST") {
57
+ return methodNotAllowed("POST");
58
+ }
59
+ // Clone before consuming the body so the authenticator can read it too.
60
+ const authRequest = request.clone();
61
+ const form = await readForm(request);
62
+ // RFC 9126 §2.1: the PAR body MUST NOT itself contain a `request_uri`.
63
+ if (form.has("request_uri")) {
64
+ emit(obs, "warn", OAuthLogEvent.PushedRequestRejected, {
65
+ reason: "request_uri_present",
66
+ });
67
+ return oauthErrorResponse(OAuthError.InvalidRequest, "`request_uri` is not allowed in a pushed authorization request");
68
+ }
69
+ const clientId = form.get("client_id") ?? "";
70
+ if (!clientId) {
71
+ emit(obs, "warn", OAuthLogEvent.PushedRequestRejected, {
72
+ reason: "client_id_missing",
73
+ });
74
+ return oauthErrorResponse(OAuthError.InvalidRequest, "`client_id` is required");
75
+ }
76
+ // Authenticate with the extracted `client_id` in hand, so the authenticator
77
+ // can enforce the RFC 9126 §2.1 match (authenticated client == client_id).
78
+ if (config.authenticate &&
79
+ !(await config.authenticate(authRequest, clientId))) {
80
+ emit(obs, "warn", OAuthLogEvent.PushedRequestRejected, {
81
+ reason: "unauthenticated",
82
+ });
83
+ return oauthErrorResponse(OAuthError.InvalidClient, "pushed authorization requests require client authentication", 401, { "WWW-Authenticate": "Bearer" });
84
+ }
85
+ const params = {};
86
+ for (const [key, value] of form)
87
+ params[key] = value;
88
+ if (config.validate) {
89
+ const problem = await config.validate(params);
90
+ if (problem !== null && problem !== undefined) {
91
+ emit(obs, "warn", OAuthLogEvent.PushedRequestRejected, {
92
+ reason: "validation_failed",
93
+ clientHost: hostFromUrl(clientId),
94
+ });
95
+ return oauthErrorResponse(OAuthError.InvalidRequest, problem);
96
+ }
97
+ }
98
+ let jkt;
99
+ if (config.dpopBinding) {
100
+ const proof = request.headers.get("DPoP");
101
+ if (proof) {
102
+ const dpop = await verifyDpopProof({
103
+ proof,
104
+ htm: "POST",
105
+ htu: config.endpoint ?? request.url,
106
+ });
107
+ if (!dpop.valid || !dpop.jkt) {
108
+ emit(obs, "warn", OAuthLogEvent.PushedRequestRejected, {
109
+ reason: "dpop_invalid",
110
+ clientHost: hostFromUrl(clientId),
111
+ });
112
+ return oauthErrorResponse(OAuthError.InvalidDpopProof, `DPoP proof verification failed: ${dpop.reason ?? "unknown"}`);
113
+ }
114
+ jkt = dpop.jkt;
115
+ }
116
+ }
117
+ const reference = randomIdentifier(REFERENCE_BYTES);
118
+ const record = {
119
+ reference,
120
+ clientId,
121
+ params,
122
+ expiresAt: clock() + lifetime,
123
+ ...(jkt ? { jkt } : {}),
124
+ };
125
+ await config.saveRequest(record);
126
+ emit(obs, "info", OAuthLogEvent.PushedRequestStored, {
127
+ clientHost: hostFromUrl(clientId),
128
+ });
129
+ return json({ request_uri: requestUriFor(reference), expires_in: lifetime }, 201);
130
+ };
131
+ }
132
+ //# sourceMappingURL=par.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"par.js","sourceRoot":"","sources":["../src/par.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,71 @@
1
+ /**
2
+ * OAuth 2.0 Dynamic Client Registration (RFC 7591).
3
+ *
4
+ * A POST endpoint where a client submits its metadata and receives an issued
5
+ * `client_id` (and, for confidential clients, a `client_secret`). The lib
6
+ * validates the metadata against the registered standard fields and the
7
+ * server's supported value lists, normalizes defaults, mints the identifier(s),
8
+ * and delegates persistence to {@link ClientRegistrationConfig.saveClient}.
9
+ *
10
+ * Validation is deliberately strict on the security-relevant fields
11
+ * (`redirect_uris`, `token_endpoint_auth_method`, and the
12
+ * grant/response-type pairing) and ignores unrecognized members rather than
13
+ * echoing arbitrary client-supplied data back as registered metadata.
14
+ *
15
+ * @see https://www.rfc-editor.org/rfc/rfc7591
16
+ */
17
+ import { type OAuthErrorCode } from "./errors";
18
+ import { type ObservabilityConfig } from "./observability";
19
+ import type { EndpointAuthenticator } from "./introspection";
20
+ import type { ClientRecord } from "./store";
21
+ /** Configuration for {@link createClientRegistrationHandler}. */
22
+ export interface ClientRegistrationConfig extends ObservabilityConfig {
23
+ /** Persist a newly registered client record. */
24
+ readonly saveClient: (record: ClientRecord) => Promise<void>;
25
+ /**
26
+ * Optionally authenticate the caller (RFC 7591 §3: an "initial access token"
27
+ * may gate open registration). Omit to allow open registration. Return
28
+ * `false` to reject with `401 invalid_client`.
29
+ */
30
+ readonly authenticate?: EndpointAuthenticator;
31
+ /** Mint the `client_id`. Defaults to a 256-bit random base64url string. */
32
+ readonly generateClientId?: () => string;
33
+ /** Mint the `client_secret`. Defaults to a 256-bit random base64url string. */
34
+ readonly generateClientSecret?: () => string;
35
+ /**
36
+ * Extra redirect-URI policy applied after structural validation (e.g. an
37
+ * allowlist of hosts). Return `false` to reject with `invalid_redirect_uri`.
38
+ */
39
+ readonly redirectUriPolicy?: (uri: string) => boolean;
40
+ /** Grant types the server allows. Defaults to authorization_code + refresh_token. */
41
+ readonly grantTypesSupported?: readonly string[];
42
+ /** Response types the server allows. Defaults to `["code"]`. */
43
+ readonly responseTypesSupported?: readonly string[];
44
+ /** Token-endpoint auth methods the server allows. Defaults to none/basic/post. */
45
+ readonly tokenEndpointAuthMethodsSupported?: readonly string[];
46
+ /** Current time (seconds since the epoch). Defaults to `Date.now()`. */
47
+ readonly now?: () => number;
48
+ }
49
+ /** A validation failure: the error code and a human-readable description. */
50
+ interface MetadataError {
51
+ readonly error: OAuthErrorCode;
52
+ readonly description: string;
53
+ }
54
+ /**
55
+ * Validate and normalize submitted client metadata. Returns the normalized
56
+ * metadata (defaults applied, only recognized members retained) or a
57
+ * {@link MetadataError}. Pure and side-effect-free, so it unit-tests directly.
58
+ */
59
+ export declare function validateClientMetadata(input: unknown, config: ClientRegistrationConfig): {
60
+ readonly metadata: Record<string, unknown>;
61
+ } | MetadataError;
62
+ /**
63
+ * Create the dynamic-client-registration endpoint handler. On success it returns
64
+ * `201` with the client information response (RFC 7591 §3.2.1): the issued
65
+ * `client_id`/`client_id_issued_at`, an optional `client_secret`
66
+ * (+`client_secret_expires_at: 0`, meaning non-expiring) for confidential
67
+ * clients, and the registered metadata.
68
+ */
69
+ export declare function createClientRegistrationHandler(config: ClientRegistrationConfig): (request: Request) => Promise<Response>;
70
+ export {};
71
+ //# sourceMappingURL=registration.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registration.d.ts","sourceRoot":"","sources":["../src/registration.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAKH,OAAO,EAAkC,KAAK,cAAc,EAAE,MAAM,UAAU,CAAC;AAG/E,OAAO,EAGL,KAAK,mBAAmB,EACzB,MAAM,iBAAiB,CAAC;AACzB,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;AAC7D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAU5C,iEAAiE;AACjE,MAAM,WAAW,wBAAyB,SAAQ,mBAAmB;IACnE,gDAAgD;IAChD,QAAQ,CAAC,UAAU,EAAE,CAAC,MAAM,EAAE,YAAY,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7D;;;;OAIG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,qBAAqB,CAAC;IAC9C,2EAA2E;IAC3E,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,MAAM,CAAC;IACzC,+EAA+E;IAC/E,QAAQ,CAAC,oBAAoB,CAAC,EAAE,MAAM,MAAM,CAAC;IAC7C;;;OAGG;IACH,QAAQ,CAAC,iBAAiB,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC;IACtD,qFAAqF;IACrF,QAAQ,CAAC,mBAAmB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACjD,gEAAgE;IAChE,QAAQ,CAAC,sBAAsB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACpD,kFAAkF;IAClF,QAAQ,CAAC,iCAAiC,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC/D,wEAAwE;IACxE,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CAC7B;AAED,6EAA6E;AAC7E,UAAU,aAAa;IACrB,QAAQ,CAAC,KAAK,EAAE,cAAc,CAAC;IAC/B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;CAC9B;AAoCD;;;;GAIG;AACH,wBAAgB,sBAAsB,CACpC,KAAK,EAAE,OAAO,EACd,MAAM,EAAE,wBAAwB,GAC/B;IAAE,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,GAAG,aAAa,CAiJhE;AAED;;;;;;GAMG;AACH,wBAAgB,+BAA+B,CAC7C,MAAM,EAAE,wBAAwB,GAC/B,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,CA+DzC"}
@@ -0,0 +1,258 @@
1
+ /**
2
+ * OAuth 2.0 Dynamic Client Registration (RFC 7591).
3
+ *
4
+ * A POST endpoint where a client submits its metadata and receives an issued
5
+ * `client_id` (and, for confidential clients, a `client_secret`). The lib
6
+ * validates the metadata against the registered standard fields and the
7
+ * server's supported value lists, normalizes defaults, mints the identifier(s),
8
+ * and delegates persistence to {@link ClientRegistrationConfig.saveClient}.
9
+ *
10
+ * Validation is deliberately strict on the security-relevant fields
11
+ * (`redirect_uris`, `token_endpoint_auth_method`, and the
12
+ * grant/response-type pairing) and ignores unrecognized members rather than
13
+ * echoing arbitrary client-supplied data back as registered metadata.
14
+ *
15
+ * @see https://www.rfc-editor.org/rfc/rfc7591
16
+ */
17
+ import { hostFromUrl } from "@dwk/log";
18
+ import { randomIdentifier } from "./encoding";
19
+ import { OAuthError, oauthErrorResponse } from "./errors";
20
+ import { json, methodNotAllowed, readJson } from "./http";
21
+ import { OAuthLogEvent } from "./log";
22
+ import { emit, resolveObservability, } from "./observability";
23
+ const DEFAULT_GRANT_TYPES = ["authorization_code", "refresh_token"];
24
+ const DEFAULT_RESPONSE_TYPES = ["code"];
25
+ const DEFAULT_AUTH_METHODS = [
26
+ "none",
27
+ "client_secret_basic",
28
+ "client_secret_post",
29
+ ];
30
+ /** Recognized RFC 7591 string-valued metadata members echoed back on success. */
31
+ const STRING_FIELDS = [
32
+ "client_name",
33
+ "client_uri",
34
+ "logo_uri",
35
+ "scope",
36
+ "tos_uri",
37
+ "policy_uri",
38
+ "jwks_uri",
39
+ "software_id",
40
+ "software_version",
41
+ ];
42
+ function isObject(value) {
43
+ return typeof value === "object" && value !== null && !Array.isArray(value);
44
+ }
45
+ function isStringArray(value) {
46
+ return Array.isArray(value) && value.every((v) => typeof v === "string");
47
+ }
48
+ /**
49
+ * Whether `uri` is an acceptable redirect URI: a parseable absolute URL with no
50
+ * fragment component (RFC 7591 §2 / RFC 6749 §3.1.2 — the fragment is reserved
51
+ * for the response and must not be pre-registered).
52
+ */
53
+ function isValidRedirectUri(uri) {
54
+ try {
55
+ return new URL(uri).hash === "";
56
+ }
57
+ catch {
58
+ return false;
59
+ }
60
+ }
61
+ /**
62
+ * Validate and normalize submitted client metadata. Returns the normalized
63
+ * metadata (defaults applied, only recognized members retained) or a
64
+ * {@link MetadataError}. Pure and side-effect-free, so it unit-tests directly.
65
+ */
66
+ export function validateClientMetadata(input, config) {
67
+ if (!isObject(input)) {
68
+ return {
69
+ error: OAuthError.InvalidClientMetadata,
70
+ description: "request body must be a JSON object",
71
+ };
72
+ }
73
+ const authMethods = config.tokenEndpointAuthMethodsSupported ?? DEFAULT_AUTH_METHODS;
74
+ const grantTypesSupported = config.grantTypesSupported ?? DEFAULT_GRANT_TYPES;
75
+ const responseTypesSupported = config.responseTypesSupported ?? DEFAULT_RESPONSE_TYPES;
76
+ // token_endpoint_auth_method (default per RFC 7591 §2).
77
+ const authMethod = input.token_endpoint_auth_method ?? "client_secret_basic";
78
+ if (typeof authMethod !== "string" || !authMethods.includes(authMethod)) {
79
+ return {
80
+ error: OAuthError.InvalidClientMetadata,
81
+ description: `unsupported token_endpoint_auth_method: ${String(authMethod)}`,
82
+ };
83
+ }
84
+ // grant_types / response_types (defaults per RFC 7591 §2).
85
+ const requestedGrants = input.grant_types ?? ["authorization_code"];
86
+ if (!isStringArray(requestedGrants)) {
87
+ return {
88
+ error: OAuthError.InvalidClientMetadata,
89
+ description: "grant_types must be an array of strings",
90
+ };
91
+ }
92
+ for (const grant of requestedGrants) {
93
+ if (!grantTypesSupported.includes(grant)) {
94
+ return {
95
+ error: OAuthError.InvalidClientMetadata,
96
+ description: `unsupported grant_type: ${grant}`,
97
+ };
98
+ }
99
+ }
100
+ const requestedResponses = input.response_types ?? ["code"];
101
+ if (!isStringArray(requestedResponses)) {
102
+ return {
103
+ error: OAuthError.InvalidClientMetadata,
104
+ description: "response_types must be an array of strings",
105
+ };
106
+ }
107
+ for (const responseType of requestedResponses) {
108
+ if (!responseTypesSupported.includes(responseType)) {
109
+ return {
110
+ error: OAuthError.InvalidClientMetadata,
111
+ description: `unsupported response_type: ${responseType}`,
112
+ };
113
+ }
114
+ }
115
+ // Grant/response-type consistency (RFC 7591 §2): authorization_code ⇔ code.
116
+ const usesCode = requestedGrants.includes("authorization_code");
117
+ const wantsCode = requestedResponses.includes("code");
118
+ if (usesCode !== wantsCode) {
119
+ return {
120
+ error: OAuthError.InvalidClientMetadata,
121
+ description: "grant_types and response_types are inconsistent: " +
122
+ "`authorization_code` requires `code` and vice versa",
123
+ };
124
+ }
125
+ // redirect_uris: required for redirect-based flows.
126
+ const needsRedirect = requestedGrants.includes("authorization_code") ||
127
+ requestedGrants.includes("implicit");
128
+ const redirectUris = input.redirect_uris;
129
+ if (redirectUris !== undefined) {
130
+ if (!isStringArray(redirectUris)) {
131
+ return {
132
+ error: OAuthError.InvalidRedirectUri,
133
+ description: "redirect_uris must be an array of strings",
134
+ };
135
+ }
136
+ for (const uri of redirectUris) {
137
+ if (!isValidRedirectUri(uri)) {
138
+ return {
139
+ error: OAuthError.InvalidRedirectUri,
140
+ description: `invalid redirect_uri: ${uri}`,
141
+ };
142
+ }
143
+ if (config.redirectUriPolicy && !config.redirectUriPolicy(uri)) {
144
+ return {
145
+ error: OAuthError.InvalidRedirectUri,
146
+ description: `redirect_uri not permitted: ${uri}`,
147
+ };
148
+ }
149
+ }
150
+ }
151
+ if (needsRedirect &&
152
+ (!isStringArray(redirectUris) || redirectUris.length === 0)) {
153
+ return {
154
+ error: OAuthError.InvalidRedirectUri,
155
+ description: "redirect_uris is required for the requested grant_types",
156
+ };
157
+ }
158
+ // contacts: optional array of strings.
159
+ if (input.contacts !== undefined && !isStringArray(input.contacts)) {
160
+ return {
161
+ error: OAuthError.InvalidClientMetadata,
162
+ description: "contacts must be an array of strings",
163
+ };
164
+ }
165
+ // jwks: optional object.
166
+ if (input.jwks !== undefined && !isObject(input.jwks)) {
167
+ return {
168
+ error: OAuthError.InvalidClientMetadata,
169
+ description: "jwks must be a JSON object",
170
+ };
171
+ }
172
+ // Recognized string fields must be strings when present.
173
+ for (const field of STRING_FIELDS) {
174
+ if (input[field] !== undefined && typeof input[field] !== "string") {
175
+ return {
176
+ error: OAuthError.InvalidClientMetadata,
177
+ description: `${field} must be a string`,
178
+ };
179
+ }
180
+ }
181
+ // Assemble normalized metadata: defaults applied, only recognized members.
182
+ const metadata = {
183
+ token_endpoint_auth_method: authMethod,
184
+ grant_types: requestedGrants,
185
+ response_types: requestedResponses,
186
+ };
187
+ if (redirectUris !== undefined)
188
+ metadata.redirect_uris = redirectUris;
189
+ if (input.contacts !== undefined)
190
+ metadata.contacts = input.contacts;
191
+ if (input.jwks !== undefined)
192
+ metadata.jwks = input.jwks;
193
+ for (const field of STRING_FIELDS) {
194
+ if (input[field] !== undefined)
195
+ metadata[field] = input[field];
196
+ }
197
+ return { metadata };
198
+ }
199
+ /**
200
+ * Create the dynamic-client-registration endpoint handler. On success it returns
201
+ * `201` with the client information response (RFC 7591 §3.2.1): the issued
202
+ * `client_id`/`client_id_issued_at`, an optional `client_secret`
203
+ * (+`client_secret_expires_at: 0`, meaning non-expiring) for confidential
204
+ * clients, and the registered metadata.
205
+ */
206
+ export function createClientRegistrationHandler(config) {
207
+ const obs = resolveObservability(config);
208
+ const clock = config.now ?? (() => Math.floor(Date.now() / 1000));
209
+ const newClientId = config.generateClientId ?? (() => randomIdentifier());
210
+ const newClientSecret = config.generateClientSecret ?? (() => randomIdentifier());
211
+ return async (request) => {
212
+ if (request.method.toUpperCase() !== "POST") {
213
+ return methodNotAllowed("POST");
214
+ }
215
+ // Clone so an authenticator that reads the body (e.g. an initial access
216
+ // token in the body) does not disturb the handler's own JSON parse. A
217
+ // registration request carries no `client_id` (one is being issued).
218
+ if (config.authenticate && !(await config.authenticate(request.clone()))) {
219
+ emit(obs, "warn", OAuthLogEvent.ClientRegistrationRejected, {
220
+ reason: "unauthenticated",
221
+ });
222
+ return oauthErrorResponse(OAuthError.InvalidClient, "client registration requires authorization", 401, { "WWW-Authenticate": "Bearer" });
223
+ }
224
+ const body = await readJson(request);
225
+ const result = validateClientMetadata(body, config);
226
+ if ("error" in result) {
227
+ emit(obs, "warn", OAuthLogEvent.ClientRegistrationRejected, {
228
+ reason: result.error,
229
+ });
230
+ return oauthErrorResponse(result.error, result.description);
231
+ }
232
+ const { metadata } = result;
233
+ const issuedAt = clock();
234
+ const clientId = newClientId();
235
+ const confidential = metadata.token_endpoint_auth_method !== "none";
236
+ const clientSecret = confidential ? newClientSecret() : undefined;
237
+ const record = {
238
+ clientId,
239
+ clientIdIssuedAt: issuedAt,
240
+ ...(clientSecret ? { clientSecret } : {}),
241
+ metadata,
242
+ };
243
+ await config.saveClient(record);
244
+ emit(obs, "info", OAuthLogEvent.ClientRegistered, {
245
+ clientHost: hostFromUrl(clientId),
246
+ });
247
+ const response = {
248
+ client_id: clientId,
249
+ client_id_issued_at: issuedAt,
250
+ ...(clientSecret
251
+ ? { client_secret: clientSecret, client_secret_expires_at: 0 }
252
+ : {}),
253
+ ...metadata,
254
+ };
255
+ return json(response, 201);
256
+ };
257
+ }
258
+ //# sourceMappingURL=registration.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registration.js","sourceRoot":"","sources":["../src/registration.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,35 @@
1
+ /**
2
+ * OAuth 2.0 Token Revocation (RFC 7009).
3
+ *
4
+ * A POST endpoint where a client asks the authorization server to invalidate a
5
+ * token. Revocation is **idempotent and forgiving**: an unknown, malformed, or
6
+ * already-revoked token still yields `200` (RFC 7009 §2.2), so a client can
7
+ * retry safely and cannot probe token existence by status code. The lib owns the
8
+ * protocol; the actual invalidation is delegated to
9
+ * {@link RevocationConfig.revokeToken}, backed by the consuming package's store.
10
+ *
11
+ * @see https://www.rfc-editor.org/rfc/rfc7009
12
+ */
13
+ import { type ObservabilityConfig } from "./observability";
14
+ import type { EndpointAuthenticator } from "./introspection";
15
+ /** Configuration for {@link createRevocationHandler}. */
16
+ export interface RevocationConfig extends ObservabilityConfig {
17
+ /**
18
+ * Revoke the presented token. MUST be idempotent and MUST NOT throw for an
19
+ * unknown token — RFC 7009 §2.2 requires `200` regardless. The optional
20
+ * `tokenTypeHint` is the client's non-binding `token_type_hint`.
21
+ */
22
+ readonly revokeToken: (token: string, tokenTypeHint?: string) => Promise<void>;
23
+ /**
24
+ * Optionally authenticate the caller (RFC 7009 §2.1: confidential clients
25
+ * MUST be authenticated). Omit for public clients using the `none` method.
26
+ * Return `false` to reject with `401 invalid_client`.
27
+ */
28
+ readonly authenticate?: EndpointAuthenticator;
29
+ }
30
+ /**
31
+ * Create the revocation endpoint handler. The returned handler accepts a `POST`
32
+ * request and returns `200` with an empty body on success.
33
+ */
34
+ export declare function createRevocationHandler(config: RevocationConfig): (request: Request) => Promise<Response>;
35
+ //# sourceMappingURL=revocation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"revocation.d.ts","sourceRoot":"","sources":["../src/revocation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAKH,OAAO,EAGL,KAAK,mBAAmB,EACzB,MAAM,iBAAiB,CAAC;AACzB,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;AAE7D,yDAAyD;AACzD,MAAM,WAAW,gBAAiB,SAAQ,mBAAmB;IAC3D;;;;OAIG;IACH,QAAQ,CAAC,WAAW,EAAE,CACpB,KAAK,EAAE,MAAM,EACb,aAAa,CAAC,EAAE,MAAM,KACnB,OAAO,CAAC,IAAI,CAAC,CAAC;IACnB;;;;OAIG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,qBAAqB,CAAC;CAC/C;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,gBAAgB,GACvB,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,CA2CzC"}
@@ -0,0 +1,50 @@
1
+ /**
2
+ * OAuth 2.0 Token Revocation (RFC 7009).
3
+ *
4
+ * A POST endpoint where a client asks the authorization server to invalidate a
5
+ * token. Revocation is **idempotent and forgiving**: an unknown, malformed, or
6
+ * already-revoked token still yields `200` (RFC 7009 §2.2), so a client can
7
+ * retry safely and cannot probe token existence by status code. The lib owns the
8
+ * protocol; the actual invalidation is delegated to
9
+ * {@link RevocationConfig.revokeToken}, backed by the consuming package's store.
10
+ *
11
+ * @see https://www.rfc-editor.org/rfc/rfc7009
12
+ */
13
+ import { OAuthError, oauthErrorResponse } from "./errors";
14
+ import { methodNotAllowed, readForm } from "./http";
15
+ import { OAuthLogEvent } from "./log";
16
+ import { emit, resolveObservability, } from "./observability";
17
+ /**
18
+ * Create the revocation endpoint handler. The returned handler accepts a `POST`
19
+ * request and returns `200` with an empty body on success.
20
+ */
21
+ export function createRevocationHandler(config) {
22
+ const obs = resolveObservability(config);
23
+ return async (request) => {
24
+ if (request.method.toUpperCase() !== "POST") {
25
+ return methodNotAllowed("POST");
26
+ }
27
+ // Clone before consuming the body so the authenticator can read it too.
28
+ const authRequest = request.clone();
29
+ const form = await readForm(request);
30
+ const clientId = form.get("client_id") ?? undefined;
31
+ if (config.authenticate &&
32
+ !(await config.authenticate(authRequest, clientId))) {
33
+ emit(obs, "warn", OAuthLogEvent.RevocationRejected, {
34
+ reason: "unauthenticated",
35
+ });
36
+ return oauthErrorResponse(OAuthError.InvalidClient, "revocation requires client authentication", 401, { "WWW-Authenticate": "Bearer" });
37
+ }
38
+ const token = form.get("token") ?? "";
39
+ // A missing `token` is the one malformed-request case RFC 7009 §2.1 lets us
40
+ // reject; a present-but-unknown token is still a success.
41
+ if (!token) {
42
+ return oauthErrorResponse(OAuthError.InvalidRequest, "`token` is required");
43
+ }
44
+ const hint = form.get("token_type_hint") ?? undefined;
45
+ await config.revokeToken(token, hint);
46
+ emit(obs, "info", OAuthLogEvent.TokenRevoked);
47
+ return new Response(null, { status: 200 });
48
+ };
49
+ }
50
+ //# sourceMappingURL=revocation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"revocation.js","sourceRoot":"","sources":["../src/revocation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,UAAU,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAC;AAC1D,OAAO,EAAE,gBAAgB,EAAE,QAAQ,EAAE,MAAM,QAAQ,CAAC;AACpD,OAAO,EAAE,aAAa,EAAE,MAAM,OAAO,CAAC;AACtC,OAAO,EACL,IAAI,EACJ,oBAAoB,GAErB,MAAM,iBAAiB,CAAC;AAsBzB;;;GAGG;AACH,MAAM,UAAU,uBAAuB,CACrC,MAAwB;IAExB,MAAM,GAAG,GAAG,oBAAoB,CAAC,MAAM,CAAC,CAAC;IAEzC,OAAO,KAAK,EAAE,OAAO,EAAE,EAAE;QACvB,IAAI,OAAO,CAAC,MAAM,CAAC,WAAW,EAAE,KAAK,MAAM,EAAE,CAAC;YAC5C,OAAO,gBAAgB,CAAC,MAAM,CAAC,CAAC;QAClC,CAAC;QAED,wEAAwE;QACxE,MAAM,WAAW,GAAG,OAAO,CAAC,KAAK,EAAE,CAAC;QACpC,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,OAAO,CAAC,CAAC;QAErC,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,CAAC,WAAW,CAAC,IAAI,SAAS,CAAC;QACpD,IACE,MAAM,CAAC,YAAY;YACnB,CAAC,CAAC,MAAM,MAAM,CAAC,YAAY,CAAC,WAAW,EAAE,QAAQ,CAAC,CAAC,EACnD,CAAC;YACD,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,aAAa,CAAC,kBAAkB,EAAE;gBAClD,MAAM,EAAE,iBAAiB;aAC1B,CAAC,CAAC;YACH,OAAO,kBAAkB,CACvB,UAAU,CAAC,aAAa,EACxB,2CAA2C,EAC3C,GAAG,EACH,EAAE,kBAAkB,EAAE,QAAQ,EAAE,CACjC,CAAC;QACJ,CAAC;QAED,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC;QACtC,4EAA4E;QAC5E,0DAA0D;QAC1D,IAAI,CAAC,KAAK,EAAE,CAAC;YACX,OAAO,kBAAkB,CACvB,UAAU,CAAC,cAAc,EACzB,qBAAqB,CACtB,CAAC;QACJ,CAAC;QACD,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,iBAAiB,CAAC,IAAI,SAAS,CAAC;QAEtD,MAAM,MAAM,CAAC,WAAW,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;QACtC,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,aAAa,CAAC,YAAY,CAAC,CAAC;QAC9C,OAAO,IAAI,QAAQ,CAAC,IAAI,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAC;IAC7C,CAAC,CAAC;AACJ,CAAC"}