@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.
- package/LICENSE +15 -0
- package/README.md +175 -0
- package/dist/encoding.d.ts +16 -0
- package/dist/encoding.d.ts.map +1 -0
- package/dist/encoding.js +26 -0
- package/dist/encoding.js.map +1 -0
- package/dist/errors.d.ts +54 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +66 -0
- package/dist/errors.js.map +1 -0
- package/dist/http.d.ts +19 -0
- package/dist/http.d.ts.map +1 -0
- package/dist/http.js +50 -0
- package/dist/http.js.map +1 -0
- package/dist/index.d.ts +42 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +39 -0
- package/dist/index.js.map +1 -0
- package/dist/introspection.d.ts +83 -0
- package/dist/introspection.d.ts.map +1 -0
- package/dist/introspection.js +118 -0
- package/dist/introspection.js.map +1 -0
- package/dist/log.d.ts +42 -0
- package/dist/log.d.ts.map +1 -0
- package/dist/log.js +40 -0
- package/dist/log.js.map +1 -0
- package/dist/metadata.d.ts +79 -0
- package/dist/metadata.d.ts.map +1 -0
- package/dist/metadata.js +67 -0
- package/dist/metadata.js.map +1 -0
- package/dist/observability.d.ts +37 -0
- package/dist/observability.d.ts.map +1 -0
- package/dist/observability.js +25 -0
- package/dist/observability.js.map +1 -0
- package/dist/par.d.ts +67 -0
- package/dist/par.d.ts.map +1 -0
- package/dist/par.js +132 -0
- package/dist/par.js.map +1 -0
- package/dist/registration.d.ts +71 -0
- package/dist/registration.d.ts.map +1 -0
- package/dist/registration.js +258 -0
- package/dist/registration.js.map +1 -0
- package/dist/revocation.d.ts +35 -0
- package/dist/revocation.d.ts.map +1 -0
- package/dist/revocation.js +50 -0
- package/dist/revocation.js.map +1 -0
- package/dist/store.d.ts +90 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +13 -0
- package/dist/store.js.map +1 -0
- package/package.json +53 -0
- package/src/encoding.ts +26 -0
- package/src/errors.ts +80 -0
- package/src/http.ts +51 -0
- package/src/index.ts +75 -0
- package/src/introspection.ts +185 -0
- package/src/log.ts +43 -0
- package/src/metadata.ts +133 -0
- package/src/observability.ts +56 -0
- package/src/par.ts +205 -0
- package/src/registration.ts +336 -0
- package/src/revocation.ts +92 -0
- 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
|
package/dist/par.js.map
ADDED
|
@@ -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"}
|