@boopkit/cap-oauth-server 0.1.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/capability.json +120 -0
- package/dist/authorization-code-service.d.ts +105 -0
- package/dist/authorization-code-service.d.ts.map +1 -0
- package/dist/authorization-code-service.js +142 -0
- package/dist/authorization-code-service.js.map +1 -0
- package/dist/bearer.d.ts +146 -0
- package/dist/bearer.d.ts.map +1 -0
- package/dist/bearer.js +150 -0
- package/dist/bearer.js.map +1 -0
- package/dist/body-field.d.ts +24 -0
- package/dist/body-field.d.ts.map +1 -0
- package/dist/body-field.js +43 -0
- package/dist/body-field.js.map +1 -0
- package/dist/client-registry-service.d.ts +120 -0
- package/dist/client-registry-service.d.ts.map +1 -0
- package/dist/client-registry-service.js +450 -0
- package/dist/client-registry-service.js.map +1 -0
- package/dist/config.d.ts +82 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +157 -0
- package/dist/config.js.map +1 -0
- package/dist/connections-controller.d.ts +106 -0
- package/dist/connections-controller.d.ts.map +1 -0
- package/dist/connections-controller.js +161 -0
- package/dist/connections-controller.js.map +1 -0
- package/dist/consent-controller.d.ts +197 -0
- package/dist/consent-controller.d.ts.map +1 -0
- package/dist/consent-controller.js +532 -0
- package/dist/consent-controller.js.map +1 -0
- package/dist/discovery-controller.d.ts +16 -0
- package/dist/discovery-controller.d.ts.map +1 -0
- package/dist/discovery-controller.js +52 -0
- package/dist/discovery-controller.js.map +1 -0
- package/dist/discovery.d.ts +95 -0
- package/dist/discovery.d.ts.map +1 -0
- package/dist/discovery.js +101 -0
- package/dist/discovery.js.map +1 -0
- package/dist/errors.d.ts +26 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +41 -0
- package/dist/errors.js.map +1 -0
- package/dist/express.d.ts +66 -0
- package/dist/express.d.ts.map +1 -0
- package/dist/express.js +3 -0
- package/dist/express.js.map +1 -0
- package/dist/grant-service.d.ts +102 -0
- package/dist/grant-service.d.ts.map +1 -0
- package/dist/grant-service.js +137 -0
- package/dist/grant-service.js.map +1 -0
- package/dist/index.d.ts +67 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +148 -0
- package/dist/index.js.map +1 -0
- package/dist/metadata-document.d.ts +60 -0
- package/dist/metadata-document.d.ts.map +1 -0
- package/dist/metadata-document.js +148 -0
- package/dist/metadata-document.js.map +1 -0
- package/dist/oauth-error.d.ts +45 -0
- package/dist/oauth-error.d.ts.map +1 -0
- package/dist/oauth-error.js +51 -0
- package/dist/oauth-error.js.map +1 -0
- package/dist/pkce.d.ts +29 -0
- package/dist/pkce.d.ts.map +1 -0
- package/dist/pkce.js +65 -0
- package/dist/pkce.js.map +1 -0
- package/dist/property-array.d.ts +60 -0
- package/dist/property-array.d.ts.map +1 -0
- package/dist/property-array.js +31 -0
- package/dist/property-array.js.map +1 -0
- package/dist/public-id.d.ts +35 -0
- package/dist/public-id.d.ts.map +1 -0
- package/dist/public-id.js +76 -0
- package/dist/public-id.js.map +1 -0
- package/dist/rate-limit.d.ts +56 -0
- package/dist/rate-limit.d.ts.map +1 -0
- package/dist/rate-limit.js +55 -0
- package/dist/rate-limit.js.map +1 -0
- package/dist/redirect-uri.d.ts +89 -0
- package/dist/redirect-uri.d.ts.map +1 -0
- package/dist/redirect-uri.js +249 -0
- package/dist/redirect-uri.js.map +1 -0
- package/dist/registration-controller.d.ts +62 -0
- package/dist/registration-controller.d.ts.map +1 -0
- package/dist/registration-controller.js +79 -0
- package/dist/registration-controller.js.map +1 -0
- package/dist/request-client.d.ts +19 -0
- package/dist/request-client.d.ts.map +1 -0
- package/dist/request-client.js +32 -0
- package/dist/request-client.js.map +1 -0
- package/dist/revocation-controller.d.ts +28 -0
- package/dist/revocation-controller.d.ts.map +1 -0
- package/dist/revocation-controller.js +176 -0
- package/dist/revocation-controller.js.map +1 -0
- package/dist/revocation-service.d.ts +113 -0
- package/dist/revocation-service.d.ts.map +1 -0
- package/dist/revocation-service.js +121 -0
- package/dist/revocation-service.js.map +1 -0
- package/dist/token-controller.d.ts +79 -0
- package/dist/token-controller.d.ts.map +1 -0
- package/dist/token-controller.js +207 -0
- package/dist/token-controller.js.map +1 -0
- package/dist/token-service.d.ts +175 -0
- package/dist/token-service.d.ts.map +1 -0
- package/dist/token-service.js +251 -0
- package/dist/token-service.js.map +1 -0
- package/dist/types.d.ts +130 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +13 -0
- package/dist/types.js.map +1 -0
- package/dist/user-store.d.ts +110 -0
- package/dist/user-store.d.ts.map +1 -0
- package/dist/user-store.js +40 -0
- package/dist/user-store.js.map +1 -0
- package/package.json +37 -0
- package/scaffold/prisma/schema/oauth-clients.prisma +80 -0
- package/scaffold/reference/oauth-server.ref.md +478 -0
- package/scaffold/src/middleware/oauth-server.ts +227 -0
- package/scaffold/src/scripts/seed-oauth-clients.ts +72 -0
- package/scaffold/src/services/oauth-server.service.ts +101 -0
- package/scaffold/styles/oauth-server.css +345 -0
- package/scaffold/views/oauth-connected-clients.hbs +98 -0
- package/scaffold/views/oauth-consent-error.hbs +22 -0
- package/scaffold/views/oauth-consent.hbs +138 -0
- package/scaffold/views/oauth-disconnect-confirm.hbs +57 -0
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Redirect-URI policy: what may be registered, and what a registered entry
|
|
4
|
+
* accepts at the authorization endpoint.
|
|
5
|
+
*
|
|
6
|
+
* Both questions are answered from one classifier, `classifyRedirectUri`,
|
|
7
|
+
* because the two halves only work as a pair. A shape registration stores but
|
|
8
|
+
* the matcher will not parse is worse than a shape neither accepts: the client
|
|
9
|
+
* registers, believes it is connected, and then fails at the authorization
|
|
10
|
+
* endpoint with `redirect_uri is not registered`. Keeping the rule in one
|
|
11
|
+
* function is what makes that disagreement impossible rather than unlikely.
|
|
12
|
+
*
|
|
13
|
+
* ## The three shapes
|
|
14
|
+
*
|
|
15
|
+
* RFC 8252 (OAuth 2.0 for Native Apps) names exactly three ways a client may be
|
|
16
|
+
* called back, and this server accepts those three and nothing else:
|
|
17
|
+
*
|
|
18
|
+
* `https` an absolute https URL — §7.2, the claimed-https redirect
|
|
19
|
+
* a hosted connector such as Claude's web app uses.
|
|
20
|
+
* `loopback-http` `http` on `localhost` or `127.0.0.1` — §7.3, because a
|
|
21
|
+
* native app cannot terminate TLS on the loopback interface.
|
|
22
|
+
* `private-use` a hierarchical URI in an application's own scheme, such as
|
|
23
|
+
* `com.example.app:/oauth2redirect/cb` or
|
|
24
|
+
* `claude://oauth/callback` — §7.1, how a desktop or mobile
|
|
25
|
+
* app is handed the response by its operating system.
|
|
26
|
+
*
|
|
27
|
+
* ## The matching rule
|
|
28
|
+
*
|
|
29
|
+
* **Exact string match**, with exactly one exception: a registered
|
|
30
|
+
* `loopback-http` entry matches a presented URI that differs only in port. That
|
|
31
|
+
* is RFC 8252 §7.3, and it is what Claude Code's published Client ID Metadata
|
|
32
|
+
* Document requires — it binds an ephemeral loopback port at connect time, so
|
|
33
|
+
* the port it will present is not knowable at registration.
|
|
34
|
+
*
|
|
35
|
+
* The exception is scoped to `loopback-http` and not merely to a loopback
|
|
36
|
+
* *host*. A private-use scheme has no TCP port to be ephemeral: the operating
|
|
37
|
+
* system routes it by scheme name, so `myapp://localhost/cb` gets the plain
|
|
38
|
+
* exact rule even though its host happens to read as loopback.
|
|
39
|
+
*
|
|
40
|
+
* The exception lives here, in one function with its own test table, rather
|
|
41
|
+
* than being threaded through the authorization controller as a special case.
|
|
42
|
+
* A redirect-URI check that is spread across a controller is how the umbrella
|
|
43
|
+
* spec's "declared-but-unenforced redirect URI lists" defect happens: the list
|
|
44
|
+
* is stored, the comparison drifts, and nothing says so.
|
|
45
|
+
*
|
|
46
|
+
* Nothing here reads a database or a request. It compares two strings.
|
|
47
|
+
*/
|
|
48
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
49
|
+
exports.classifyRedirectUri = classifyRedirectUri;
|
|
50
|
+
exports.matchRedirectUri = matchRedirectUri;
|
|
51
|
+
exports.matchesAnyRedirectUri = matchesAnyRedirectUri;
|
|
52
|
+
exports.isLoopbackHost = isLoopbackHost;
|
|
53
|
+
/** Hosts RFC 8252 §7.3 lets a native app bind an ephemeral port on. */
|
|
54
|
+
const LOOPBACK_HOSTS = new Set(['localhost', '127.0.0.1']);
|
|
55
|
+
/**
|
|
56
|
+
* Schemes a private-use redirect may not claim, on three separate grounds.
|
|
57
|
+
*
|
|
58
|
+
* **The WHATWG URL standard's "special" schemes** — `http`, `https`, `ws`,
|
|
59
|
+
* `wss`, `ftp`, `file`. These are network and filesystem locations rather than
|
|
60
|
+
* application callbacks, and `file:///etc/…` in particular parses into a
|
|
61
|
+
* perfectly well-formed hierarchical URL, so the hierarchy rule alone would
|
|
62
|
+
* admit it. `http` and `https` never reach this list today, because the branches
|
|
63
|
+
* above answer them first; they are named anyway so that reordering those
|
|
64
|
+
* branches cannot quietly re-offer `http://evil.example/cb` — a shape already
|
|
65
|
+
* refused as a non-loopback redirect — as a private-use URI.
|
|
66
|
+
*
|
|
67
|
+
* **Schemes whose payload is content or code rather than a location** —
|
|
68
|
+
* `javascript`, `vbscript`, `data`, `blob`, `about`, `filesystem`,
|
|
69
|
+
* `view-source`. A redirect URI is somewhere a browser is *sent*. Sending it to
|
|
70
|
+
* one of these runs script or renders attacker-supplied content in the
|
|
71
|
+
* response, which is the open-redirect defect with a cross-site scripting bug
|
|
72
|
+
* on top. The hierarchy rule below already refuses the usual spellings —
|
|
73
|
+
* `javascript:alert(1)` has an opaque path — but `javascript:/alert(1)` does
|
|
74
|
+
* not, and a security boundary should not rest on a URL-parsing detail.
|
|
75
|
+
*
|
|
76
|
+
* **Privileged browser origins** — `chrome`, `chrome-extension`,
|
|
77
|
+
* `moz-extension`, `resource`. Never a legitimate OAuth callback, and a
|
|
78
|
+
* redirect into one is a request to run inside a context this server has no
|
|
79
|
+
* business reaching.
|
|
80
|
+
*/
|
|
81
|
+
const RESERVED_SCHEMES = new Set([
|
|
82
|
+
// Special schemes (WHATWG URL).
|
|
83
|
+
'http',
|
|
84
|
+
'https',
|
|
85
|
+
'ws',
|
|
86
|
+
'wss',
|
|
87
|
+
'ftp',
|
|
88
|
+
'file',
|
|
89
|
+
// Content and code.
|
|
90
|
+
'javascript',
|
|
91
|
+
'vbscript',
|
|
92
|
+
'data',
|
|
93
|
+
'blob',
|
|
94
|
+
'about',
|
|
95
|
+
'filesystem',
|
|
96
|
+
'view-source',
|
|
97
|
+
// Privileged browser origins.
|
|
98
|
+
'chrome',
|
|
99
|
+
'chrome-extension',
|
|
100
|
+
'moz-extension',
|
|
101
|
+
'resource',
|
|
102
|
+
]);
|
|
103
|
+
const SHAPE_REASON = 'must be https, http on a loopback host, or a private-use scheme (RFC 8252 §7.1)';
|
|
104
|
+
const HIERARCHY_REASON = 'uses a private-use scheme but is not a hierarchical URI, as in com.example.app:/callback';
|
|
105
|
+
const refuse = (reason) => ({ accepted: false, reason });
|
|
106
|
+
/**
|
|
107
|
+
* Is this URI hierarchical — does anything after the scheme start with `/`?
|
|
108
|
+
*
|
|
109
|
+
* This is the structural line between a redirect *location* and a URI whose
|
|
110
|
+
* body is an opaque payload. `claude://oauth/callback` and
|
|
111
|
+
* `com.example.app:/oauth2redirect/cb` are hierarchical;
|
|
112
|
+
* `urn:ietf:wg:oauth:2.0:oob`, `mailto:a@b.example` and `javascript:alert(1)`
|
|
113
|
+
* are not, and none of those names a place an operating system can deliver an
|
|
114
|
+
* authorization response to.
|
|
115
|
+
*
|
|
116
|
+
* Read off the serialized `href` rather than `pathname`, because a private-use
|
|
117
|
+
* URI may carry its target in the authority (`myapp://callback`, empty path) or
|
|
118
|
+
* in the path (`com.example.app:/callback`, empty host), and only the
|
|
119
|
+
* serialization sees both.
|
|
120
|
+
*/
|
|
121
|
+
function isHierarchical(url) {
|
|
122
|
+
return url.href.slice(url.protocol.length).startsWith('/');
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Classify one redirect URI, or say why it is refused.
|
|
126
|
+
*
|
|
127
|
+
* The single source of the policy. `validateRedirectUris` in the client
|
|
128
|
+
* registry calls it to decide whether a URI may be **stored**; `matchRedirectUri`
|
|
129
|
+
* below calls it to decide whether a stored or presented URI may be
|
|
130
|
+
* **compared**.
|
|
131
|
+
*/
|
|
132
|
+
function classifyRedirectUri(value) {
|
|
133
|
+
if (typeof value !== 'string' || value === '')
|
|
134
|
+
return refuse('is not an absolute URI');
|
|
135
|
+
let url;
|
|
136
|
+
try {
|
|
137
|
+
url = new URL(value);
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
return refuse('is not an absolute URI');
|
|
141
|
+
}
|
|
142
|
+
// RFC 6749 §3.1.2 forbids a fragment on a redirection endpoint, and the
|
|
143
|
+
// fragment is where an implicit-style authorization response's own parameters
|
|
144
|
+
// would land.
|
|
145
|
+
if (url.hash !== '')
|
|
146
|
+
return refuse('must not carry a fragment');
|
|
147
|
+
// `url.protocol` is `scheme:`, already lowercased and already known to match
|
|
148
|
+
// RFC 3986 scheme syntax — `new URL` would have thrown otherwise.
|
|
149
|
+
const scheme = url.protocol.slice(0, -1);
|
|
150
|
+
if (scheme === 'https') {
|
|
151
|
+
if (!url.hostname)
|
|
152
|
+
return refuse(SHAPE_REASON);
|
|
153
|
+
return { accepted: true, shape: 'https', url };
|
|
154
|
+
}
|
|
155
|
+
if (scheme === 'http') {
|
|
156
|
+
if (!isLoopbackHost(url.hostname))
|
|
157
|
+
return refuse(SHAPE_REASON);
|
|
158
|
+
return { accepted: true, shape: 'loopback-http', url };
|
|
159
|
+
}
|
|
160
|
+
// Everything remaining is a candidate private-use scheme (RFC 8252 §7.1).
|
|
161
|
+
// A one-character scheme is refused because `c:/Users/…` parses as scheme `c`
|
|
162
|
+
// with path `/Users/…`, and a Windows path is not a callback.
|
|
163
|
+
if (scheme.length < 2 || RESERVED_SCHEMES.has(scheme))
|
|
164
|
+
return refuse(SHAPE_REASON);
|
|
165
|
+
if (!isHierarchical(url))
|
|
166
|
+
return refuse(HIERARCHY_REASON);
|
|
167
|
+
// `claude://` parses, is hierarchical, and names nowhere at all.
|
|
168
|
+
if (!url.hostname && !url.pathname)
|
|
169
|
+
return refuse(HIERARCHY_REASON);
|
|
170
|
+
return { accepted: true, shape: 'private-use', url };
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Parse, or refuse.
|
|
174
|
+
*
|
|
175
|
+
* A registry row that is not an acceptable redirect URI is a registration bug
|
|
176
|
+
* rather than a match question, and `matchRedirectUri` answers it the same way
|
|
177
|
+
* it answers a hostile input: no. Routing this through the same classifier
|
|
178
|
+
* registration uses means a row that should never have been written cannot be
|
|
179
|
+
* matched even by an exact string comparison, which is the case that would
|
|
180
|
+
* otherwise sail through the fast path below.
|
|
181
|
+
*/
|
|
182
|
+
function parse(value) {
|
|
183
|
+
const verdict = classifyRedirectUri(value);
|
|
184
|
+
return verdict.accepted ? { shape: verdict.shape, url: verdict.url } : undefined;
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Does `presented` satisfy the registered entry `registered`?
|
|
188
|
+
*
|
|
189
|
+
* @param registered one entry from the client's `redirect_uris`
|
|
190
|
+
* @param presented the `redirect_uri` the authorization request carried
|
|
191
|
+
*/
|
|
192
|
+
function matchRedirectUri(registered, presented) {
|
|
193
|
+
const registeredUri = parse(registered);
|
|
194
|
+
const presentedUri = parse(presented);
|
|
195
|
+
// Both must be acceptable redirect URIs before either the fast path or the
|
|
196
|
+
// exception applies. Checking this *before* the string comparison is
|
|
197
|
+
// deliberate: two identical strings that are not acceptable redirect URIs —
|
|
198
|
+
// `javascript:alert(1)` against itself, say — must not match, because a row
|
|
199
|
+
// like that should never have been written and matching it would hide the
|
|
200
|
+
// fact that it was.
|
|
201
|
+
if (!registeredUri || !presentedUri)
|
|
202
|
+
return false;
|
|
203
|
+
// The rule. Everything below is the one exception to it.
|
|
204
|
+
if (registered === presented)
|
|
205
|
+
return true;
|
|
206
|
+
// The exception is about an ephemeral *TCP port*, and a private-use scheme
|
|
207
|
+
// has none: the operating system routes it by scheme name. So
|
|
208
|
+
// `myapp://localhost/cb` is matched exactly, loopback-looking authority or
|
|
209
|
+
// not. Written as a refusal of the new shape rather than as a requirement of
|
|
210
|
+
// `loopback-http`, because keying it on `loopback-http` would also drop the
|
|
211
|
+
// relaxation `https://localhost/cb` gets today — a narrowing this change has
|
|
212
|
+
// no business making on its way past.
|
|
213
|
+
if (registeredUri.shape === 'private-use')
|
|
214
|
+
return false;
|
|
215
|
+
const registeredUrl = registeredUri.url;
|
|
216
|
+
const presentedUrl = presentedUri.url;
|
|
217
|
+
if (!isLoopbackHost(registeredUrl.hostname))
|
|
218
|
+
return false;
|
|
219
|
+
// The intent to use an ephemeral port is declared by the *registration*. A
|
|
220
|
+
// registration for a public host is not relaxed because the presented value
|
|
221
|
+
// happens to be a loopback address, so the hosts must agree — and `localhost`
|
|
222
|
+
// and `127.0.0.1` are two different hosts, since RFC 8252 relaxes the port
|
|
223
|
+
// rather than name resolution.
|
|
224
|
+
if (registeredUrl.hostname !== presentedUrl.hostname)
|
|
225
|
+
return false;
|
|
226
|
+
// Only the port may differ. Everything else is compared through the parsed
|
|
227
|
+
// URL rather than the raw string, because at this point the two strings are
|
|
228
|
+
// already known to differ and the port is what is allowed to account for it.
|
|
229
|
+
return (registeredUrl.protocol === presentedUrl.protocol &&
|
|
230
|
+
registeredUrl.username === presentedUrl.username &&
|
|
231
|
+
registeredUrl.password === presentedUrl.password &&
|
|
232
|
+
registeredUrl.pathname === presentedUrl.pathname &&
|
|
233
|
+
registeredUrl.search === presentedUrl.search &&
|
|
234
|
+
registeredUrl.hash === presentedUrl.hash);
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Does any registered entry accept the presented URI?
|
|
238
|
+
*
|
|
239
|
+
* The authorization controller's actual question, kept next to the rule it is
|
|
240
|
+
* built from so a caller never has to write the loop itself.
|
|
241
|
+
*/
|
|
242
|
+
function matchesAnyRedirectUri(registered, presented) {
|
|
243
|
+
return registered.some((entry) => matchRedirectUri(entry, presented));
|
|
244
|
+
}
|
|
245
|
+
/** Is this host one the loopback exception applies to? */
|
|
246
|
+
function isLoopbackHost(host) {
|
|
247
|
+
return LOOPBACK_HOSTS.has(host);
|
|
248
|
+
}
|
|
249
|
+
//# sourceMappingURL=redirect-uri.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"redirect-uri.js","sourceRoot":"","sources":["../src/redirect-uri.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { Router, type RequestHandler } from 'express';
|
|
2
|
+
import type { ClientRegistryService } from './client-registry-service.js';
|
|
3
|
+
import { type RateLimiterOptions } from './rate-limit.js';
|
|
4
|
+
import type { OauthClient } from './types.js';
|
|
5
|
+
/**
|
|
6
|
+
* `POST /oauth/register` — RFC 7591 open dynamic client registration.
|
|
7
|
+
*
|
|
8
|
+
* ## The one shape that lifts from the donor, and everything that does not
|
|
9
|
+
*
|
|
10
|
+
* Paywalls' `/oauth/register` contributes its response vocabulary: `client_id`,
|
|
11
|
+
* `client_id_issued_at`, the echoed metadata, `grant_types`,
|
|
12
|
+
* `token_endpoint_auth_method`, `response_types` and `scope`. That is about
|
|
13
|
+
* twenty-five lines' worth of shape and it is reproduced below.
|
|
14
|
+
*
|
|
15
|
+
* Its authentication model inverts and is discarded whole. Paywalls reads
|
|
16
|
+
* `req.user`, refuses anyone who is not a `service-user`, looks up that user's
|
|
17
|
+
* account, and creates a row owned by it. Open registration has no caller and
|
|
18
|
+
* no account, so what replaces that gate had to be written fresh:
|
|
19
|
+
* `redirect_uris` required and validated (paywalls never accepts the field at
|
|
20
|
+
* all), a per-IP rate limit here, and a row ceiling in the registry.
|
|
21
|
+
*
|
|
22
|
+
* ## Thin dispatch
|
|
23
|
+
*
|
|
24
|
+
* Parse, call, format. Every rule about what a client may register lives in
|
|
25
|
+
* `ClientRegistryService`, so the seed script and this endpoint cannot disagree
|
|
26
|
+
* about what a valid client is.
|
|
27
|
+
*/
|
|
28
|
+
export interface RegistrationHandlerDeps {
|
|
29
|
+
clientRegistry: ClientRegistryService;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* The RFC 7591 registration response.
|
|
33
|
+
*
|
|
34
|
+
* `client_secret` is deliberately absent rather than null: this server issues
|
|
35
|
+
* only public clients, and a client that found the field would try to use it.
|
|
36
|
+
*/
|
|
37
|
+
export declare function toRegistrationResponse(client: OauthClient): Record<string, unknown>;
|
|
38
|
+
export declare function createRegistrationHandlers(deps: RegistrationHandlerDeps): {
|
|
39
|
+
register: RequestHandler;
|
|
40
|
+
};
|
|
41
|
+
export interface RegistrationRouterOptions extends RegistrationHandlerDeps {
|
|
42
|
+
/**
|
|
43
|
+
* Rate-limiter settings, or `false` to mount none.
|
|
44
|
+
*
|
|
45
|
+
* `false` exists for an app that puts its own limiter in front — a shared
|
|
46
|
+
* one across instances, say. It is not a convenience: an unlimited
|
|
47
|
+
* unauthenticated write endpoint is exactly what the abuse controls are for,
|
|
48
|
+
* so turning this off means taking on the job elsewhere.
|
|
49
|
+
*/
|
|
50
|
+
rateLimit?: RateLimiterOptions | false;
|
|
51
|
+
/** Maximum accepted body size. A registration document is small. */
|
|
52
|
+
bodyLimit?: string;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* `POST /oauth/register`, with its abuse controls attached.
|
|
56
|
+
*
|
|
57
|
+
* The router parses its own JSON body rather than relying on an app-level
|
|
58
|
+
* `express.json()`, so the endpoint works whatever order the host app mounts
|
|
59
|
+
* its middleware in — and so the body limit here is the one that applies.
|
|
60
|
+
*/
|
|
61
|
+
export declare function createRegistrationRouter(options: RegistrationRouterOptions): Router;
|
|
62
|
+
//# sourceMappingURL=registration-controller.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"registration-controller.d.ts","sourceRoot":"","sources":["../src/registration-controller.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAQ,KAAK,cAAc,EAAE,MAAM,SAAS,CAAC;AAE5D,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,8BAA8B,CAAC;AAE1E,OAAO,EAAiC,KAAK,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AACzF,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,MAAM,WAAW,uBAAuB;IACtC,cAAc,EAAE,qBAAqB,CAAC;CACvC;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,WAAW,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAkBnF;AAED,wBAAgB,0BAA0B,CAAC,IAAI,EAAE,uBAAuB,GAAG;IACzE,QAAQ,EAAE,cAAc,CAAC;CAC1B,CA6BA;AAED,MAAM,WAAW,yBAA0B,SAAQ,uBAAuB;IACxE;;;;;;;OAOG;IACH,SAAS,CAAC,EAAE,kBAAkB,GAAG,KAAK,CAAC;IACvC,oEAAoE;IACpE,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;GAMG;AACH,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,yBAAyB,GAAG,MAAM,CAYnF"}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.toRegistrationResponse = toRegistrationResponse;
|
|
4
|
+
exports.createRegistrationHandlers = createRegistrationHandlers;
|
|
5
|
+
exports.createRegistrationRouter = createRegistrationRouter;
|
|
6
|
+
const express_1 = require("express");
|
|
7
|
+
const errors_js_1 = require("./errors.js");
|
|
8
|
+
const rate_limit_js_1 = require("./rate-limit.js");
|
|
9
|
+
/**
|
|
10
|
+
* The RFC 7591 registration response.
|
|
11
|
+
*
|
|
12
|
+
* `client_secret` is deliberately absent rather than null: this server issues
|
|
13
|
+
* only public clients, and a client that found the field would try to use it.
|
|
14
|
+
*/
|
|
15
|
+
function toRegistrationResponse(client) {
|
|
16
|
+
return {
|
|
17
|
+
client_id: client.clientId,
|
|
18
|
+
client_id_issued_at: Math.floor(client.createdAt.getTime() / 1000),
|
|
19
|
+
client_name: client.name ?? undefined,
|
|
20
|
+
client_uri: client.clientUri ?? undefined,
|
|
21
|
+
logo_uri: client.logoUri ?? undefined,
|
|
22
|
+
contacts: client.contacts.length > 0 ? client.contacts : undefined,
|
|
23
|
+
policy_uri: client.policyUri ?? undefined,
|
|
24
|
+
tos_uri: client.tosUri ?? undefined,
|
|
25
|
+
software_id: client.softwareId ?? undefined,
|
|
26
|
+
software_version: client.softwareVersion ?? undefined,
|
|
27
|
+
redirect_uris: client.redirectUris,
|
|
28
|
+
grant_types: client.grantTypes,
|
|
29
|
+
response_types: client.responseTypes,
|
|
30
|
+
token_endpoint_auth_method: client.tokenEndpointAuthMethod,
|
|
31
|
+
scope: client.scopes.join(' '),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
function createRegistrationHandlers(deps) {
|
|
35
|
+
const { clientRegistry } = deps;
|
|
36
|
+
const register = async (req, res, next) => {
|
|
37
|
+
const body = req.body;
|
|
38
|
+
if (typeof body !== 'object' || body === null || Array.isArray(body)) {
|
|
39
|
+
res.status(400).json({
|
|
40
|
+
error: 'invalid_client_metadata',
|
|
41
|
+
error_description: 'the request body must be a JSON object',
|
|
42
|
+
});
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
const client = await clientRegistry.registerDynamic(body);
|
|
47
|
+
res.status(201).json(toRegistrationResponse(client));
|
|
48
|
+
}
|
|
49
|
+
catch (err) {
|
|
50
|
+
// RFC 7591 §3.2.2 error bodies. Translated here rather than left to the
|
|
51
|
+
// global handler, because the registry's refusals are part of this
|
|
52
|
+
// endpoint's contract and a 500 would tell a client to retry.
|
|
53
|
+
if ((0, errors_js_1.isClientRegistryError)(err)) {
|
|
54
|
+
res.status(err.status).json({ error: err.code, error_description: err.message });
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
next(err);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
return { register };
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* `POST /oauth/register`, with its abuse controls attached.
|
|
64
|
+
*
|
|
65
|
+
* The router parses its own JSON body rather than relying on an app-level
|
|
66
|
+
* `express.json()`, so the endpoint works whatever order the host app mounts
|
|
67
|
+
* its middleware in — and so the body limit here is the one that applies.
|
|
68
|
+
*/
|
|
69
|
+
function createRegistrationRouter(options) {
|
|
70
|
+
const handlers = createRegistrationHandlers(options);
|
|
71
|
+
const router = (0, express_1.Router)();
|
|
72
|
+
const middleware = [(0, express_1.json)({ limit: options.bodyLimit ?? '16kb' })];
|
|
73
|
+
if (options.rateLimit !== false) {
|
|
74
|
+
middleware.push((0, rate_limit_js_1.createRegistrationRateLimiter)(options.rateLimit ?? {}).middleware);
|
|
75
|
+
}
|
|
76
|
+
router.post('/oauth/register', ...middleware, handlers.register);
|
|
77
|
+
return router;
|
|
78
|
+
}
|
|
79
|
+
//# sourceMappingURL=registration-controller.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"registration-controller.js","sourceRoot":"","sources":["../src/registration-controller.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ClientRegistryService } from './client-registry-service.js';
|
|
2
|
+
import type { OauthClient } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Authenticating the client behind a back-channel POST.
|
|
5
|
+
*
|
|
6
|
+
* Every client this server issues to is public, so `token_endpoint_auth_method`
|
|
7
|
+
* is `none` and "authentication" here is nothing more than a `client_id` that
|
|
8
|
+
* names a registered, enabled client. That is a small rule with two edges worth
|
|
9
|
+
* keeping in one place: an unknown id and a **suspended** one both answer `401
|
|
10
|
+
* invalid_client`, and the suspension check is the whole reason the `status`
|
|
11
|
+
* column exists — a client whose row is still pointed at by live tokens is
|
|
12
|
+
* stopped here rather than deleted.
|
|
13
|
+
*
|
|
14
|
+
* `POST /oauth/token` and `POST /oauth/revoke` both begin this way. A second
|
|
15
|
+
* copy of the check is the one that would forget suspension, and then a retired
|
|
16
|
+
* client could still reach one of the two endpoints.
|
|
17
|
+
*/
|
|
18
|
+
export declare function resolveRequestClient(clientRegistry: ClientRegistryService, body: unknown): Promise<OauthClient>;
|
|
19
|
+
//# sourceMappingURL=request-client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"request-client.d.ts","sourceRoot":"","sources":["../src/request-client.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,8BAA8B,CAAC;AAE1E,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,oBAAoB,CACxC,cAAc,EAAE,qBAAqB,EACrC,IAAI,EAAE,OAAO,GACZ,OAAO,CAAC,WAAW,CAAC,CAUtB"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.resolveRequestClient = resolveRequestClient;
|
|
4
|
+
const body_field_js_1 = require("./body-field.js");
|
|
5
|
+
const oauth_error_js_1 = require("./oauth-error.js");
|
|
6
|
+
/**
|
|
7
|
+
* Authenticating the client behind a back-channel POST.
|
|
8
|
+
*
|
|
9
|
+
* Every client this server issues to is public, so `token_endpoint_auth_method`
|
|
10
|
+
* is `none` and "authentication" here is nothing more than a `client_id` that
|
|
11
|
+
* names a registered, enabled client. That is a small rule with two edges worth
|
|
12
|
+
* keeping in one place: an unknown id and a **suspended** one both answer `401
|
|
13
|
+
* invalid_client`, and the suspension check is the whole reason the `status`
|
|
14
|
+
* column exists — a client whose row is still pointed at by live tokens is
|
|
15
|
+
* stopped here rather than deleted.
|
|
16
|
+
*
|
|
17
|
+
* `POST /oauth/token` and `POST /oauth/revoke` both begin this way. A second
|
|
18
|
+
* copy of the check is the one that would forget suspension, and then a retired
|
|
19
|
+
* client could still reach one of the two endpoints.
|
|
20
|
+
*/
|
|
21
|
+
async function resolveRequestClient(clientRegistry, body) {
|
|
22
|
+
const clientId = (0, body_field_js_1.required)(body, 'client_id');
|
|
23
|
+
const client = await clientRegistry.findByClientId(clientId);
|
|
24
|
+
if (!client) {
|
|
25
|
+
throw new oauth_error_js_1.OAuthError('invalid_client', 'unknown client_id');
|
|
26
|
+
}
|
|
27
|
+
if (client.status !== 'enabled') {
|
|
28
|
+
throw new oauth_error_js_1.OAuthError('invalid_client', 'this client is suspended');
|
|
29
|
+
}
|
|
30
|
+
return client;
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=request-client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"request-client.js","sourceRoot":"","sources":["../src/request-client.ts"],"names":[],"mappings":";;AAoBA,oDAaC;AAjCD,mDAA2C;AAE3C,qDAA8C;AAG9C;;;;;;;;;;;;;;GAcG;AACI,KAAK,UAAU,oBAAoB,CACxC,cAAqC,EACrC,IAAa;IAEb,MAAM,QAAQ,GAAG,IAAA,wBAAQ,EAAC,IAAI,EAAE,WAAW,CAAC,CAAC;IAC7C,MAAM,MAAM,GAAG,MAAM,cAAc,CAAC,cAAc,CAAC,QAAQ,CAAC,CAAC;IAC7D,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,MAAM,IAAI,2BAAU,CAAC,gBAAgB,EAAE,mBAAmB,CAAC,CAAC;IAC9D,CAAC;IACD,IAAI,MAAM,CAAC,MAAM,KAAK,SAAS,EAAE,CAAC;QAChC,MAAM,IAAI,2BAAU,CAAC,gBAAgB,EAAE,0BAA0B,CAAC,CAAC;IACrE,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Router, type RequestHandler } from 'express';
|
|
2
|
+
import type { ClientRegistryService } from './client-registry-service.js';
|
|
3
|
+
import { type RevocationService } from './revocation-service.js';
|
|
4
|
+
import { type TokenService } from './token-service.js';
|
|
5
|
+
export interface RevocationHandlerDeps {
|
|
6
|
+
clientRegistry: ClientRegistryService;
|
|
7
|
+
tokenService: TokenService;
|
|
8
|
+
revocationService: RevocationService;
|
|
9
|
+
}
|
|
10
|
+
export interface RevocationHandlers {
|
|
11
|
+
revoke: RequestHandler;
|
|
12
|
+
}
|
|
13
|
+
export declare function createRevocationHandlers(deps: RevocationHandlerDeps): RevocationHandlers;
|
|
14
|
+
export interface RevocationRouterOptions extends RevocationHandlerDeps {
|
|
15
|
+
/** Maximum accepted body size. A revocation request is three fields. */
|
|
16
|
+
bodyLimit?: string;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* `POST /oauth/revoke`, with both body parsers attached.
|
|
20
|
+
*
|
|
21
|
+
* Form and JSON, for the reason `createTokenRouter` mounts both: the RFC says
|
|
22
|
+
* form and the ecosystem sends JSON, and a client that gets this wrong on the
|
|
23
|
+
* revocation endpoint has already got it right on the token endpoint. The
|
|
24
|
+
* router parses its own body so the endpoint behaves the same whatever order the
|
|
25
|
+
* host application mounts its middleware in.
|
|
26
|
+
*/
|
|
27
|
+
export declare function createRevocationRouter(options: RevocationRouterOptions): Router;
|
|
28
|
+
//# sourceMappingURL=revocation-controller.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"revocation-controller.d.ts","sourceRoot":"","sources":["../src/revocation-controller.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAkC,KAAK,cAAc,EAAiB,MAAM,SAAS,CAAC;AAGrG,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,8BAA8B,CAAC;AAG1E,OAAO,EAAqB,KAAK,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AACpF,OAAO,EAIL,KAAK,YAAY,EAClB,MAAM,oBAAoB,CAAC;AAwF5B,MAAM,WAAW,qBAAqB;IACpC,cAAc,EAAE,qBAAqB,CAAC;IACtC,YAAY,EAAE,YAAY,CAAC;IAC3B,iBAAiB,EAAE,iBAAiB,CAAC;CACtC;AAED,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,cAAc,CAAC;CACxB;AAED,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,qBAAqB,GAAG,kBAAkB,CAiFxF;AAED,MAAM,WAAW,uBAAwB,SAAQ,qBAAqB;IACpE,wEAAwE;IACxE,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;;GAQG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,uBAAuB,GAAG,MAAM,CAa/E"}
|