@qauth-labs/plugin-sdk 0.0.0-stage → 0.2.0-rc.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/LICENSE +201 -0
- package/README.md +148 -2
- package/dist/host.d.ts +24 -0
- package/dist/host.js +24 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.js +31 -0
- package/dist/lib/auth-method.d.ts +424 -0
- package/dist/lib/auth-method.js +1 -0
- package/dist/lib/context.d.ts +163 -0
- package/dist/lib/context.js +390 -0
- package/dist/lib/contract.d.ts +274 -0
- package/dist/lib/contract.js +285 -0
- package/dist/lib/credential-provider.d.ts +86 -0
- package/dist/lib/credential-provider.js +15 -0
- package/dist/lib/engine.d.ts +125 -0
- package/dist/lib/engine.js +367 -0
- package/dist/lib/errors.d.ts +49 -0
- package/dist/lib/errors.js +63 -0
- package/dist/lib/parameters.d.ts +21 -0
- package/dist/lib/parameters.js +62 -0
- package/dist/lib/validate.d.ts +55 -0
- package/dist/lib/validate.js +265 -0
- package/dist/testing/contract-kit.d.ts +116 -0
- package/dist/testing/contract-kit.js +526 -0
- package/dist/testing/fetch-guard.d.ts +38 -0
- package/dist/testing/fetch-guard.js +49 -0
- package/dist/testing/index.d.ts +12 -0
- package/dist/testing/index.js +1 -0
- package/package.json +60 -3
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
import type { z } from 'zod';
|
|
2
|
+
import type { AUTH_METHOD_CONTRACT, AUTH_METHOD_ICON_MEDIA_TYPES, AuthMethodStepUrn, UpstreamFailureReason } from './contract.js';
|
|
3
|
+
import type { CredentialProvider } from './credential-provider.js';
|
|
4
|
+
/**
|
|
5
|
+
* The AuthMethod contract (#599): a sign-in method that plugs into QAuth's
|
|
6
|
+
* engine, such as an upstream OpenID Connect provider.
|
|
7
|
+
*
|
|
8
|
+
* EXPERIMENTAL. This is the plugin API of ADR-018 §4. It is public and
|
|
9
|
+
* documented, but outside the 1.0 stability promise: it may change in a minor
|
|
10
|
+
* release, and `contract` names the version a method is written against.
|
|
11
|
+
*
|
|
12
|
+
* ## What it adds to `CredentialProvider`
|
|
13
|
+
*
|
|
14
|
+
* `CredentialProvider` stays the verification half (ADR-003, as amended):
|
|
15
|
+
* `type`, `verify()` and `extractAttributes()` keep their meaning. An
|
|
16
|
+
* AuthMethod adds what a method needs beyond verification: a stable `id`, the
|
|
17
|
+
* contract version, default display data, a config schema, the Interaction API
|
|
18
|
+
* steps it contributes, the routes it mounts, and `start()`.
|
|
19
|
+
*
|
|
20
|
+
* ## The upstream step
|
|
21
|
+
*
|
|
22
|
+
* 1. The engine offers the upstream step and the person picks the method.
|
|
23
|
+
* 2. The engine calls {@link AuthMethod.start}. The method returns where the
|
|
24
|
+
* browser goes, the transaction it needs back (`state`, `nonce`, a PKCE
|
|
25
|
+
* verifier) and the binding: the random value the authorization request
|
|
26
|
+
* carries to the provider and the callback carries back (`state`). The
|
|
27
|
+
* engine keeps the transaction and the binding server-side, bound to the
|
|
28
|
+
* interaction and its flow cookie; the transaction never reaches the
|
|
29
|
+
* browser.
|
|
30
|
+
* 3. The provider sends the browser back to one of the method's callback
|
|
31
|
+
* routes, under `/upstream/<type>/`. The host compares the binding the
|
|
32
|
+
* callback carries (in the route's `bindingParameter`, `state` by default)
|
|
33
|
+
* with the pending transaction's, in constant time, before it takes the
|
|
34
|
+
* transaction: with no pending transaction (no flow cookie, another
|
|
35
|
+
* interaction, a replay) or a binding that is missing, repeated or
|
|
36
|
+
* another's, it answers `transaction_mismatch` itself, leaves the
|
|
37
|
+
* transaction pending, and the method never runs. Otherwise it takes the
|
|
38
|
+
* transaction (single use) and the route's handler gets it, checks the
|
|
39
|
+
* provider's answer and returns an outcome.
|
|
40
|
+
* 4. The engine turns a `result` outcome into a `VerifiedIdentity` by calling
|
|
41
|
+
* `verify(result)`, checks it, and takes the attributes from
|
|
42
|
+
* `extractAttributes()`. Every attribute's `source` must equal `type`.
|
|
43
|
+
* The identity's `rawClaims.iss` names the issuer that assigned its
|
|
44
|
+
* `externalSub`: a subject is unique only within its issuer (OpenID
|
|
45
|
+
* Connect Core 1.0 §5.7). The sign-in flow stores the issuer with the
|
|
46
|
+
* credential, refuses a later sign-in under another one, and refuses an
|
|
47
|
+
* identity that names none.
|
|
48
|
+
*
|
|
49
|
+
* `verify()` of an AuthMethod receives only what its own callback returned;
|
|
50
|
+
* the engine calls it, once per result, and nothing else does. It still
|
|
51
|
+
* rejects anything that is not such a result, a copy of a real result and a
|
|
52
|
+
* result already used among them: the contract test kit checks that it does.
|
|
53
|
+
*
|
|
54
|
+
* ## What a method is handed (ADR-019 Decision 12)
|
|
55
|
+
*
|
|
56
|
+
* Plugin code is trusted code that runs in-process. The contract hands it only
|
|
57
|
+
* what it needs, through {@link AuthMethodContext}: its config, the egress
|
|
58
|
+
* client, the signer role and a logger. No database handle, no Redis handle,
|
|
59
|
+
* no private key, no Fastify instance, no cookie and no request header.
|
|
60
|
+
*
|
|
61
|
+
* @typeParam TConfig - the parsed config, the output of `configSchema`
|
|
62
|
+
* @typeParam TTransaction - what `start()` asks the engine to keep
|
|
63
|
+
* @experimental
|
|
64
|
+
*/
|
|
65
|
+
export interface AuthMethod<TConfig = unknown, TTransaction = unknown> extends CredentialProvider {
|
|
66
|
+
/**
|
|
67
|
+
* The contract version the method is written against:
|
|
68
|
+
* `qauth-auth-method/1-rc.0`. Its presence is what makes a registered
|
|
69
|
+
* provider an AuthMethod.
|
|
70
|
+
*/
|
|
71
|
+
readonly contract: typeof AUTH_METHOD_CONTRACT;
|
|
72
|
+
/**
|
|
73
|
+
* The stable name of the method's implementation, such as `oidc` or
|
|
74
|
+
* `corp-sso`. Instances of one implementation (one per configured provider)
|
|
75
|
+
* share it; `type` tells them apart.
|
|
76
|
+
*/
|
|
77
|
+
readonly id: string;
|
|
78
|
+
/** What the sign-in page shows for the method until an admin changes it. */
|
|
79
|
+
readonly display: AuthMethodDisplay;
|
|
80
|
+
/**
|
|
81
|
+
* The schema of the method's config. The host parses the stored config with
|
|
82
|
+
* it before it builds a context, so `context.config` is its output. A
|
|
83
|
+
* secret member, such as a client secret, is marked with
|
|
84
|
+
* `.meta({ writeOnly: true })`, so an admin API can keep it write-only.
|
|
85
|
+
*/
|
|
86
|
+
readonly configSchema: z.ZodType<TConfig>;
|
|
87
|
+
/** The Interaction API steps the method contributes, by versioned URN. */
|
|
88
|
+
readonly steps: readonly AuthMethodStepUrn[];
|
|
89
|
+
/** The routes the method mounts, below `/upstream/<type>`. */
|
|
90
|
+
readonly routes: readonly AuthMethodRoute<TConfig, TTransaction>[];
|
|
91
|
+
/**
|
|
92
|
+
* Start an upstream transaction: where the browser goes, and what the
|
|
93
|
+
* method needs back at its callback. Rejects when it cannot start, such as
|
|
94
|
+
* when the provider's discovery document cannot be read.
|
|
95
|
+
*/
|
|
96
|
+
start(request: UpstreamStartRequest, context: AuthMethodContext<TConfig>): Promise<UpstreamStart<TTransaction>>;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* A method's default display data. An admin may override both per realm.
|
|
100
|
+
*
|
|
101
|
+
* @experimental
|
|
102
|
+
*/
|
|
103
|
+
export interface AuthMethodDisplay {
|
|
104
|
+
/**
|
|
105
|
+
* The provider's name, as the button says it: `Example Corp`. At most 64
|
|
106
|
+
* characters, under the Interaction API's display rule.
|
|
107
|
+
*/
|
|
108
|
+
readonly name: string;
|
|
109
|
+
/** An icon for the button, or none. */
|
|
110
|
+
readonly icon?: AuthMethodIcon;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* A default icon: a PNG image of at most 16 KiB. The host serves it from the
|
|
114
|
+
* realm origin.
|
|
115
|
+
*
|
|
116
|
+
* @experimental
|
|
117
|
+
*/
|
|
118
|
+
export interface AuthMethodIcon {
|
|
119
|
+
readonly mediaType: (typeof AUTH_METHOD_ICON_MEDIA_TYPES)[number];
|
|
120
|
+
readonly bytes: Uint8Array;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* What a method is handed for one realm (ADR-019 Decision 12): its config,
|
|
124
|
+
* the egress client, the signer role and a logger. Nothing else: no database
|
|
125
|
+
* or cache handle, no key material.
|
|
126
|
+
*
|
|
127
|
+
* @experimental
|
|
128
|
+
*/
|
|
129
|
+
export interface AuthMethodContext<TConfig = unknown> {
|
|
130
|
+
/** The method's config for this realm, parsed by its `configSchema`. Frozen. */
|
|
131
|
+
readonly config: TConfig;
|
|
132
|
+
/** Every outbound call goes through here (ADR-019 Decision 8). */
|
|
133
|
+
readonly egress: AuthMethodEgress;
|
|
134
|
+
/** Signatures without keys (ADR-019 Decision 9). */
|
|
135
|
+
readonly signer: AuthMethodSigner;
|
|
136
|
+
/** A logger. Never log a secret, a code or a token. */
|
|
137
|
+
readonly log: AuthMethodLogger;
|
|
138
|
+
/**
|
|
139
|
+
* The absolute URL of one of the method's own routes on this realm's host,
|
|
140
|
+
* such as the `redirect_uri` of an authorization request:
|
|
141
|
+
* `routeUrl('/callback')` is `https://<realm host>/upstream/<type>/callback`.
|
|
142
|
+
*/
|
|
143
|
+
routeUrl(path: string): string;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* The egress client as a method sees it. `createAuthMethodContext` builds it
|
|
147
|
+
* over the host's client: it passes on only the members below and adds the
|
|
148
|
+
* realm itself, so an allowlist entry of another realm never applies and a
|
|
149
|
+
* method cannot lift the client's caps.
|
|
150
|
+
*
|
|
151
|
+
* @experimental
|
|
152
|
+
*/
|
|
153
|
+
export interface AuthMethodEgress {
|
|
154
|
+
/**
|
|
155
|
+
* Send one request. Resolves with the final response, whatever its status;
|
|
156
|
+
* rejects when a check refuses the call (a private address, a redirect to
|
|
157
|
+
* one, a cap) or the call fails.
|
|
158
|
+
*/
|
|
159
|
+
request(request: AuthMethodEgressRequest): Promise<AuthMethodEgressResponse>;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* One outbound call of a method.
|
|
163
|
+
*
|
|
164
|
+
* @experimental
|
|
165
|
+
*/
|
|
166
|
+
export interface AuthMethodEgressRequest {
|
|
167
|
+
readonly method: 'GET' | 'POST';
|
|
168
|
+
/** An absolute `https` URL without credentials. */
|
|
169
|
+
readonly url: string;
|
|
170
|
+
readonly headers?: Readonly<Record<string, string>>;
|
|
171
|
+
/** Not allowed with `GET`. */
|
|
172
|
+
readonly body?: string | Uint8Array;
|
|
173
|
+
/**
|
|
174
|
+
* What the call is for, a fixed label: `upstream-discovery`,
|
|
175
|
+
* `upstream-token`. Lower-case letters, digits and `_ . : -`, starting with
|
|
176
|
+
* a letter, at most 64 characters.
|
|
177
|
+
*/
|
|
178
|
+
readonly purpose: string;
|
|
179
|
+
/**
|
|
180
|
+
* Who set the destination: `operator` (the method's own code, installed
|
|
181
|
+
* with the deployment), `admin` (the realm's config of the method) or
|
|
182
|
+
* `remote_document` (a document the method fetched, such as a discovery
|
|
183
|
+
* document). A method never sends to a destination a client or an end user
|
|
184
|
+
* chose.
|
|
185
|
+
*/
|
|
186
|
+
readonly source: 'operator' | 'admin' | 'remote_document';
|
|
187
|
+
/**
|
|
188
|
+
* What a redirect answer does: `follow` (the default) follows it, each
|
|
189
|
+
* target checked as the first URL was, within the host client's redirect
|
|
190
|
+
* cap; `error` follows none, and a redirect answer rejects the call before
|
|
191
|
+
* anything is sent to its target. A call that carries a credential in its
|
|
192
|
+
* body (a client secret, an assertion, a code) uses `error`: a `307` or
|
|
193
|
+
* `308` would otherwise send that body on to wherever the redirect points.
|
|
194
|
+
* It can only tighten the host client's caps, never lift them.
|
|
195
|
+
*/
|
|
196
|
+
readonly redirect?: 'follow' | 'error';
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* The final response of a call, after the redirects it followed.
|
|
200
|
+
*
|
|
201
|
+
* @experimental
|
|
202
|
+
*/
|
|
203
|
+
export interface AuthMethodEgressResponse {
|
|
204
|
+
readonly status: number;
|
|
205
|
+
/** Lower-case names. */
|
|
206
|
+
readonly headers: Readonly<Record<string, string>>;
|
|
207
|
+
readonly body: Uint8Array;
|
|
208
|
+
/** The URL that answered. */
|
|
209
|
+
readonly url: string;
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* The signer role as a method sees it: signatures and public keys, never a
|
|
213
|
+
* key. `createAuthMethodContext` binds the realm, the provider and the caller
|
|
214
|
+
* role, and passes on only the members below, so a
|
|
215
|
+
* method cannot sign for another provider or another realm.
|
|
216
|
+
*
|
|
217
|
+
* @experimental
|
|
218
|
+
*/
|
|
219
|
+
export interface AuthMethodSigner {
|
|
220
|
+
/**
|
|
221
|
+
* Sign a client assertion for `private_key_jwt` toward this method's
|
|
222
|
+
* provider (RFC 7523): `iss` and `sub` are the client id the realm's config
|
|
223
|
+
* names, `aud` is `audience`.
|
|
224
|
+
*/
|
|
225
|
+
signClientAssertion(request: AuthMethodClientAssertionRequest): Promise<string>;
|
|
226
|
+
/** The public keys of those assertions, for the provider to register or fetch. */
|
|
227
|
+
getUpstreamJwks(): Promise<{
|
|
228
|
+
readonly keys: readonly Readonly<Record<string, unknown>>[];
|
|
229
|
+
}>;
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Input of {@link AuthMethodSigner.signClientAssertion}.
|
|
233
|
+
*
|
|
234
|
+
* @experimental
|
|
235
|
+
*/
|
|
236
|
+
export interface AuthMethodClientAssertionRequest {
|
|
237
|
+
/** The provider's issuer identifier, the assertion's sole `aud`. */
|
|
238
|
+
readonly audience: string;
|
|
239
|
+
/** The other claims, `exp` and `jti` among them, without `iss`, `sub` or `aud`. */
|
|
240
|
+
readonly claims: Readonly<Record<string, unknown>>;
|
|
241
|
+
/** The algorithm of the key registered with the provider. */
|
|
242
|
+
readonly algorithm?: 'RS256' | 'ES256' | 'EdDSA';
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* The part of a pino-style logger a method uses.
|
|
246
|
+
*
|
|
247
|
+
* @experimental
|
|
248
|
+
*/
|
|
249
|
+
export interface AuthMethodLogger {
|
|
250
|
+
debug(fields: Record<string, unknown>, message: string): void;
|
|
251
|
+
info(fields: Record<string, unknown>, message: string): void;
|
|
252
|
+
warn(fields: Record<string, unknown>, message: string): void;
|
|
253
|
+
error(fields: Record<string, unknown>, message: string): void;
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* What the engine tells a method when it starts.
|
|
257
|
+
*
|
|
258
|
+
* @experimental
|
|
259
|
+
*/
|
|
260
|
+
export interface UpstreamStartRequest {
|
|
261
|
+
/**
|
|
262
|
+
* The person's preferred languages, BCP 47 tags, most preferred first, for
|
|
263
|
+
* the provider's own pages (`ui_locales`). Empty when none is known.
|
|
264
|
+
*/
|
|
265
|
+
readonly uiLocales: readonly string[];
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* What a method answers when it starts.
|
|
269
|
+
*
|
|
270
|
+
* @experimental
|
|
271
|
+
*/
|
|
272
|
+
export interface UpstreamStart<TTransaction = unknown> {
|
|
273
|
+
/**
|
|
274
|
+
* Where the browser goes: the provider's authorization endpoint with the
|
|
275
|
+
* request's parameters. An absolute `https` URL, no credentials, no
|
|
276
|
+
* fragment, at most 8192 characters.
|
|
277
|
+
*/
|
|
278
|
+
readonly redirectTo: string;
|
|
279
|
+
/**
|
|
280
|
+
* What the method needs back at its callback: `state`, `nonce`, a PKCE
|
|
281
|
+
* verifier. An object of JSON data (objects, arrays, strings, finite
|
|
282
|
+
* numbers, booleans, `null`), at most 4096 bytes as JSON. The engine keeps it server-side and
|
|
283
|
+
* hands it, once, to the callback the same browser reaches.
|
|
284
|
+
*/
|
|
285
|
+
readonly transaction: TTransaction;
|
|
286
|
+
/**
|
|
287
|
+
* The value that binds the callback to this start: fresh and random for
|
|
288
|
+
* each start, sent to the provider in the authorization request (as
|
|
289
|
+
* `state`, usually) so the provider sends it back. 22 to 512 base64url
|
|
290
|
+
* characters (`AUTH_METHOD_BINDING_PATTERN`): 22 is the length of 128
|
|
291
|
+
* random bits. The host compares it with what the callback carries before
|
|
292
|
+
* it hands over the transaction.
|
|
293
|
+
*/
|
|
294
|
+
readonly binding: string;
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* What the host keeps for a started upstream step until its callback: the
|
|
298
|
+
* `transaction` and `binding` that `startUpstreamStep` answered.
|
|
299
|
+
*
|
|
300
|
+
* @experimental
|
|
301
|
+
*/
|
|
302
|
+
export interface PendingUpstreamTransaction<TTransaction = unknown> {
|
|
303
|
+
readonly binding: string;
|
|
304
|
+
readonly transaction: TTransaction;
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* A route a method mounts below `/upstream/<type>`, on the realm host.
|
|
308
|
+
*
|
|
309
|
+
* @experimental
|
|
310
|
+
*/
|
|
311
|
+
export type AuthMethodRoute<TConfig = unknown, TTransaction = unknown> = AuthMethodCallbackRoute<TConfig, TTransaction> | AuthMethodBounceRoute;
|
|
312
|
+
/**
|
|
313
|
+
* Members every route has.
|
|
314
|
+
*
|
|
315
|
+
* @experimental
|
|
316
|
+
*/
|
|
317
|
+
export interface AuthMethodRouteBase {
|
|
318
|
+
readonly method: 'GET' | 'POST';
|
|
319
|
+
/**
|
|
320
|
+
* The path below the method's prefix: `/callback`. One to four segments of
|
|
321
|
+
* lower-case letters, digits, `_` and `-`.
|
|
322
|
+
*/
|
|
323
|
+
readonly path: string;
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* The route the provider sends the browser back to. Its handler checks the
|
|
327
|
+
* provider's answer against the pending transaction and hands an outcome to
|
|
328
|
+
* the engine.
|
|
329
|
+
*
|
|
330
|
+
* @experimental
|
|
331
|
+
*/
|
|
332
|
+
export interface AuthMethodCallbackRoute<TConfig = unknown, TTransaction = unknown> extends AuthMethodRouteBase {
|
|
333
|
+
readonly kind: 'callback';
|
|
334
|
+
/**
|
|
335
|
+
* The parameter the provider carries the binding back in: a query
|
|
336
|
+
* parameter of a `GET` route, a form member of a `POST` one. Default
|
|
337
|
+
* `state`. The host reads it, once and unrepeated, and compares it with
|
|
338
|
+
* the pending transaction's binding before the handler runs.
|
|
339
|
+
*/
|
|
340
|
+
readonly bindingParameter?: string;
|
|
341
|
+
handle(request: AuthMethodCallbackRequest<TTransaction>, context: AuthMethodContext<TConfig>): Promise<UpstreamCallbackOutcome>;
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* A route that only sends the browser on to one of the method's own `GET`
|
|
345
|
+
* routes, on the same origin: a provider's `form_post` arrives cross-site,
|
|
346
|
+
* without the `SameSite=Lax` flow cookie, and the bounce makes the browser
|
|
347
|
+
* bring it. It is handed no transaction and no context.
|
|
348
|
+
*
|
|
349
|
+
* @experimental
|
|
350
|
+
*/
|
|
351
|
+
export interface AuthMethodBounceRoute extends AuthMethodRouteBase {
|
|
352
|
+
readonly kind: 'bounce';
|
|
353
|
+
/**
|
|
354
|
+
* Name where the browser goes. No context: anyone can send a cross-site
|
|
355
|
+
* POST here, so a bounce holds no egress client, no signer and no config.
|
|
356
|
+
*/
|
|
357
|
+
handle(request: AuthMethodRouteRequest): Promise<AuthMethodBounce>;
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* Query and form parameters, by name. A repeated parameter is a list.
|
|
361
|
+
*
|
|
362
|
+
* @experimental
|
|
363
|
+
*/
|
|
364
|
+
export type AuthMethodParameters = Readonly<Record<string, string | readonly string[]>>;
|
|
365
|
+
/**
|
|
366
|
+
* A request to one of a method's routes, as the method sees it: its
|
|
367
|
+
* parameters, and no header and no cookie.
|
|
368
|
+
*
|
|
369
|
+
* @experimental
|
|
370
|
+
*/
|
|
371
|
+
export interface AuthMethodRouteRequest {
|
|
372
|
+
readonly method: 'GET' | 'POST';
|
|
373
|
+
readonly query: AuthMethodParameters;
|
|
374
|
+
/** The form members of a `POST`; empty for a `GET`. */
|
|
375
|
+
readonly form: AuthMethodParameters;
|
|
376
|
+
}
|
|
377
|
+
/**
|
|
378
|
+
* A request to a callback route, with the transaction the engine kept.
|
|
379
|
+
*
|
|
380
|
+
* @experimental
|
|
381
|
+
*/
|
|
382
|
+
export interface AuthMethodCallbackRequest<TTransaction = unknown> extends AuthMethodRouteRequest {
|
|
383
|
+
/**
|
|
384
|
+
* The transaction `start()` returned for this method, which the request's
|
|
385
|
+
* flow cookie names, taken: a second request never sees it again. Always
|
|
386
|
+
* present, and always the one whose binding the request carries: without a
|
|
387
|
+
* pending transaction, or with a binding that does not match, the host
|
|
388
|
+
* answers `transaction_mismatch` and never calls the handler. A handler may
|
|
389
|
+
* still compare `state` with the transaction itself; it costs nothing.
|
|
390
|
+
*/
|
|
391
|
+
readonly transaction: TTransaction;
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* Where a bounce route sends the browser: one of the method's own `GET`
|
|
395
|
+
* routes, by its path, with the query to carry.
|
|
396
|
+
*
|
|
397
|
+
* @experimental
|
|
398
|
+
*/
|
|
399
|
+
export interface AuthMethodBounce {
|
|
400
|
+
/** The path of one of the method's own `GET` routes: `/callback`. */
|
|
401
|
+
readonly to: string;
|
|
402
|
+
readonly query: Readonly<Record<string, string>>;
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* What a callback hands the engine:
|
|
406
|
+
*
|
|
407
|
+
* - `result`: the provider's answer passed every check. The engine passes
|
|
408
|
+
* `result` to `verify()`.
|
|
409
|
+
* - `declined`: the person cancelled or declined at the provider
|
|
410
|
+
* (`access_denied`). Nothing is sent to the client: the person has not
|
|
411
|
+
* refused the client's request, and may pick another method.
|
|
412
|
+
* - `failed`: anything else, with its reason.
|
|
413
|
+
*
|
|
414
|
+
* @experimental
|
|
415
|
+
*/
|
|
416
|
+
export type UpstreamCallbackOutcome<TResult = unknown> = {
|
|
417
|
+
readonly kind: 'result';
|
|
418
|
+
readonly result: TResult;
|
|
419
|
+
} | {
|
|
420
|
+
readonly kind: 'declined';
|
|
421
|
+
} | {
|
|
422
|
+
readonly kind: 'failed';
|
|
423
|
+
readonly reason: UpstreamFailureReason;
|
|
424
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import type { AuthMethod, AuthMethodContext, AuthMethodEgressResponse, AuthMethodLogger } from './auth-method.js';
|
|
2
|
+
/**
|
|
3
|
+
* The egress client as the host has it: the shape of `EgressClient.request`
|
|
4
|
+
* in `@qauth-labs/server-egress`, which takes the realm a destination belongs
|
|
5
|
+
* to and per-call caps. The context's adapter sets the realm itself, and
|
|
6
|
+
* passes one cap only: `maxRedirects: 0`, when the method asks to follow no
|
|
7
|
+
* redirect. No other cap, so a method can tighten the client's redirect cap
|
|
8
|
+
* and lift nothing.
|
|
9
|
+
*
|
|
10
|
+
* @experimental
|
|
11
|
+
*/
|
|
12
|
+
export interface AuthMethodHostEgress {
|
|
13
|
+
request(request: {
|
|
14
|
+
method: 'GET' | 'POST';
|
|
15
|
+
url: string;
|
|
16
|
+
headers?: Record<string, string>;
|
|
17
|
+
body?: string | Uint8Array;
|
|
18
|
+
purpose: string;
|
|
19
|
+
source: 'operator' | 'admin' | 'remote_document';
|
|
20
|
+
realm?: string;
|
|
21
|
+
limits?: {
|
|
22
|
+
maxRedirects: 0;
|
|
23
|
+
};
|
|
24
|
+
}): Promise<AuthMethodEgressResponse>;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* The signer as the host has it: the shape of the two `Signer` operations of
|
|
28
|
+
* `@qauth-labs/server-keys` a method needs, each of which names its caller
|
|
29
|
+
* role, the realm and the provider. The context's adapter fixes all three.
|
|
30
|
+
*
|
|
31
|
+
* @experimental
|
|
32
|
+
*/
|
|
33
|
+
export interface AuthMethodHostSigner {
|
|
34
|
+
signClientAssertion(caller: string, request: {
|
|
35
|
+
readonly realmId: string;
|
|
36
|
+
readonly providerId: string;
|
|
37
|
+
readonly audience: string;
|
|
38
|
+
readonly claims: Readonly<Record<string, unknown>>;
|
|
39
|
+
readonly algorithm?: 'RS256' | 'ES256' | 'EdDSA';
|
|
40
|
+
}): Promise<string>;
|
|
41
|
+
getUpstreamJwks(caller: string, realmId: string, providerId: string): Promise<{
|
|
42
|
+
readonly keys: readonly unknown[];
|
|
43
|
+
}>;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* What a host passes to build a method's context for one realm.
|
|
47
|
+
*
|
|
48
|
+
* @experimental
|
|
49
|
+
*/
|
|
50
|
+
export interface AuthMethodContextParts {
|
|
51
|
+
/** The realm's stored config of the method, before parsing. */
|
|
52
|
+
readonly config: unknown;
|
|
53
|
+
/**
|
|
54
|
+
* The realm host's origin, as the browser reaches it:
|
|
55
|
+
* `https://auth.example.com`. Route URLs are built on it.
|
|
56
|
+
*/
|
|
57
|
+
readonly origin: string;
|
|
58
|
+
/** The realm's name: the egress allowlist entries that name it apply. */
|
|
59
|
+
readonly realmName: string;
|
|
60
|
+
/** The realm's id, for the signer. */
|
|
61
|
+
readonly realmId: string;
|
|
62
|
+
/** The upstream provider, the scope of its client-assertion keys. */
|
|
63
|
+
readonly providerId: string;
|
|
64
|
+
/** The signer caller role the method's calls are made for, such as `api`. */
|
|
65
|
+
readonly signerCaller: string;
|
|
66
|
+
readonly egress: AuthMethodHostEgress;
|
|
67
|
+
readonly signer: AuthMethodHostSigner;
|
|
68
|
+
/** A logger, such as a pino child logger; only its four levels are used. */
|
|
69
|
+
readonly log: AuthMethodLogger;
|
|
70
|
+
/**
|
|
71
|
+
* The method's secret members as the host decrypted them into `config`, by
|
|
72
|
+
* path (`clientSecret`, `mtls.keyPassword`). The context keeps them out of
|
|
73
|
+
* two places: its egress labels, so a secret that parses as a URL opens no
|
|
74
|
+
* allowlisted destination; and its log lines, where a field named as a
|
|
75
|
+
* secret path's last segment, and every occurrence of a secret's value in a
|
|
76
|
+
* string or the message, is redacted.
|
|
77
|
+
*/
|
|
78
|
+
readonly secrets?: ReadonlyMap<string, string>;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* The longest a client assertion signed through the context stays valid, in
|
|
82
|
+
* seconds: its `exp` is lowered to at most this far past the moment it is
|
|
83
|
+
* signed. Five minutes covers a token request and clock skew; an assertion
|
|
84
|
+
* that outlives it is a replayable credential at the provider.
|
|
85
|
+
*
|
|
86
|
+
* @experimental
|
|
87
|
+
*/
|
|
88
|
+
export declare const MAX_AUTH_METHOD_CLIENT_ASSERTION_LIFETIME_SECONDS = 300;
|
|
89
|
+
/**
|
|
90
|
+
* The fields a method's log line may not carry, at any depth, matched without
|
|
91
|
+
* regard to case, `_` or `-` (`clientSecret` matches `client_secret`): the
|
|
92
|
+
* value is replaced with `[Redacted]`. Codes, verifiers, client secrets and
|
|
93
|
+
* assertions, tokens, nonces and passwords: what an upstream sign-in handles
|
|
94
|
+
* and a log must never hold. The last segment of each of the method's own
|
|
95
|
+
* secret paths joins them (`AuthMethodContextParts.secrets`), so does every
|
|
96
|
+
* occurrence of a secret's value.
|
|
97
|
+
*
|
|
98
|
+
* @experimental
|
|
99
|
+
*/
|
|
100
|
+
export declare const AUTH_METHOD_LOG_REDACTED_FIELDS: readonly string[];
|
|
101
|
+
/**
|
|
102
|
+
* The key a method's log fields are nested under, so they never overwrite
|
|
103
|
+
* the host's own fields of the line (`reqId`, `realm`, …).
|
|
104
|
+
*
|
|
105
|
+
* @experimental
|
|
106
|
+
*/
|
|
107
|
+
export declare const AUTH_METHOD_LOG_FIELDS_KEY = "authMethod";
|
|
108
|
+
/**
|
|
109
|
+
* Build the context a method is handed for one realm (ADR-019 Decision 12).
|
|
110
|
+
*
|
|
111
|
+
* The context holds exactly five members: the config, parsed by the method's
|
|
112
|
+
* `configSchema`, copied and deep-frozen (it must be plain data: objects,
|
|
113
|
+
* arrays and primitives, so a Map, Set, Date or other class instance is
|
|
114
|
+
* refused, as none of them freezes); `egress`; `signer`; `log`; and
|
|
115
|
+
* `routeUrl`. Each is an adapter that calls the host's object with the members
|
|
116
|
+
* the contract names and nothing else:
|
|
117
|
+
*
|
|
118
|
+
* - `egress.request` passes `method` (`GET` or `POST`), `url`, `headers`,
|
|
119
|
+
* `body`, `purpose` and `source`, and adds this realm's name. One cap only,
|
|
120
|
+
* `maxRedirects: 0` when the method asks for `redirect: 'error'`; no other
|
|
121
|
+
* cap, no other realm, no other method. `source` is `operator` or `admin` (the
|
|
122
|
+
* labels the realm's egress allowlist applies to, for non-public
|
|
123
|
+
* addresses) only when the URL's origin is the origin of a URL among the
|
|
124
|
+
* plain members of the method's parsed config, never a secret one; any
|
|
125
|
+
* other call goes as `remote_document`, so a method cannot reach an
|
|
126
|
+
* allowlisted address its config does not name.
|
|
127
|
+
* - `signer.signClientAssertion` passes `audience`, `claims` and `algorithm`,
|
|
128
|
+
* with this realm, this provider and the host's caller role. It sets `iat`
|
|
129
|
+
* when the claims carry none, and lowers `exp` to at most
|
|
130
|
+
* {@link MAX_AUTH_METHOD_CLIENT_ASSERTION_LIFETIME_SECONDS} from now.
|
|
131
|
+
* `signer.getUpstreamJwks` takes no argument and asks for this realm and
|
|
132
|
+
* provider.
|
|
133
|
+
* - `log` calls the logger's four levels with a message and the method's
|
|
134
|
+
* fields nested under {@link AUTH_METHOD_LOG_FIELDS_KEY}, as a copy in which
|
|
135
|
+
* every field {@link AUTH_METHOD_LOG_REDACTED_FIELDS} names, or a secret
|
|
136
|
+
* path's last segment, is redacted at any depth, and every secret value is
|
|
137
|
+
* cut out of the message and of every string.
|
|
138
|
+
* - `routeUrl` answers URLs only for the paths the method's own `routes`
|
|
139
|
+
* declare.
|
|
140
|
+
*
|
|
141
|
+
* So the host can hand over its whole egress client, signer and logger, and a
|
|
142
|
+
* method still reaches no `validateDestination`, no `decrypt`, no `child`, no
|
|
143
|
+
* database or cache handle and no key material.
|
|
144
|
+
*
|
|
145
|
+
* @throws AuthMethodConfigError when the config does not pass `configSchema`,
|
|
146
|
+
* or its parsed form is not plain data; the error names the failing
|
|
147
|
+
* members, never their values
|
|
148
|
+
* @experimental
|
|
149
|
+
*/
|
|
150
|
+
export declare function createAuthMethodContext<TConfig>(method: AuthMethod<TConfig>, parts: AuthMethodContextParts): AuthMethodContext<TConfig>;
|
|
151
|
+
/**
|
|
152
|
+
* A method's config as a context holds it: parsed by its `configSchema`, and
|
|
153
|
+
* a copy of plain data (objects with the plain prototype or none, arrays,
|
|
154
|
+
* primitives). The admin API checks a plugin's config with it before it
|
|
155
|
+
* writes it (#600, #639), so a config the context would refuse is refused at
|
|
156
|
+
* the configure, never at every sign-in.
|
|
157
|
+
*
|
|
158
|
+
* @throws AuthMethodConfigError when the config does not pass the schema, or
|
|
159
|
+
* its parsed form is not plain data; the error names the failing member's
|
|
160
|
+
* path, never its value
|
|
161
|
+
* @experimental
|
|
162
|
+
*/
|
|
163
|
+
export declare function parseAuthMethodConfig<TConfig>(method: AuthMethod<TConfig>, raw: unknown): TConfig;
|