@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/LICENSE
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
ISC License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 David W. Keith
|
|
4
|
+
|
|
5
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
|
6
|
+
purpose with or without fee is hereby granted, provided that the above
|
|
7
|
+
copyright notice and this permission notice appear in all copies.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
10
|
+
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
11
|
+
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
12
|
+
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
13
|
+
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
14
|
+
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
|
15
|
+
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
# `@dwk/oauth`
|
|
2
|
+
|
|
3
|
+
> OAuth 2.0 authorization-server building blocks: RFC 8414 metadata, RFC 7662
|
|
4
|
+
> introspection, RFC 7009 revocation, RFC 9126 PAR, RFC 7591 dynamic client
|
|
5
|
+
> registration. Cross-standard reusable.
|
|
6
|
+
|
|
7
|
+
Part of the [`@dwk` IndieWeb + Solid cohort](../../README.md). See the
|
|
8
|
+
[package specification](../../spec/packages/oauth.md) for the full requirements.
|
|
9
|
+
|
|
10
|
+
This package is **cross-standard reusable**: it takes plain-data inputs only, has
|
|
11
|
+
no Workers-runtime dependency (only Web Fetch + Web Crypto), and unit-tests in
|
|
12
|
+
isolation (Node, no `workerd`). It is **protocol-agnostic** — no IndieWeb/Solid
|
|
13
|
+
claim handling is baked in; the caller supplies issuer/audience policy and the
|
|
14
|
+
storage seams. It exists so [`@dwk/indieauth`](../indieauth) and the eventual
|
|
15
|
+
Solid-OIDC OP share one audited implementation of these primitives rather than
|
|
16
|
+
diverging.
|
|
17
|
+
|
|
18
|
+
## What it provides
|
|
19
|
+
|
|
20
|
+
| Endpoint / artifact | RFC | Factory |
|
|
21
|
+
| --- | --- | --- |
|
|
22
|
+
| Authorization-server metadata | [8414](https://www.rfc-editor.org/rfc/rfc8414) | `buildAuthorizationServerMetadata` |
|
|
23
|
+
| Token introspection | [7662](https://www.rfc-editor.org/rfc/rfc7662) | `createIntrospectionHandler` |
|
|
24
|
+
| Token revocation | [7009](https://www.rfc-editor.org/rfc/rfc7009) | `createRevocationHandler` |
|
|
25
|
+
| Pushed authorization requests | [9126](https://www.rfc-editor.org/rfc/rfc9126) | `createPushedAuthorizationRequestHandler` |
|
|
26
|
+
| Dynamic client registration | [7591](https://www.rfc-editor.org/rfc/rfc7591) | `createClientRegistrationHandler` |
|
|
27
|
+
| OAuth error registry | [6749 §5.2](https://www.rfc-editor.org/rfc/rfc6749#section-5.2) | `OAuthError`, `oauthErrorResponse` |
|
|
28
|
+
|
|
29
|
+
## The static / dynamic split
|
|
30
|
+
|
|
31
|
+
The **metadata document** (`/.well-known/oauth-authorization-server`) is static
|
|
32
|
+
JSON derived from config known at build time, so **Anglesite serves it** — no
|
|
33
|
+
Worker route is needed merely to publish it. `buildAuthorizationServerMetadata`
|
|
34
|
+
is the single source of truth that drives both that static document and any
|
|
35
|
+
runtime behaviour that must agree with what was advertised.
|
|
36
|
+
|
|
37
|
+
The **four POST endpoints** are stateful, so they are the handlers this lib
|
|
38
|
+
provides. Each is a path-agnostic `(request) => Promise<Response>` you mount
|
|
39
|
+
wherever you like and back with your own strongly-consistent storage.
|
|
40
|
+
|
|
41
|
+
## Storage is yours (never KV)
|
|
42
|
+
|
|
43
|
+
This lib owns no database. Token, client, and pushed-request records flow through
|
|
44
|
+
the plain-data seams in `./store`, which you back with a strongly-consistent
|
|
45
|
+
store (D1 with session consistency, or a Durable Object) via
|
|
46
|
+
[`@dwk/store`](../store). Authoritative token/authz state **MUST NOT** live in
|
|
47
|
+
KV: a stale introspection or revocation result is a security bug.
|
|
48
|
+
|
|
49
|
+
## Usage
|
|
50
|
+
|
|
51
|
+
### Metadata (RFC 8414)
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
import { buildAuthorizationServerMetadata } from "@dwk/oauth";
|
|
55
|
+
|
|
56
|
+
const metadata = buildAuthorizationServerMetadata({
|
|
57
|
+
issuer: "https://example.com",
|
|
58
|
+
authorizationEndpoint: "https://example.com/authorize",
|
|
59
|
+
tokenEndpoint: "https://example.com/token",
|
|
60
|
+
introspectionEndpoint: "https://example.com/introspect",
|
|
61
|
+
revocationEndpoint: "https://example.com/revoke",
|
|
62
|
+
pushedAuthorizationRequestEndpoint: "https://example.com/par",
|
|
63
|
+
registrationEndpoint: "https://example.com/register",
|
|
64
|
+
scopesSupported: ["read", "write"],
|
|
65
|
+
dpopSigningAlgValuesSupported: ["ES256"],
|
|
66
|
+
});
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Endpoint URLs and optional value lists are emitted only when supplied, so the
|
|
70
|
+
document never advertises an endpoint you did not mount.
|
|
71
|
+
|
|
72
|
+
### Introspection (RFC 7662)
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
import { createIntrospectionHandler } from "@dwk/oauth";
|
|
76
|
+
|
|
77
|
+
const introspect = createIntrospectionHandler({
|
|
78
|
+
// Required: the endpoint MUST be protected against token scanning.
|
|
79
|
+
authenticate: (request) => verifyResourceServer(request),
|
|
80
|
+
// Look the token up in your strongly-consistent store.
|
|
81
|
+
lookupToken: async (token) => store.findToken(token),
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// active → mapped RFC 7662 claims (snake_case), inactive → { active: false }.
|
|
85
|
+
const response = await introspect(request);
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
A DPoP-bound token surfaces its key thumbprint as `cnf: { jkt }`. The `active`
|
|
89
|
+
flag is derived from the record's `revoked`/`expiresAt`/`notBefore` (or an
|
|
90
|
+
explicit `active` field). `isTokenActive` and `buildIntrospectionResponse` are
|
|
91
|
+
exported as pure helpers.
|
|
92
|
+
|
|
93
|
+
### Revocation (RFC 7009)
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
import { createRevocationHandler } from "@dwk/oauth";
|
|
97
|
+
|
|
98
|
+
const revoke = createRevocationHandler({
|
|
99
|
+
revokeToken: async (token) => store.revoke(token), // MUST be idempotent
|
|
100
|
+
// authenticate is optional — omit for public clients using `none`.
|
|
101
|
+
});
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Revocation always returns `200` (even for an unknown token), so a client can
|
|
105
|
+
retry safely and cannot probe token existence by status code.
|
|
106
|
+
|
|
107
|
+
### Pushed authorization requests (RFC 9126)
|
|
108
|
+
|
|
109
|
+
```ts
|
|
110
|
+
import {
|
|
111
|
+
createPushedAuthorizationRequestHandler,
|
|
112
|
+
parseRequestUri,
|
|
113
|
+
} from "@dwk/oauth";
|
|
114
|
+
|
|
115
|
+
const par = createPushedAuthorizationRequestHandler({
|
|
116
|
+
saveRequest: async (record) => store.savePushedRequest(record),
|
|
117
|
+
validate: (params) => (params.code_challenge ? null : "PKCE required"),
|
|
118
|
+
lifetimeSeconds: 60,
|
|
119
|
+
dpopBinding: true, // verify a DPoP proof at PAR and record its jkt
|
|
120
|
+
endpoint: "https://example.com/par",
|
|
121
|
+
});
|
|
122
|
+
// → 201 { request_uri, expires_in }
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
At the **authorization endpoint** (in your package), recover and single-use the
|
|
126
|
+
reference:
|
|
127
|
+
|
|
128
|
+
```ts
|
|
129
|
+
const reference = parseRequestUri(url.searchParams.get("request_uri")!);
|
|
130
|
+
const pushed = reference ? await store.consumePushedRequest(reference, now) : null;
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
The `authenticate` hook is `(request, clientId?) => boolean | Promise<boolean>`:
|
|
134
|
+
the handler parses the body, hands the extracted `client_id` to the hook, and
|
|
135
|
+
passes a **pre-parse clone** of the request, so an authenticator can read the
|
|
136
|
+
body itself (e.g. a `client_secret_post` credential) without disturbing the
|
|
137
|
+
handler's own parse. This is what lets a PAR authenticator enforce the RFC 9126
|
|
138
|
+
§2.1 requirement that the authenticated client match the request's `client_id`.
|
|
139
|
+
|
|
140
|
+
### Dynamic client registration (RFC 7591)
|
|
141
|
+
|
|
142
|
+
```ts
|
|
143
|
+
import { createClientRegistrationHandler } from "@dwk/oauth";
|
|
144
|
+
|
|
145
|
+
const register = createClientRegistrationHandler({
|
|
146
|
+
saveClient: async (record) => store.saveClient(record),
|
|
147
|
+
// authenticate is optional (gate open registration with an initial token).
|
|
148
|
+
redirectUriPolicy: (uri) => uri.startsWith("https://"),
|
|
149
|
+
});
|
|
150
|
+
// → 201 client information response with the issued client_id (+ secret).
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Validation is strict on the security-relevant fields (`redirect_uris`,
|
|
154
|
+
`token_endpoint_auth_method`, the `authorization_code` ⇔ `code` pairing) and
|
|
155
|
+
ignores unrecognized members rather than echoing arbitrary client-supplied data
|
|
156
|
+
back. `validateClientMetadata` is exported as a pure helper.
|
|
157
|
+
|
|
158
|
+
## Errors
|
|
159
|
+
|
|
160
|
+
`OAuthError` is the registered error-code registry; `oauthErrorResponse(code,
|
|
161
|
+
description?, status?, headers?)` builds the RFC 6749 §5.2 JSON body. `code` is
|
|
162
|
+
typed to the registry, so a non-registered code (the kind that breaks
|
|
163
|
+
standards-compliant clients) cannot be emitted by accident.
|
|
164
|
+
|
|
165
|
+
## Out of scope
|
|
166
|
+
|
|
167
|
+
- The authorization and token endpoints themselves (those are
|
|
168
|
+
[`@dwk/indieauth`](../indieauth) / the future OP, which compose this lib).
|
|
169
|
+
- Storage (you supply it; see above).
|
|
170
|
+
- `DPoP-Nonce` issuance and the `use_dpop_nonce` flow.
|
|
171
|
+
- JWK Set hosting and access-token signing/verification.
|
|
172
|
+
|
|
173
|
+
## License
|
|
174
|
+
|
|
175
|
+
[ISC](../../LICENSE)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* base64url + random-identifier helpers.
|
|
3
|
+
*
|
|
4
|
+
* Kept dependency-free and runtime-agnostic (Web Crypto / `btoa` only) so the
|
|
5
|
+
* surrounding modules unit-test without a Workers runtime.
|
|
6
|
+
*/
|
|
7
|
+
/** Encode bytes as unpadded base64url (RFC 4648 §5). */
|
|
8
|
+
export declare function bytesToBase64url(bytes: Uint8Array): string;
|
|
9
|
+
/**
|
|
10
|
+
* A cryptographically-random unpadded-base64url identifier of `bytes` entropy
|
|
11
|
+
* (default 32 bytes = 256 bits). Used for `request_uri` references, generated
|
|
12
|
+
* `client_id`s, and client secrets, where unguessability is the security
|
|
13
|
+
* property.
|
|
14
|
+
*/
|
|
15
|
+
export declare function randomIdentifier(bytes?: number): string;
|
|
16
|
+
//# sourceMappingURL=encoding.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"encoding.d.ts","sourceRoot":"","sources":["../src/encoding.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,wDAAwD;AACxD,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,CAO1D;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,SAAK,GAAG,MAAM,CAEnD"}
|
package/dist/encoding.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* base64url + random-identifier helpers.
|
|
3
|
+
*
|
|
4
|
+
* Kept dependency-free and runtime-agnostic (Web Crypto / `btoa` only) so the
|
|
5
|
+
* surrounding modules unit-test without a Workers runtime.
|
|
6
|
+
*/
|
|
7
|
+
/** Encode bytes as unpadded base64url (RFC 4648 §5). */
|
|
8
|
+
export function bytesToBase64url(bytes) {
|
|
9
|
+
let binary = "";
|
|
10
|
+
for (const byte of bytes)
|
|
11
|
+
binary += String.fromCharCode(byte);
|
|
12
|
+
return btoa(binary)
|
|
13
|
+
.replace(/\+/g, "-")
|
|
14
|
+
.replace(/\//g, "_")
|
|
15
|
+
.replace(/=+$/, "");
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* A cryptographically-random unpadded-base64url identifier of `bytes` entropy
|
|
19
|
+
* (default 32 bytes = 256 bits). Used for `request_uri` references, generated
|
|
20
|
+
* `client_id`s, and client secrets, where unguessability is the security
|
|
21
|
+
* property.
|
|
22
|
+
*/
|
|
23
|
+
export function randomIdentifier(bytes = 32) {
|
|
24
|
+
return bytesToBase64url(crypto.getRandomValues(new Uint8Array(bytes)));
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=encoding.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"encoding.js","sourceRoot":"","sources":["../src/encoding.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,wDAAwD;AACxD,MAAM,UAAU,gBAAgB,CAAC,KAAiB;IAChD,IAAI,MAAM,GAAG,EAAE,CAAC;IAChB,KAAK,MAAM,IAAI,IAAI,KAAK;QAAE,MAAM,IAAI,MAAM,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC;IAC9D,OAAO,IAAI,CAAC,MAAM,CAAC;SAChB,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC;SACnB,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC;SACnB,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;AACxB,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,gBAAgB,CAAC,KAAK,GAAG,EAAE;IACzC,OAAO,gBAAgB,CAAC,MAAM,CAAC,eAAe,CAAC,IAAI,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;AACzE,CAAC"}
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shared OAuth 2.0 error registry.
|
|
3
|
+
*
|
|
4
|
+
* OAuth defines a fixed vocabulary of `error` codes across its specs; emitting a
|
|
5
|
+
* non-registered code (the finding in [#39]) makes a server fail conformance and
|
|
6
|
+
* confuses standards-compliant clients, which switch on these exact strings.
|
|
7
|
+
* Centralizing them here lets `@dwk/indieauth` and the eventual Solid-OIDC OP
|
|
8
|
+
* share one audited registry rather than re-spelling literals per call site.
|
|
9
|
+
*
|
|
10
|
+
* @see https://www.rfc-editor.org/rfc/rfc6749#section-5.2 (token endpoint)
|
|
11
|
+
* @see https://www.rfc-editor.org/rfc/rfc7591#section-3.2.2 (registration)
|
|
12
|
+
* @see https://www.rfc-editor.org/rfc/rfc9449#section-12 (DPoP)
|
|
13
|
+
*/
|
|
14
|
+
/** The registered OAuth 2.0 `error` codes this lib emits. */
|
|
15
|
+
export declare const OAuthError: {
|
|
16
|
+
/** The request is missing a parameter, malformed, or otherwise invalid. */
|
|
17
|
+
readonly InvalidRequest: "invalid_request";
|
|
18
|
+
/** Client authentication failed or the client is unknown. */
|
|
19
|
+
readonly InvalidClient: "invalid_client";
|
|
20
|
+
/** The grant (code, refresh token, …) is invalid, expired, or revoked. */
|
|
21
|
+
readonly InvalidGrant: "invalid_grant";
|
|
22
|
+
/** The authenticated client is not authorized to use this grant type. */
|
|
23
|
+
readonly UnauthorizedClient: "unauthorized_client";
|
|
24
|
+
/** The grant type is not supported by the authorization server. */
|
|
25
|
+
readonly UnsupportedGrantType: "unsupported_grant_type";
|
|
26
|
+
/** The requested scope is invalid, unknown, or exceeds what was granted. */
|
|
27
|
+
readonly InvalidScope: "invalid_scope";
|
|
28
|
+
/** An unexpected server-side condition prevented fulfilling the request. */
|
|
29
|
+
readonly ServerError: "server_error";
|
|
30
|
+
/** The server is temporarily overloaded or under maintenance. */
|
|
31
|
+
readonly TemporarilyUnavailable: "temporarily_unavailable";
|
|
32
|
+
/** One or more submitted client metadata fields are invalid. */
|
|
33
|
+
readonly InvalidClientMetadata: "invalid_client_metadata";
|
|
34
|
+
/** One or more `redirect_uris` are invalid. */
|
|
35
|
+
readonly InvalidRedirectUri: "invalid_redirect_uri";
|
|
36
|
+
/** The server does not support revoking the presented token type. */
|
|
37
|
+
readonly UnsupportedTokenType: "unsupported_token_type";
|
|
38
|
+
/** The `request_uri` is unknown, expired, or already consumed. */
|
|
39
|
+
readonly InvalidRequestUri: "invalid_request_uri";
|
|
40
|
+
/** The DPoP proof is missing or fails verification. */
|
|
41
|
+
readonly InvalidDpopProof: "invalid_dpop_proof";
|
|
42
|
+
};
|
|
43
|
+
/** Union of the registered error-code string literals in {@link OAuthError}. */
|
|
44
|
+
export type OAuthErrorCode = (typeof OAuthError)[keyof typeof OAuthError];
|
|
45
|
+
/**
|
|
46
|
+
* Build an OAuth 2.0 error response (RFC 6749 §5.2): a JSON body with `error`
|
|
47
|
+
* and optional `error_description`. `error` is typed to the registry so a
|
|
48
|
+
* non-registered code cannot be emitted by accident.
|
|
49
|
+
*
|
|
50
|
+
* Extra headers (e.g. `WWW-Authenticate` for a `401`) are merged in; callers
|
|
51
|
+
* MUST keep `error_description` free of secrets and untrusted echoes.
|
|
52
|
+
*/
|
|
53
|
+
export declare function oauthErrorResponse(error: OAuthErrorCode, description?: string, status?: number, headers?: Record<string, string>): Response;
|
|
54
|
+
//# sourceMappingURL=errors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,6DAA6D;AAC7D,eAAO,MAAM,UAAU;IAErB,2EAA2E;;IAE3E,6DAA6D;;IAE7D,0EAA0E;;IAE1E,yEAAyE;;IAEzE,mEAAmE;;IAEnE,4EAA4E;;IAE5E,4EAA4E;;IAE5E,iEAAiE;;IAIjE,gEAAgE;;IAEhE,+CAA+C;;IAI/C,qEAAqE;;IAIrE,kEAAkE;;IAIlE,uDAAuD;;CAE/C,CAAC;AAEX,gFAAgF;AAChF,MAAM,MAAM,cAAc,GAAG,CAAC,OAAO,UAAU,CAAC,CAAC,MAAM,OAAO,UAAU,CAAC,CAAC;AAI1E;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAChC,KAAK,EAAE,cAAc,EACrB,WAAW,CAAC,EAAE,MAAM,EACpB,MAAM,SAAM,EACZ,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAC/B,QAAQ,CAQV"}
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shared OAuth 2.0 error registry.
|
|
3
|
+
*
|
|
4
|
+
* OAuth defines a fixed vocabulary of `error` codes across its specs; emitting a
|
|
5
|
+
* non-registered code (the finding in [#39]) makes a server fail conformance and
|
|
6
|
+
* confuses standards-compliant clients, which switch on these exact strings.
|
|
7
|
+
* Centralizing them here lets `@dwk/indieauth` and the eventual Solid-OIDC OP
|
|
8
|
+
* share one audited registry rather than re-spelling literals per call site.
|
|
9
|
+
*
|
|
10
|
+
* @see https://www.rfc-editor.org/rfc/rfc6749#section-5.2 (token endpoint)
|
|
11
|
+
* @see https://www.rfc-editor.org/rfc/rfc7591#section-3.2.2 (registration)
|
|
12
|
+
* @see https://www.rfc-editor.org/rfc/rfc9449#section-12 (DPoP)
|
|
13
|
+
*/
|
|
14
|
+
/** The registered OAuth 2.0 `error` codes this lib emits. */
|
|
15
|
+
export const OAuthError = {
|
|
16
|
+
// RFC 6749 §5.2 — token endpoint / general protocol errors.
|
|
17
|
+
/** The request is missing a parameter, malformed, or otherwise invalid. */
|
|
18
|
+
InvalidRequest: "invalid_request",
|
|
19
|
+
/** Client authentication failed or the client is unknown. */
|
|
20
|
+
InvalidClient: "invalid_client",
|
|
21
|
+
/** The grant (code, refresh token, …) is invalid, expired, or revoked. */
|
|
22
|
+
InvalidGrant: "invalid_grant",
|
|
23
|
+
/** The authenticated client is not authorized to use this grant type. */
|
|
24
|
+
UnauthorizedClient: "unauthorized_client",
|
|
25
|
+
/** The grant type is not supported by the authorization server. */
|
|
26
|
+
UnsupportedGrantType: "unsupported_grant_type",
|
|
27
|
+
/** The requested scope is invalid, unknown, or exceeds what was granted. */
|
|
28
|
+
InvalidScope: "invalid_scope",
|
|
29
|
+
/** An unexpected server-side condition prevented fulfilling the request. */
|
|
30
|
+
ServerError: "server_error",
|
|
31
|
+
/** The server is temporarily overloaded or under maintenance. */
|
|
32
|
+
TemporarilyUnavailable: "temporarily_unavailable",
|
|
33
|
+
// RFC 7591 §3.2.2 — dynamic client registration.
|
|
34
|
+
/** One or more submitted client metadata fields are invalid. */
|
|
35
|
+
InvalidClientMetadata: "invalid_client_metadata",
|
|
36
|
+
/** One or more `redirect_uris` are invalid. */
|
|
37
|
+
InvalidRedirectUri: "invalid_redirect_uri",
|
|
38
|
+
// RFC 7009 §2.2.1 — revocation.
|
|
39
|
+
/** The server does not support revoking the presented token type. */
|
|
40
|
+
UnsupportedTokenType: "unsupported_token_type",
|
|
41
|
+
// RFC 9126 §2.3 — pushed authorization requests.
|
|
42
|
+
/** The `request_uri` is unknown, expired, or already consumed. */
|
|
43
|
+
InvalidRequestUri: "invalid_request_uri",
|
|
44
|
+
// RFC 9449 §5 / §12 — DPoP.
|
|
45
|
+
/** The DPoP proof is missing or fails verification. */
|
|
46
|
+
InvalidDpopProof: "invalid_dpop_proof",
|
|
47
|
+
};
|
|
48
|
+
const JSON_HEADERS = { "content-type": "application/json" };
|
|
49
|
+
/**
|
|
50
|
+
* Build an OAuth 2.0 error response (RFC 6749 §5.2): a JSON body with `error`
|
|
51
|
+
* and optional `error_description`. `error` is typed to the registry so a
|
|
52
|
+
* non-registered code cannot be emitted by accident.
|
|
53
|
+
*
|
|
54
|
+
* Extra headers (e.g. `WWW-Authenticate` for a `401`) are merged in; callers
|
|
55
|
+
* MUST keep `error_description` free of secrets and untrusted echoes.
|
|
56
|
+
*/
|
|
57
|
+
export function oauthErrorResponse(error, description, status = 400, headers) {
|
|
58
|
+
const body = description
|
|
59
|
+
? { error, error_description: description }
|
|
60
|
+
: { error };
|
|
61
|
+
return new Response(JSON.stringify(body), {
|
|
62
|
+
status,
|
|
63
|
+
headers: { ...JSON_HEADERS, ...headers },
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=errors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,6DAA6D;AAC7D,MAAM,CAAC,MAAM,UAAU,GAAG;IACxB,4DAA4D;IAC5D,2EAA2E;IAC3E,cAAc,EAAE,iBAAiB;IACjC,6DAA6D;IAC7D,aAAa,EAAE,gBAAgB;IAC/B,0EAA0E;IAC1E,YAAY,EAAE,eAAe;IAC7B,yEAAyE;IACzE,kBAAkB,EAAE,qBAAqB;IACzC,mEAAmE;IACnE,oBAAoB,EAAE,wBAAwB;IAC9C,4EAA4E;IAC5E,YAAY,EAAE,eAAe;IAC7B,4EAA4E;IAC5E,WAAW,EAAE,cAAc;IAC3B,iEAAiE;IACjE,sBAAsB,EAAE,yBAAyB;IAEjD,iDAAiD;IACjD,gEAAgE;IAChE,qBAAqB,EAAE,yBAAyB;IAChD,+CAA+C;IAC/C,kBAAkB,EAAE,sBAAsB;IAE1C,gCAAgC;IAChC,qEAAqE;IACrE,oBAAoB,EAAE,wBAAwB;IAE9C,iDAAiD;IACjD,kEAAkE;IAClE,iBAAiB,EAAE,qBAAqB;IAExC,4BAA4B;IAC5B,uDAAuD;IACvD,gBAAgB,EAAE,oBAAoB;CAC9B,CAAC;AAKX,MAAM,YAAY,GAAG,EAAE,cAAc,EAAE,kBAAkB,EAAW,CAAC;AAErE;;;;;;;GAOG;AACH,MAAM,UAAU,kBAAkB,CAChC,KAAqB,EACrB,WAAoB,EACpB,MAAM,GAAG,GAAG,EACZ,OAAgC;IAEhC,MAAM,IAAI,GAAG,WAAW;QACtB,CAAC,CAAC,EAAE,KAAK,EAAE,iBAAiB,EAAE,WAAW,EAAE;QAC3C,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC;IACd,OAAO,IAAI,QAAQ,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE;QACxC,MAAM;QACN,OAAO,EAAE,EAAE,GAAG,YAAY,EAAE,GAAG,OAAO,EAAE;KACzC,CAAC,CAAC;AACL,CAAC"}
|
package/dist/http.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Small request/response helpers shared by the four endpoint handlers. Web
|
|
3
|
+
* Fetch types only (`Request`/`Response`/`URLSearchParams`) so the handlers run
|
|
4
|
+
* and test under plain Node.
|
|
5
|
+
*/
|
|
6
|
+
/** Serialize `body` as a JSON response with the given status. */
|
|
7
|
+
export declare function json(body: unknown, status?: number): Response;
|
|
8
|
+
/** A `405 Method Not Allowed` carrying the permitted methods in `Allow`. */
|
|
9
|
+
export declare function methodNotAllowed(allow: string): Response;
|
|
10
|
+
/**
|
|
11
|
+
* Read an `application/x-www-form-urlencoded` body into `URLSearchParams`. A
|
|
12
|
+
* malformed/empty body or wrong content-type yields empty params (never throws),
|
|
13
|
+
* so the caller's own field validation reports the problem as a `400` rather
|
|
14
|
+
* than the handler crashing.
|
|
15
|
+
*/
|
|
16
|
+
export declare function readForm(request: Request): Promise<URLSearchParams>;
|
|
17
|
+
/** Parse a JSON request body, returning `undefined` if it is absent/malformed. */
|
|
18
|
+
export declare function readJson(request: Request): Promise<unknown>;
|
|
19
|
+
//# sourceMappingURL=http.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../src/http.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,iEAAiE;AACjE,wBAAgB,IAAI,CAAC,IAAI,EAAE,OAAO,EAAE,MAAM,SAAM,GAAG,QAAQ,CAK1D;AAED,4EAA4E;AAC5E,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,QAAQ,CAKxD;AAED;;;;;GAKG;AACH,wBAAsB,QAAQ,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,eAAe,CAAC,CAWzE;AAED,kFAAkF;AAClF,wBAAsB,QAAQ,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAMjE"}
|
package/dist/http.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Small request/response helpers shared by the four endpoint handlers. Web
|
|
3
|
+
* Fetch types only (`Request`/`Response`/`URLSearchParams`) so the handlers run
|
|
4
|
+
* and test under plain Node.
|
|
5
|
+
*/
|
|
6
|
+
const JSON_HEADERS = { "content-type": "application/json" };
|
|
7
|
+
/** Serialize `body` as a JSON response with the given status. */
|
|
8
|
+
export function json(body, status = 200) {
|
|
9
|
+
return new Response(JSON.stringify(body), {
|
|
10
|
+
status,
|
|
11
|
+
headers: JSON_HEADERS,
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
/** A `405 Method Not Allowed` carrying the permitted methods in `Allow`. */
|
|
15
|
+
export function methodNotAllowed(allow) {
|
|
16
|
+
return new Response("Method Not Allowed", {
|
|
17
|
+
status: 405,
|
|
18
|
+
headers: { allow },
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Read an `application/x-www-form-urlencoded` body into `URLSearchParams`. A
|
|
23
|
+
* malformed/empty body or wrong content-type yields empty params (never throws),
|
|
24
|
+
* so the caller's own field validation reports the problem as a `400` rather
|
|
25
|
+
* than the handler crashing.
|
|
26
|
+
*/
|
|
27
|
+
export async function readForm(request) {
|
|
28
|
+
const params = new URLSearchParams();
|
|
29
|
+
try {
|
|
30
|
+
const form = await request.formData();
|
|
31
|
+
for (const [key, value] of form) {
|
|
32
|
+
if (typeof value === "string")
|
|
33
|
+
params.set(key, value);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
// Intentionally swallowed — see the doc comment.
|
|
38
|
+
}
|
|
39
|
+
return params;
|
|
40
|
+
}
|
|
41
|
+
/** Parse a JSON request body, returning `undefined` if it is absent/malformed. */
|
|
42
|
+
export async function readJson(request) {
|
|
43
|
+
try {
|
|
44
|
+
return (await request.json());
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=http.js.map
|
package/dist/http.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"http.js","sourceRoot":"","sources":["../src/http.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,MAAM,YAAY,GAAG,EAAE,cAAc,EAAE,kBAAkB,EAAW,CAAC;AAErE,iEAAiE;AACjE,MAAM,UAAU,IAAI,CAAC,IAAa,EAAE,MAAM,GAAG,GAAG;IAC9C,OAAO,IAAI,QAAQ,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE;QACxC,MAAM;QACN,OAAO,EAAE,YAAY;KACtB,CAAC,CAAC;AACL,CAAC;AAED,4EAA4E;AAC5E,MAAM,UAAU,gBAAgB,CAAC,KAAa;IAC5C,OAAO,IAAI,QAAQ,CAAC,oBAAoB,EAAE;QACxC,MAAM,EAAE,GAAG;QACX,OAAO,EAAE,EAAE,KAAK,EAAE;KACnB,CAAC,CAAC;AACL,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,QAAQ,CAAC,OAAgB;IAC7C,MAAM,MAAM,GAAG,IAAI,eAAe,EAAE,CAAC;IACrC,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,MAAM,OAAO,CAAC,QAAQ,EAAE,CAAC;QACtC,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,IAAI,EAAE,CAAC;YAChC,IAAI,OAAO,KAAK,KAAK,QAAQ;gBAAE,MAAM,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;QACxD,CAAC;IACH,CAAC;IAAC,MAAM,CAAC;QACP,iDAAiD;IACnD,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,kFAAkF;AAClF,MAAM,CAAC,KAAK,UAAU,QAAQ,CAAC,OAAgB;IAC7C,IAAI,CAAC;QACH,OAAO,CAAC,MAAM,OAAO,CAAC,IAAI,EAAE,CAAY,CAAC;IAC3C,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAC;IACnB,CAAC;AACH,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@dwk/oauth` — OAuth 2.0 authorization-server building blocks.
|
|
3
|
+
*
|
|
4
|
+
* A cross-standard reusable lib (like `@dwk/dpop`, `@dwk/log`): it provides the
|
|
5
|
+
* shared OAuth 2.0 *server* primitives that `@dwk/indieauth` already partly
|
|
6
|
+
* implements and that the eventual Solid-OIDC OP will need, so they share one
|
|
7
|
+
* audited implementation rather than diverging. It owns the protocol mechanics
|
|
8
|
+
* of four stateful endpoints plus the metadata document and the error registry;
|
|
9
|
+
* it stays **protocol-agnostic** (no IndieWeb/Solid claim handling) and
|
|
10
|
+
* **runtime-agnostic** (plain-data storage seams, Web Fetch + Web Crypto only),
|
|
11
|
+
* so it unit-tests under plain Node without a Workers runtime.
|
|
12
|
+
*
|
|
13
|
+
* What it provides:
|
|
14
|
+
* - **RFC 8414** authorization-server metadata document (config → JSON), the
|
|
15
|
+
* single source of truth shared with the static document Anglesite publishes.
|
|
16
|
+
* - **RFC 7662** token introspection (protected; derives `active`, maps claims,
|
|
17
|
+
* surfaces DPoP `cnf.jkt`).
|
|
18
|
+
* - **RFC 7009** token revocation (idempotent, always `200`).
|
|
19
|
+
* - **RFC 9126** pushed authorization requests (single-use `request_uri`,
|
|
20
|
+
* optional DPoP binding via `@dwk/dpop`).
|
|
21
|
+
* - **RFC 7591** dynamic client registration (strict metadata validation).
|
|
22
|
+
* - A shared, registered **OAuth error registry**.
|
|
23
|
+
*
|
|
24
|
+
* Storage is never owned here: token/client/pushed-request records flow through
|
|
25
|
+
* the plain-data seams in {@link module:store}, which the consuming endpoint
|
|
26
|
+
* package backs with a strongly-consistent store (D1/DO via `@dwk/store`) —
|
|
27
|
+
* never KV.
|
|
28
|
+
*
|
|
29
|
+
* @see spec/packages/oauth.md
|
|
30
|
+
* @packageDocumentation
|
|
31
|
+
*/
|
|
32
|
+
export { OAuthError, oauthErrorResponse, type OAuthErrorCode } from "./errors";
|
|
33
|
+
export { buildAuthorizationServerMetadata, type AuthorizationServerMetadata, type AuthorizationServerMetadataConfig, } from "./metadata";
|
|
34
|
+
export { createIntrospectionHandler, isTokenActive, buildIntrospectionResponse, type IntrospectionConfig, type IntrospectionResponse, type EndpointAuthenticator, } from "./introspection";
|
|
35
|
+
export { createRevocationHandler, type RevocationConfig } from "./revocation";
|
|
36
|
+
export { createPushedAuthorizationRequestHandler, parseRequestUri, requestUriFor, PUSHED_REQUEST_URI_PREFIX, type PushedAuthorizationRequestConfig, } from "./par";
|
|
37
|
+
export { createClientRegistrationHandler, validateClientMetadata, type ClientRegistrationConfig, } from "./registration";
|
|
38
|
+
export type { IntrospectionTokenRecord, PushedRequestRecord, PushedAuthorizationStore, ClientRecord, } from "./store";
|
|
39
|
+
export { OAuthLogEvent } from "./log";
|
|
40
|
+
export type { ObservabilityConfig } from "./observability";
|
|
41
|
+
export type { Logger, Metrics } from "@dwk/log";
|
|
42
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAEH,OAAO,EAAE,UAAU,EAAE,kBAAkB,EAAE,KAAK,cAAc,EAAE,MAAM,UAAU,CAAC;AAE/E,OAAO,EACL,gCAAgC,EAChC,KAAK,2BAA2B,EAChC,KAAK,iCAAiC,GACvC,MAAM,YAAY,CAAC;AAEpB,OAAO,EACL,0BAA0B,EAC1B,aAAa,EACb,0BAA0B,EAC1B,KAAK,mBAAmB,EACxB,KAAK,qBAAqB,EAC1B,KAAK,qBAAqB,GAC3B,MAAM,iBAAiB,CAAC;AAEzB,OAAO,EAAE,uBAAuB,EAAE,KAAK,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAE9E,OAAO,EACL,uCAAuC,EACvC,eAAe,EACf,aAAa,EACb,yBAAyB,EACzB,KAAK,gCAAgC,GACtC,MAAM,OAAO,CAAC;AAEf,OAAO,EACL,+BAA+B,EAC/B,sBAAsB,EACtB,KAAK,wBAAwB,GAC9B,MAAM,gBAAgB,CAAC;AAExB,YAAY,EACV,wBAAwB,EACxB,mBAAmB,EACnB,wBAAwB,EACxB,YAAY,GACb,MAAM,SAAS,CAAC;AAEjB,OAAO,EAAE,aAAa,EAAE,MAAM,OAAO,CAAC;AACtC,YAAY,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AAC3D,YAAY,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@dwk/oauth` — OAuth 2.0 authorization-server building blocks.
|
|
3
|
+
*
|
|
4
|
+
* A cross-standard reusable lib (like `@dwk/dpop`, `@dwk/log`): it provides the
|
|
5
|
+
* shared OAuth 2.0 *server* primitives that `@dwk/indieauth` already partly
|
|
6
|
+
* implements and that the eventual Solid-OIDC OP will need, so they share one
|
|
7
|
+
* audited implementation rather than diverging. It owns the protocol mechanics
|
|
8
|
+
* of four stateful endpoints plus the metadata document and the error registry;
|
|
9
|
+
* it stays **protocol-agnostic** (no IndieWeb/Solid claim handling) and
|
|
10
|
+
* **runtime-agnostic** (plain-data storage seams, Web Fetch + Web Crypto only),
|
|
11
|
+
* so it unit-tests under plain Node without a Workers runtime.
|
|
12
|
+
*
|
|
13
|
+
* What it provides:
|
|
14
|
+
* - **RFC 8414** authorization-server metadata document (config → JSON), the
|
|
15
|
+
* single source of truth shared with the static document Anglesite publishes.
|
|
16
|
+
* - **RFC 7662** token introspection (protected; derives `active`, maps claims,
|
|
17
|
+
* surfaces DPoP `cnf.jkt`).
|
|
18
|
+
* - **RFC 7009** token revocation (idempotent, always `200`).
|
|
19
|
+
* - **RFC 9126** pushed authorization requests (single-use `request_uri`,
|
|
20
|
+
* optional DPoP binding via `@dwk/dpop`).
|
|
21
|
+
* - **RFC 7591** dynamic client registration (strict metadata validation).
|
|
22
|
+
* - A shared, registered **OAuth error registry**.
|
|
23
|
+
*
|
|
24
|
+
* Storage is never owned here: token/client/pushed-request records flow through
|
|
25
|
+
* the plain-data seams in {@link module:store}, which the consuming endpoint
|
|
26
|
+
* package backs with a strongly-consistent store (D1/DO via `@dwk/store`) —
|
|
27
|
+
* never KV.
|
|
28
|
+
*
|
|
29
|
+
* @see spec/packages/oauth.md
|
|
30
|
+
* @packageDocumentation
|
|
31
|
+
*/
|
|
32
|
+
export { OAuthError, oauthErrorResponse } from "./errors";
|
|
33
|
+
export { buildAuthorizationServerMetadata, } from "./metadata";
|
|
34
|
+
export { createIntrospectionHandler, isTokenActive, buildIntrospectionResponse, } from "./introspection";
|
|
35
|
+
export { createRevocationHandler } from "./revocation";
|
|
36
|
+
export { createPushedAuthorizationRequestHandler, parseRequestUri, requestUriFor, PUSHED_REQUEST_URI_PREFIX, } from "./par";
|
|
37
|
+
export { createClientRegistrationHandler, validateClientMetadata, } from "./registration";
|
|
38
|
+
export { OAuthLogEvent } from "./log";
|
|
39
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAEH,OAAO,EAAE,UAAU,EAAE,kBAAkB,EAAuB,MAAM,UAAU,CAAC;AAE/E,OAAO,EACL,gCAAgC,GAGjC,MAAM,YAAY,CAAC;AAEpB,OAAO,EACL,0BAA0B,EAC1B,aAAa,EACb,0BAA0B,GAI3B,MAAM,iBAAiB,CAAC;AAEzB,OAAO,EAAE,uBAAuB,EAAyB,MAAM,cAAc,CAAC;AAE9E,OAAO,EACL,uCAAuC,EACvC,eAAe,EACf,aAAa,EACb,yBAAyB,GAE1B,MAAM,OAAO,CAAC;AAEf,OAAO,EACL,+BAA+B,EAC/B,sBAAsB,GAEvB,MAAM,gBAAgB,CAAC;AASxB,OAAO,EAAE,aAAa,EAAE,MAAM,OAAO,CAAC"}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OAuth 2.0 Token Introspection (RFC 7662).
|
|
3
|
+
*
|
|
4
|
+
* A protected POST endpoint a Resource Server queries to learn whether a token
|
|
5
|
+
* is currently active and, if so, its metadata. The endpoint **MUST** be
|
|
6
|
+
* protected (RFC 7662 §2.1) so it cannot be used as a token-scanning oracle —
|
|
7
|
+
* hence {@link IntrospectionConfig.authenticate} is required, not optional.
|
|
8
|
+
*
|
|
9
|
+
* The lib owns only the protocol mechanics: it asks the caller to look the token
|
|
10
|
+
* up (storage stays in the consuming package, see `./store`), derives the
|
|
11
|
+
* `active` flag, and maps the record to the snake_case response. A DPoP-bound
|
|
12
|
+
* token surfaces its key thumbprint as `cnf: { jkt }` so the RS can complete the
|
|
13
|
+
* proof-of-possession binding via `@dwk/dpop`.
|
|
14
|
+
*
|
|
15
|
+
* @see https://www.rfc-editor.org/rfc/rfc7662
|
|
16
|
+
*/
|
|
17
|
+
import { type ObservabilityConfig } from "./observability";
|
|
18
|
+
import type { IntrospectionTokenRecord } from "./store";
|
|
19
|
+
/**
|
|
20
|
+
* Authenticates the calling Resource Server / client at a protected endpoint.
|
|
21
|
+
*
|
|
22
|
+
* Receives the request and, when the handler could extract one from the body,
|
|
23
|
+
* the requested `client_id`. The handler passes a **pre-parse clone** of the
|
|
24
|
+
* request, so an authenticator that itself reads the body (e.g. a
|
|
25
|
+
* `client_secret_post` credential, or matching the authenticated client against
|
|
26
|
+
* `clientId` per RFC 9126 §2.1) does not disturb the handler's own parse.
|
|
27
|
+
*/
|
|
28
|
+
export type EndpointAuthenticator = (request: Request, clientId?: string) => boolean | Promise<boolean>;
|
|
29
|
+
/** Configuration for {@link createIntrospectionHandler}. */
|
|
30
|
+
export interface IntrospectionConfig extends ObservabilityConfig {
|
|
31
|
+
/**
|
|
32
|
+
* Look up the presented token, returning the record to introspect or `null`
|
|
33
|
+
* if it is unknown. The optional `tokenTypeHint` is the client's
|
|
34
|
+
* `token_type_hint` (RFC 7662 §2.1) — a non-binding optimization the store may
|
|
35
|
+
* ignore. Storage is the consuming package's concern; this stays plain-data.
|
|
36
|
+
*/
|
|
37
|
+
readonly lookupToken: (token: string, tokenTypeHint?: string) => Promise<IntrospectionTokenRecord | null>;
|
|
38
|
+
/**
|
|
39
|
+
* Authenticate the caller. **Required** — an unprotected introspection
|
|
40
|
+
* endpoint is a token-scanning oracle (RFC 7662 §2.1). Return `false` to
|
|
41
|
+
* reject with `401 invalid_client`.
|
|
42
|
+
*/
|
|
43
|
+
readonly authenticate: EndpointAuthenticator;
|
|
44
|
+
/** Current time (seconds since the epoch). Defaults to `Date.now()`. */
|
|
45
|
+
readonly now?: () => number;
|
|
46
|
+
}
|
|
47
|
+
/** The JSON shape of an introspection response (RFC 7662 §2.2). */
|
|
48
|
+
export interface IntrospectionResponse {
|
|
49
|
+
readonly active: boolean;
|
|
50
|
+
readonly scope?: string;
|
|
51
|
+
readonly client_id?: string;
|
|
52
|
+
readonly username?: string;
|
|
53
|
+
readonly token_type?: string;
|
|
54
|
+
readonly exp?: number;
|
|
55
|
+
readonly iat?: number;
|
|
56
|
+
readonly nbf?: number;
|
|
57
|
+
readonly sub?: string;
|
|
58
|
+
readonly aud?: string | readonly string[];
|
|
59
|
+
readonly iss?: string;
|
|
60
|
+
readonly jti?: string;
|
|
61
|
+
readonly cnf?: {
|
|
62
|
+
readonly jkt: string;
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Whether `record` represents a currently-active token at `now`. A record is
|
|
67
|
+
* active unless it is `null`, explicitly `active: false`, revoked, past its
|
|
68
|
+
* `expiresAt`, or before its `notBefore`. Pure and side-effect-free.
|
|
69
|
+
*/
|
|
70
|
+
export declare function isTokenActive(record: IntrospectionTokenRecord | null, now: number): boolean;
|
|
71
|
+
/**
|
|
72
|
+
* Map an active token record to its RFC 7662 response, emitting only members the
|
|
73
|
+
* record actually carries (so the response never advertises empty claims). The
|
|
74
|
+
* caller MUST have already established the token is active.
|
|
75
|
+
*/
|
|
76
|
+
export declare function buildIntrospectionResponse(record: IntrospectionTokenRecord): IntrospectionResponse;
|
|
77
|
+
/**
|
|
78
|
+
* Create the introspection endpoint handler. The returned handler accepts a
|
|
79
|
+
* `POST` request and returns the introspection JSON; route it at whatever path
|
|
80
|
+
* the deployer mounts (it is path-agnostic).
|
|
81
|
+
*/
|
|
82
|
+
export declare function createIntrospectionHandler(config: IntrospectionConfig): (request: Request) => Promise<Response>;
|
|
83
|
+
//# sourceMappingURL=introspection.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"introspection.d.ts","sourceRoot":"","sources":["../src/introspection.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAOH,OAAO,EAGL,KAAK,mBAAmB,EACzB,MAAM,iBAAiB,CAAC;AACzB,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,SAAS,CAAC;AAExD;;;;;;;;GAQG;AACH,MAAM,MAAM,qBAAqB,GAAG,CAClC,OAAO,EAAE,OAAO,EAChB,QAAQ,CAAC,EAAE,MAAM,KACd,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;AAEhC,4DAA4D;AAC5D,MAAM,WAAW,mBAAoB,SAAQ,mBAAmB;IAC9D;;;;;OAKG;IACH,QAAQ,CAAC,WAAW,EAAE,CACpB,KAAK,EAAE,MAAM,EACb,aAAa,CAAC,EAAE,MAAM,KACnB,OAAO,CAAC,wBAAwB,GAAG,IAAI,CAAC,CAAC;IAC9C;;;;OAIG;IACH,QAAQ,CAAC,YAAY,EAAE,qBAAqB,CAAC;IAC7C,wEAAwE;IACxE,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CAC7B;AAED,mEAAmE;AACnE,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,CAAC;IAC1C,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,GAAG,CAAC,EAAE;QAAE,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC;CACzC;AAKD;;;;GAIG;AACH,wBAAgB,aAAa,CAC3B,MAAM,EAAE,wBAAwB,GAAG,IAAI,EACvC,GAAG,EAAE,MAAM,GACV,OAAO,CAOT;AAED;;;;GAIG;AACH,wBAAgB,0BAA0B,CACxC,MAAM,EAAE,wBAAwB,GAC/B,qBAAqB,CAevB;AAED;;;;GAIG;AACH,wBAAgB,0BAA0B,CACxC,MAAM,EAAE,mBAAmB,GAC1B,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,CAmDzC"}
|