@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
package/dist/bearer.js
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DEFAULT_BEARER_CHALLENGE = void 0;
|
|
4
|
+
exports.createOAuthTokenResolver = createOAuthTokenResolver;
|
|
5
|
+
exports.createOAuthBearerMiddleware = createOAuthBearerMiddleware;
|
|
6
|
+
exports.missingScopes = missingScopes;
|
|
7
|
+
exports.requireOAuth = requireOAuth;
|
|
8
|
+
exports.createOAuthResourceServer = createOAuthResourceServer;
|
|
9
|
+
const cap_users_1 = require("@boopkit/cap-users");
|
|
10
|
+
require("./express.js");
|
|
11
|
+
const token_service_js_1 = require("./token-service.js");
|
|
12
|
+
/**
|
|
13
|
+
* The lookup itself, exported so it can be tested without a request.
|
|
14
|
+
*
|
|
15
|
+
* Returns `null` for every reason a token is not usable — malformed, unknown,
|
|
16
|
+
* revoked, expired, or belonging to a grant that has been ended. The caller
|
|
17
|
+
* cannot tell them apart, and should not: the difference is only interesting to
|
|
18
|
+
* whoever stole the token.
|
|
19
|
+
*/
|
|
20
|
+
function createOAuthTokenResolver(options) {
|
|
21
|
+
const { tokenService, grantService, findUserById } = options;
|
|
22
|
+
return async function resolveOAuthToken(token) {
|
|
23
|
+
const resolved = await tokenService.resolve(token_service_js_1.OAUTH_ACCESS_DOMAIN, token);
|
|
24
|
+
if (!resolved)
|
|
25
|
+
return null;
|
|
26
|
+
const grant = await grantService.findActive(resolved.userId, resolved.clientPublicId);
|
|
27
|
+
if (!grant)
|
|
28
|
+
return null;
|
|
29
|
+
const user = await findUserById(resolved.userId);
|
|
30
|
+
if (!user)
|
|
31
|
+
return null;
|
|
32
|
+
return { user, grant, clientPublicId: resolved.clientPublicId };
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* The middleware an application mounts.
|
|
37
|
+
*
|
|
38
|
+
* cap-users' factory takes the lookup as a plain callback with no request in
|
|
39
|
+
* its signature — deliberately, since resolving a credential has no business
|
|
40
|
+
* reading one. But this capability has to hand the resolved grant to the
|
|
41
|
+
* handlers downstream, and the closure is the only place the two can meet, so
|
|
42
|
+
* the binding is made per request. That is a closure allocation, not work; the
|
|
43
|
+
* three reads inside it are the cost of the request either way.
|
|
44
|
+
*/
|
|
45
|
+
function createOAuthBearerMiddleware(deps, options) {
|
|
46
|
+
const resolveOAuthToken = createOAuthTokenResolver(options);
|
|
47
|
+
const challenge = options.challenge ?? 'Bearer';
|
|
48
|
+
return function oauthBearer(req, res, next) {
|
|
49
|
+
// Hand the challenge forward to whatever gate runs behind this. Written
|
|
50
|
+
// unconditionally, before any lookup, because the gate's 401 is for the
|
|
51
|
+
// caller who presented nothing at all — the path on which the resolver
|
|
52
|
+
// itself never refuses anybody.
|
|
53
|
+
req.oauthChallenge = challenge;
|
|
54
|
+
const middleware = (0, cap_users_1.createBearerMiddleware)(deps, {
|
|
55
|
+
strategy: 'oauth',
|
|
56
|
+
challenge,
|
|
57
|
+
resolve: async (token) => {
|
|
58
|
+
const resolved = await resolveOAuthToken(token);
|
|
59
|
+
if (!resolved)
|
|
60
|
+
return null;
|
|
61
|
+
req.oauthGrant = resolved.grant;
|
|
62
|
+
return { user: resolved.user };
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
middleware(req, res, next);
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
/** The challenge sent when the request carried no usable OAuth token at all. */
|
|
69
|
+
exports.DEFAULT_BEARER_CHALLENGE = 'Bearer';
|
|
70
|
+
/**
|
|
71
|
+
* Which of the required scopes this grant does not cover.
|
|
72
|
+
*
|
|
73
|
+
* Set membership, so the comparison does not depend on the order either list
|
|
74
|
+
* happens to be in — a grant approved as `email profile` covers a route that
|
|
75
|
+
* asks for `profile email`. Duplicates in the requirement collapse, so the
|
|
76
|
+
* answer is stable enough to put in a header.
|
|
77
|
+
*/
|
|
78
|
+
function missingScopes(granted, required) {
|
|
79
|
+
const held = new Set(granted);
|
|
80
|
+
return [...new Set(required)].filter((scope) => !held.has(scope));
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Quote a value we are about to place inside a `WWW-Authenticate` parameter.
|
|
84
|
+
*
|
|
85
|
+
* Scope names reach here from a route definition rather than from a request, so
|
|
86
|
+
* this is not defending against an attacker. It is defending against a header
|
|
87
|
+
* that silently stops parsing at a stray quote, which is the kind of fault that
|
|
88
|
+
* shows up as "the client ignores our error" three weeks later.
|
|
89
|
+
*/
|
|
90
|
+
function headerSafe(value) {
|
|
91
|
+
return value.replace(/[\\"]/g, '');
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* `requireOAuth(...scopes)` — the resource-server gate.
|
|
95
|
+
*
|
|
96
|
+
* The request must have arrived on an OAuth access token backed by a live
|
|
97
|
+
* grant, and that grant must cover every named scope. Both refusals are RFC
|
|
98
|
+
* 6750:
|
|
99
|
+
*
|
|
100
|
+
* - **401** with `WWW-Authenticate: <challenge>` when there is no usable
|
|
101
|
+
* token. The challenge is the resolver's — see the file header — so it
|
|
102
|
+
* carries RFC 9728's `resource_metadata` pointer and an unauthenticated
|
|
103
|
+
* caller can find its way to the document that says where to go. Claude
|
|
104
|
+
* does not honour that header on a 200, so the 401 is the only place it can
|
|
105
|
+
* arrive.
|
|
106
|
+
* - **403** with `error="insufficient_scope"` and the required `scope`, in
|
|
107
|
+
* the header per §3.1 and in the body so a client that reads JSON errors
|
|
108
|
+
* rather than headers gets the same answer.
|
|
109
|
+
*
|
|
110
|
+
* The **grant** is the authority, not the token: a token minted under a wider
|
|
111
|
+
* consent is bound by a narrowed grant on its very next request, with no
|
|
112
|
+
* revocation involved.
|
|
113
|
+
*
|
|
114
|
+
* Mount it behind `resolveBearer`. `createOAuthResourceServer` pairs the two so
|
|
115
|
+
* an application cannot mount one without the other.
|
|
116
|
+
*/
|
|
117
|
+
function requireOAuth(...requiredScopes) {
|
|
118
|
+
const required = [...new Set(requiredScopes)];
|
|
119
|
+
return function oauthGate(req, res, next) {
|
|
120
|
+
const challenge = req.oauthChallenge ?? exports.DEFAULT_BEARER_CHALLENGE;
|
|
121
|
+
const grant = req.oauthGrant;
|
|
122
|
+
if (req.authStrategy !== 'oauth' || !grant) {
|
|
123
|
+
res.set('WWW-Authenticate', challenge).status(401).json({
|
|
124
|
+
error: 'invalid_token',
|
|
125
|
+
error_description: 'a valid OAuth access token is required',
|
|
126
|
+
});
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
const missing = missingScopes(grant.scopes, required);
|
|
130
|
+
if (missing.length > 0) {
|
|
131
|
+
const scope = required.join(' ');
|
|
132
|
+
const description = `this connection was not approved for: ${missing.join(' ')}`;
|
|
133
|
+
res
|
|
134
|
+
.set('WWW-Authenticate', `${challenge}, error="insufficient_scope", ` +
|
|
135
|
+
`error_description="${headerSafe(description)}", scope="${headerSafe(scope)}"`)
|
|
136
|
+
.status(403)
|
|
137
|
+
.json({ error: 'insufficient_scope', error_description: description, scope });
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
next();
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
function createOAuthResourceServer(deps, options) {
|
|
144
|
+
const challenge = options.challenge ?? exports.DEFAULT_BEARER_CHALLENGE;
|
|
145
|
+
return {
|
|
146
|
+
resolveBearer: createOAuthBearerMiddleware(deps, { ...options, challenge }),
|
|
147
|
+
requireOAuth,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
//# sourceMappingURL=bearer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bearer.js","sourceRoot":"","sources":["../src/bearer.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reading one string field off a body that arrived as a form or as JSON.
|
|
3
|
+
*
|
|
4
|
+
* ## Why this is a module rather than a helper in each controller
|
|
5
|
+
*
|
|
6
|
+
* Two endpoints — `POST /oauth/token` and `POST /oauth/revoke` — mount both
|
|
7
|
+
* body parsers and accept either encoding, for the reason the token
|
|
8
|
+
* controller's header gives: the specification says form, and real clients send
|
|
9
|
+
* JSON. Both therefore need the same answer to one awkward question, which is
|
|
10
|
+
* what counts as *present*.
|
|
11
|
+
*
|
|
12
|
+
* That answer is not obvious and it is not the same as truthiness. A form body
|
|
13
|
+
* turns an omitted field into `""` rather than into `undefined`, and Express
|
|
14
|
+
* hands back an **array** when a field is repeated — `token=a&token=b` arrives
|
|
15
|
+
* as `['a', 'b']`, which a naive read would pass along to a database query as
|
|
16
|
+
* an object. So: only a `string` counts, it is trimmed, and an empty result is
|
|
17
|
+
* absence. A second copy of that rule is a second place that has to be right
|
|
18
|
+
* about it, and the copy that drifts is the one that lets an array through.
|
|
19
|
+
*/
|
|
20
|
+
/** A single string off a body Express may have handed us an array for. */
|
|
21
|
+
export declare function field(body: unknown, name: string): string | undefined;
|
|
22
|
+
/** The same, refusing with RFC 6749 §5.2's `invalid_request` when absent. */
|
|
23
|
+
export declare function required(body: unknown, name: string): string;
|
|
24
|
+
//# sourceMappingURL=body-field.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"body-field.d.ts","sourceRoot":"","sources":["../src/body-field.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,0EAA0E;AAC1E,wBAAgB,KAAK,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAMrE;AAED,6EAA6E;AAC7E,wBAAgB,QAAQ,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAM5D"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.field = field;
|
|
4
|
+
exports.required = required;
|
|
5
|
+
const oauth_error_js_1 = require("./oauth-error.js");
|
|
6
|
+
/**
|
|
7
|
+
* Reading one string field off a body that arrived as a form or as JSON.
|
|
8
|
+
*
|
|
9
|
+
* ## Why this is a module rather than a helper in each controller
|
|
10
|
+
*
|
|
11
|
+
* Two endpoints — `POST /oauth/token` and `POST /oauth/revoke` — mount both
|
|
12
|
+
* body parsers and accept either encoding, for the reason the token
|
|
13
|
+
* controller's header gives: the specification says form, and real clients send
|
|
14
|
+
* JSON. Both therefore need the same answer to one awkward question, which is
|
|
15
|
+
* what counts as *present*.
|
|
16
|
+
*
|
|
17
|
+
* That answer is not obvious and it is not the same as truthiness. A form body
|
|
18
|
+
* turns an omitted field into `""` rather than into `undefined`, and Express
|
|
19
|
+
* hands back an **array** when a field is repeated — `token=a&token=b` arrives
|
|
20
|
+
* as `['a', 'b']`, which a naive read would pass along to a database query as
|
|
21
|
+
* an object. So: only a `string` counts, it is trimmed, and an empty result is
|
|
22
|
+
* absence. A second copy of that rule is a second place that has to be right
|
|
23
|
+
* about it, and the copy that drifts is the one that lets an array through.
|
|
24
|
+
*/
|
|
25
|
+
/** A single string off a body Express may have handed us an array for. */
|
|
26
|
+
function field(body, name) {
|
|
27
|
+
if (typeof body !== 'object' || body === null)
|
|
28
|
+
return undefined;
|
|
29
|
+
const value = body[name];
|
|
30
|
+
if (typeof value !== 'string')
|
|
31
|
+
return undefined;
|
|
32
|
+
const trimmed = value.trim();
|
|
33
|
+
return trimmed === '' ? undefined : trimmed;
|
|
34
|
+
}
|
|
35
|
+
/** The same, refusing with RFC 6749 §5.2's `invalid_request` when absent. */
|
|
36
|
+
function required(body, name) {
|
|
37
|
+
const value = field(body, name);
|
|
38
|
+
if (value === undefined) {
|
|
39
|
+
throw new oauth_error_js_1.OAuthError('invalid_request', `${name} is required`);
|
|
40
|
+
}
|
|
41
|
+
return value;
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=body-field.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"body-field.js","sourceRoot":"","sources":["../src/body-field.ts"],"names":[],"mappings":";;AAuBA,sBAMC;AAGD,4BAMC;AAtCD,qDAA8C;AAE9C;;;;;;;;;;;;;;;;;;GAkBG;AAEH,0EAA0E;AAC1E,SAAgB,KAAK,CAAC,IAAa,EAAE,IAAY;IAC/C,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,KAAK,IAAI;QAAE,OAAO,SAAS,CAAC;IAChE,MAAM,KAAK,GAAI,IAAgC,CAAC,IAAI,CAAC,CAAC;IACtD,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,SAAS,CAAC;IAChD,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC;IAC7B,OAAO,OAAO,KAAK,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC;AAC9C,CAAC;AAED,6EAA6E;AAC7E,SAAgB,QAAQ,CAAC,IAAa,EAAE,IAAY;IAClD,MAAM,KAAK,GAAG,KAAK,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;IAChC,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;QACxB,MAAM,IAAI,2BAAU,CAAC,iBAAiB,EAAE,GAAG,IAAI,cAAc,CAAC,CAAC;IACjE,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC"}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { type FetchLike } from './metadata-document.js';
|
|
2
|
+
import type { ClientMetadataInput, OauthClient, OauthClientDelegate, StaticClientInput } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* The client registry — who is asking, and what they are allowed to ask for.
|
|
5
|
+
*
|
|
6
|
+
* One table, one lookup path, three registration modes. A mode says where a
|
|
7
|
+
* client's *authoritative* metadata lives, and almost nothing else:
|
|
8
|
+
*
|
|
9
|
+
* `dynamic` open RFC 7591 registration; we own the metadata
|
|
10
|
+
* `metadata_document` the `client_id` is an https URL; the client owns it
|
|
11
|
+
* `static` an operator wrote the row; only an operator changes it
|
|
12
|
+
*
|
|
13
|
+
* ## What is not here
|
|
14
|
+
*
|
|
15
|
+
* Nothing user- or account-shaped. A client is software; the record tying a
|
|
16
|
+
* client to a *person* is the grant, which lives in cap-users' user properties
|
|
17
|
+
* and belongs to Slice 2. The registry never learns who consented.
|
|
18
|
+
*
|
|
19
|
+
* ## The abuse controls, and why they are fresh work
|
|
20
|
+
*
|
|
21
|
+
* Neither donor has an unauthenticated registration endpoint. Paywalls'
|
|
22
|
+
* `/oauth/register` requires a caller of user type `service-user` and creates a
|
|
23
|
+
* row owned by that caller's account; open registration has no caller and no
|
|
24
|
+
* account, so the gate that made paywalls' endpoint safe has nothing to map
|
|
25
|
+
* onto. What replaces it is here and in `createRegistrationRateLimiter`:
|
|
26
|
+
* `redirect_uris` required and validated at registration, a ceiling on total
|
|
27
|
+
* rows, per-IP rate limiting at the transport, and a prune for rows that never
|
|
28
|
+
* went anywhere.
|
|
29
|
+
*/
|
|
30
|
+
/** Sensible RFC 7591 defaults for a client that asked for nothing specific. */
|
|
31
|
+
export declare const DEFAULT_GRANT_TYPES: string[];
|
|
32
|
+
export declare const DEFAULT_RESPONSE_TYPES: string[];
|
|
33
|
+
/** 24 hours. A cached metadata document older than this is re-fetched lazily. */
|
|
34
|
+
export declare const DEFAULT_METADATA_MAX_AGE_MS: number;
|
|
35
|
+
/** 30 days, per the plan: a dynamic row that never went anywhere is garbage. */
|
|
36
|
+
export declare const DEFAULT_ABANDONED_CLIENT_TTL_MS: number;
|
|
37
|
+
/** How many rows open registration may create before it starts refusing. */
|
|
38
|
+
export declare const DEFAULT_MAX_REGISTERED_CLIENTS = 1000;
|
|
39
|
+
export interface ClientRegistryServiceOptions {
|
|
40
|
+
/**
|
|
41
|
+
* The HTTP boundary for metadata documents. Required only if
|
|
42
|
+
* `metadata_document` clients are in play; a registry that never sees one
|
|
43
|
+
* never calls it.
|
|
44
|
+
*/
|
|
45
|
+
fetch?: FetchLike;
|
|
46
|
+
/** Ceiling on `dynamic` rows. `Infinity` disables the check. */
|
|
47
|
+
maxRegisteredClients?: number;
|
|
48
|
+
/** Cache age past which a metadata document is re-fetched. */
|
|
49
|
+
metadataMaxAgeMs?: number;
|
|
50
|
+
metadataTimeoutMs?: number;
|
|
51
|
+
metadataMaxBytes?: number;
|
|
52
|
+
metadataMaxRedirects?: number;
|
|
53
|
+
/** Scopes a client may be granted, if it named none. */
|
|
54
|
+
defaultScopes?: string[];
|
|
55
|
+
/** Injectable clock, so expiry and cache-age tests do not sleep. */
|
|
56
|
+
now?: () => Date;
|
|
57
|
+
/** Called when a re-fetch fails and the cached copy is used instead. */
|
|
58
|
+
onMetadataRefreshFailed?(clientId: string, err: unknown): void;
|
|
59
|
+
}
|
|
60
|
+
export interface PruneOptions {
|
|
61
|
+
/** Delete abandoned `dynamic` rows created before this instant. */
|
|
62
|
+
before?: Date;
|
|
63
|
+
/**
|
|
64
|
+
* Is this row still in use?
|
|
65
|
+
*
|
|
66
|
+
* Slice 1 has no grants, so the default answer is "no" and the prune is safe
|
|
67
|
+
* to run only against a registry whose clients have not connected yet.
|
|
68
|
+
* Slice 3 owns grants and injects the real predicate; until it does, the
|
|
69
|
+
* scaffold does not schedule this.
|
|
70
|
+
*/
|
|
71
|
+
isInUse?(client: OauthClient): Promise<boolean>;
|
|
72
|
+
}
|
|
73
|
+
export interface ClientRegistryService {
|
|
74
|
+
/** RFC 7591 open registration. Generates the `publicId` and the `client_id`. */
|
|
75
|
+
registerDynamic(input: ClientMetadataInput): Promise<OauthClient>;
|
|
76
|
+
/**
|
|
77
|
+
* Resolve a Client ID Metadata Document client, fetching and caching the
|
|
78
|
+
* document on first sight and lazily re-fetching a stale copy.
|
|
79
|
+
*/
|
|
80
|
+
resolveMetadataDocument(clientId: string): Promise<OauthClient>;
|
|
81
|
+
/** Operator-configured client. Idempotent, so a seed script may re-run. */
|
|
82
|
+
registerStaticClient(input: StaticClientInput): Promise<OauthClient>;
|
|
83
|
+
/**
|
|
84
|
+
* The authorization endpoint's lookup: a raw `client_id` string off a query
|
|
85
|
+
* parameter, which for a CIMD client is a URL.
|
|
86
|
+
*/
|
|
87
|
+
findByClientId(clientId: string): Promise<OauthClient | null>;
|
|
88
|
+
/** Lookup by the token prefix, for the resource-server path. */
|
|
89
|
+
findByPublicId(publicId: string): Promise<OauthClient | null>;
|
|
90
|
+
/** Suspend without deleting a row that live tokens carry the prefix of. */
|
|
91
|
+
suspend(clientId: string): Promise<OauthClient>;
|
|
92
|
+
/** Re-enable a suspended client. */
|
|
93
|
+
unsuspend(clientId: string): Promise<OauthClient>;
|
|
94
|
+
/** Registration-surface update. Refuses a `static` row. */
|
|
95
|
+
updateRegistration(clientId: string, input: ClientMetadataInput): Promise<OauthClient>;
|
|
96
|
+
/** Registration-surface delete. Refuses a `static` row. */
|
|
97
|
+
deleteRegistration(clientId: string): Promise<void>;
|
|
98
|
+
/** How many rows the ceiling counts. */
|
|
99
|
+
countRegistered(): Promise<number>;
|
|
100
|
+
/** Garbage-collect abandoned dynamic rows. Returns the client ids removed. */
|
|
101
|
+
pruneAbandonedClients(options?: PruneOptions): Promise<string[]>;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* The redirect-URI rule at *registration*, which is the other half of
|
|
105
|
+
* `matchRedirectUri`. The matcher decides whether a presented URI satisfies a
|
|
106
|
+
* stored one; this decides whether a URI may be stored at all.
|
|
107
|
+
*
|
|
108
|
+
* Both halves read `classifyRedirectUri`, and that is the point: a URI this
|
|
109
|
+
* function stores is by construction a URI the matcher will compare. The three
|
|
110
|
+
* shapes it admits — claimed https, loopback http, and an application's own
|
|
111
|
+
* private-use scheme — and the grounds each is admitted or refused on live in
|
|
112
|
+
* `redirect-uri.ts` next to the matching rule they have to agree with.
|
|
113
|
+
*
|
|
114
|
+
* This is the umbrella spec's "declared-but-unenforced redirect URI lists"
|
|
115
|
+
* defect closed at the point of entry: a list that was never validated going in
|
|
116
|
+
* cannot be enforced coming out, whatever the matcher does.
|
|
117
|
+
*/
|
|
118
|
+
export declare function validateRedirectUris(value: unknown): string[];
|
|
119
|
+
export declare function createClientRegistryService(model: OauthClientDelegate, options?: ClientRegistryServiceOptions): ClientRegistryService;
|
|
120
|
+
//# sourceMappingURL=client-registry-service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client-registry-service.d.ts","sourceRoot":"","sources":["../src/client-registry-service.ts"],"names":[],"mappings":"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"}
|