@boopkit/cap-oauth-server 0.1.4 → 0.3.1
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 +14 -2
- package/dist/authorization-code-service.d.ts +40 -11
- package/dist/authorization-code-service.d.ts.map +1 -1
- package/dist/authorization-code-service.js +93 -41
- package/dist/authorization-code-service.js.map +1 -1
- package/dist/bearer.d.ts +27 -4
- package/dist/bearer.d.ts.map +1 -1
- package/dist/bearer.js +4 -4
- package/dist/bearer.js.map +1 -1
- package/dist/client-prune.d.ts +102 -0
- package/dist/client-prune.d.ts.map +1 -0
- package/dist/client-prune.js +51 -0
- package/dist/client-prune.js.map +1 -0
- package/dist/client-registry-service.d.ts +85 -8
- package/dist/client-registry-service.d.ts.map +1 -1
- package/dist/client-registry-service.js +267 -64
- package/dist/client-registry-service.js.map +1 -1
- package/dist/config.d.ts +6 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +1 -0
- package/dist/config.js.map +1 -1
- package/dist/connections-controller.js +1 -1
- package/dist/connections-controller.js.map +1 -1
- package/dist/consent-controller.d.ts +80 -6
- package/dist/consent-controller.d.ts.map +1 -1
- package/dist/consent-controller.js +134 -18
- package/dist/consent-controller.js.map +1 -1
- package/dist/discovery-controller.d.ts +2 -1
- package/dist/discovery-controller.d.ts.map +1 -1
- package/dist/discovery-controller.js +14 -4
- package/dist/discovery-controller.js.map +1 -1
- package/dist/discovery.d.ts +26 -0
- package/dist/discovery.d.ts.map +1 -1
- package/dist/discovery.js +55 -1
- package/dist/discovery.js.map +1 -1
- package/dist/errors.d.ts +38 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +59 -0
- package/dist/errors.js.map +1 -1
- package/dist/grant-service.d.ts +11 -4
- package/dist/grant-service.d.ts.map +1 -1
- package/dist/grant-service.js +6 -3
- package/dist/grant-service.js.map +1 -1
- package/dist/index.d.ts +10 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +18 -3
- package/dist/index.js.map +1 -1
- package/dist/metadata-document.d.ts +22 -0
- package/dist/metadata-document.d.ts.map +1 -1
- package/dist/metadata-document.js +44 -2
- package/dist/metadata-document.js.map +1 -1
- package/dist/outbound-target.d.ts +48 -0
- package/dist/outbound-target.d.ts.map +1 -0
- package/dist/outbound-target.js +211 -0
- package/dist/outbound-target.js.map +1 -0
- package/dist/rate-limit.d.ts +53 -11
- package/dist/rate-limit.d.ts.map +1 -1
- package/dist/rate-limit.js +27 -4
- package/dist/rate-limit.js.map +1 -1
- package/dist/redirect-uri.d.ts +17 -9
- package/dist/redirect-uri.d.ts.map +1 -1
- package/dist/redirect-uri.js +26 -10
- package/dist/redirect-uri.js.map +1 -1
- package/dist/registration-controller.d.ts +10 -0
- package/dist/registration-controller.d.ts.map +1 -1
- package/dist/registration-controller.js +10 -0
- package/dist/registration-controller.js.map +1 -1
- package/dist/request-client.d.ts +7 -1
- package/dist/request-client.d.ts.map +1 -1
- package/dist/request-client.js +31 -13
- package/dist/request-client.js.map +1 -1
- package/dist/revocation-controller.d.ts +5 -0
- package/dist/revocation-controller.d.ts.map +1 -1
- package/dist/revocation-controller.js +5 -1
- package/dist/revocation-controller.js.map +1 -1
- package/dist/same-site.d.ts +54 -0
- package/dist/same-site.d.ts.map +1 -0
- package/dist/same-site.js +124 -0
- package/dist/same-site.js.map +1 -0
- package/dist/token-controller.d.ts +26 -0
- package/dist/token-controller.d.ts.map +1 -1
- package/dist/token-controller.js +44 -19
- package/dist/token-controller.js.map +1 -1
- package/dist/types.d.ts +4 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/scaffold/reference/oauth-server.ref.md +359 -11
- package/scaffold/src/middleware/oauth-server.ts +33 -17
- package/scaffold/src/scripts/prune-oauth-clients.ts +86 -0
- package/scaffold/src/services/oauth-server.service.ts +24 -0
- package/scaffold/views/oauth-consent-error.hbs +10 -3
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Garbage-collect abandoned OAuth client registrations.
|
|
3
|
+
*
|
|
4
|
+
* Run with `npm run oauth:prune-clients`. **Schedule it** — see below.
|
|
5
|
+
*
|
|
6
|
+
* ## What it deletes, and what stops it
|
|
7
|
+
*
|
|
8
|
+
* `POST /oauth/register` and the Client ID Metadata Document path both write a
|
|
9
|
+
* registry row for a caller nobody authenticated. Most of those rows are real
|
|
10
|
+
* connectors. Some are a script that walked away, and those never go anywhere:
|
|
11
|
+
* no consent, no grant, no token. After thirty days they are garbage, and this
|
|
12
|
+
* is what collects them.
|
|
13
|
+
*
|
|
14
|
+
* A row is deleted only when **nobody has that client connected** — that is,
|
|
15
|
+
* when no user's `oauth-grants` holds a live entry naming its `publicId`. The
|
|
16
|
+
* capability owns that question (`createClientInUsePredicate`), because the row
|
|
17
|
+
* that matters is the one carrying the `publicId` every live token for that
|
|
18
|
+
* client has as its prefix.
|
|
19
|
+
*
|
|
20
|
+
* Rows an operator wrote with `npm run oauth:seed-clients` are never touched,
|
|
21
|
+
* however old they are. Only the two unauthenticated modes are swept.
|
|
22
|
+
*
|
|
23
|
+
* ## Scheduling it
|
|
24
|
+
*
|
|
25
|
+
* Nothing here runs on a timer by itself, and an unscheduled prune is the same
|
|
26
|
+
* as no prune: `OAUTH_MAX_REGISTERED_CLIENTS` then becomes the only bound on
|
|
27
|
+
* registry growth, and a deployment that reaches it starts refusing legitimate
|
|
28
|
+
* registrations. Pick one:
|
|
29
|
+
*
|
|
30
|
+
* - **Heroku Scheduler** (or your platform's equivalent): run
|
|
31
|
+
* `npm run oauth:prune-clients` daily.
|
|
32
|
+
* - **cron**, on a host you control:
|
|
33
|
+
* `0 4 * * * cd /srv/app && npm run oauth:prune-clients`
|
|
34
|
+
* - **cap-background-jobs**, if this project has it: import
|
|
35
|
+
* `pruneAbandonedOAuthClients` from a job controller's `onMessage` and give
|
|
36
|
+
* the queue a daily entry in `src/scheduler.ts`. That is the right home once
|
|
37
|
+
* the sweep is one of several recurring jobs.
|
|
38
|
+
*
|
|
39
|
+
* Daily is a suggestion, not a requirement. The sweep is idempotent and reads
|
|
40
|
+
* only rows older than the cutoff, so running it more often changes nothing but
|
|
41
|
+
* the size of each batch.
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
import { createClientPruneJob } from '@boopkit/cap-oauth-server';
|
|
45
|
+
|
|
46
|
+
import {
|
|
47
|
+
clientRegistryService,
|
|
48
|
+
grantService,
|
|
49
|
+
tokenService,
|
|
50
|
+
} from '../services/oauth-server.service';
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* How long an unconnected registration survives. Thirty days, per the plan.
|
|
54
|
+
*
|
|
55
|
+
* Shorten it if open registration is being abused; do not shorten it below the
|
|
56
|
+
* time a person plausibly takes between installing a connector and approving
|
|
57
|
+
* it, because that gap is exactly what this measures.
|
|
58
|
+
*/
|
|
59
|
+
const ABANDONED_AFTER_DAYS = 30;
|
|
60
|
+
|
|
61
|
+
const pruneJob = createClientPruneJob({
|
|
62
|
+
clientRegistry: clientRegistryService,
|
|
63
|
+
grantService,
|
|
64
|
+
tokenService,
|
|
65
|
+
ttlMs: ABANDONED_AFTER_DAYS * 24 * 60 * 60 * 1000,
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
/** One sweep. Exported so a background-jobs consumer can call it directly. */
|
|
69
|
+
export async function pruneAbandonedOAuthClients(): Promise<string[]> {
|
|
70
|
+
const { before, removed } = await pruneJob.run();
|
|
71
|
+
console.log(
|
|
72
|
+
`[oauth-server] pruned ${removed.length} abandoned client registration(s) ` +
|
|
73
|
+
`created before ${before.toISOString()}`,
|
|
74
|
+
);
|
|
75
|
+
for (const clientId of removed) console.log(` removed ${clientId}`);
|
|
76
|
+
return removed;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (require.main === module) {
|
|
80
|
+
pruneAbandonedOAuthClients()
|
|
81
|
+
.then(() => process.exit(0))
|
|
82
|
+
.catch((err) => {
|
|
83
|
+
console.error('Pruning abandoned OAuth clients failed:', err);
|
|
84
|
+
process.exit(1);
|
|
85
|
+
});
|
|
86
|
+
}
|
|
@@ -20,6 +20,8 @@
|
|
|
20
20
|
* `oauth-grants` and `oauth-codes` the only two properties it ever reaches.
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
|
+
import { promises as dns } from 'node:dns';
|
|
24
|
+
|
|
23
25
|
import db from '../db';
|
|
24
26
|
import UserService from './users.service';
|
|
25
27
|
import {
|
|
@@ -43,7 +45,29 @@ export const clientRegistryService = createClientRegistryService(db.oauthClient,
|
|
|
43
45
|
// is the seam boop's own tests drive; here it is the real network.
|
|
44
46
|
fetch: globalThis.fetch as unknown as FetchLike,
|
|
45
47
|
maxRegisteredClients: oauthServerConfig.maxRegisteredClients,
|
|
48
|
+
// Where a metadata document may live. A Client ID Metadata Document is
|
|
49
|
+
// fetched at a URL an unauthenticated caller chose, so without this the
|
|
50
|
+
// consent endpoint is an SSRF probe of whatever network this app runs in —
|
|
51
|
+
// the body is discarded, but existence and latency are readable off the
|
|
52
|
+
// response time. The capability refuses every literal private, loopback and
|
|
53
|
+
// link-local target on its own; this resolver is what extends that to a name
|
|
54
|
+
// like `db.internal.example` that merely *points* somewhere private.
|
|
55
|
+
//
|
|
56
|
+
// It lives here rather than in the package because a capability has no
|
|
57
|
+
// business importing `node:dns`. Swap it for your own if you run a resolver
|
|
58
|
+
// with a different view of the network.
|
|
59
|
+
metadataResolveHost: async (hostname) => {
|
|
60
|
+
const records = await dns.lookup(hostname, { all: true });
|
|
61
|
+
return records.map((record) => record.address);
|
|
62
|
+
},
|
|
63
|
+
// Two different questions, answered from one list here: what a client is
|
|
64
|
+
// granted when it asks for nothing, and what it may ask for at all. Narrow
|
|
65
|
+
// `defaultScopes` if you want a smaller default grant — but leave
|
|
66
|
+
// `scopesSupported` as the advertised list, because it is the ceiling that
|
|
67
|
+
// keeps an unauthenticated registrant from writing itself a scope this
|
|
68
|
+
// server's discovery document says it does not have.
|
|
46
69
|
defaultScopes: oauthServerConfig.scopesSupported,
|
|
70
|
+
scopesSupported: oauthServerConfig.scopesSupported,
|
|
47
71
|
onMetadataRefreshFailed: (clientId, err) => {
|
|
48
72
|
// A stale document that could not be refreshed is served from cache, so the
|
|
49
73
|
// user's consent still works — but the failure is not silent, because a
|
|
@@ -6,9 +6,16 @@
|
|
|
6
6
|
redirect: sending an error to an unverified redirect URI is the open redirect.
|
|
7
7
|
So the person is told here instead, on our own page.
|
|
8
8
|
|
|
9
|
-
The message names what was wrong
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
The message names what was wrong, including when what was wrong is that the
|
|
10
|
+
client has been suspended. That distinction is deliberate. It is not the
|
|
11
|
+
enumeration oracle it looks like: a registered `client_id` is already
|
|
12
|
+
distinguishable from an unregistered one here — probe either with a
|
|
13
|
+
`redirect_uri` the client does not own and only the registered one is told the
|
|
14
|
+
URI is unregistered, which is a sentence RFC 6749 requires this page to say —
|
|
15
|
+
and a `client_id` is a public identifier regardless, carried in the
|
|
16
|
+
authorization URL of every user the client has ever sent. What naming the
|
|
17
|
+
suspension buys is a suspended client's operator reading the right column
|
|
18
|
+
instead of hunting a registration that is perfectly intact (boop-wwxg).
|
|
12
19
|
--}}
|
|
13
20
|
<div class="consent-page">
|
|
14
21
|
<section class="consent-card consent-card--error">
|