@aooth/auth 0.1.7 → 0.1.9
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/dist/atscript-db.cjs +36 -25
- package/dist/atscript-db.d.cts +28 -22
- package/dist/atscript-db.d.mts +28 -22
- package/dist/atscript-db.mjs +36 -25
- package/dist/authz.cjs +378 -0
- package/dist/authz.d.cts +502 -0
- package/dist/authz.d.mts +502 -0
- package/dist/authz.mjs +365 -0
- package/dist/client.cjs +59 -0
- package/dist/client.d.cts +64 -0
- package/dist/client.d.mts +64 -0
- package/dist/client.mjs +58 -0
- package/dist/clock-Bdsep_1j.mjs +4 -0
- package/dist/clock-BjXa0LXb.d.cts +14 -0
- package/dist/clock-BjXa0LXb.d.mts +14 -0
- package/dist/clock-Bl-H3eqE.cjs +9 -0
- package/dist/index.cjs +294 -65
- package/dist/index.d.cts +166 -44
- package/dist/index.d.mts +166 -44
- package/dist/index.mjs +289 -60
- package/dist/payload-BJjvj8AH.cjs +32 -0
- package/dist/payload-D-DzH5-J.mjs +27 -0
- package/dist/redis.cjs +9 -0
- package/dist/redis.d.cts +8 -7
- package/dist/redis.d.mts +8 -7
- package/dist/redis.mjs +9 -0
- package/dist/store-BG6m6oSJ.d.cts +263 -0
- package/dist/store-BG6m6oSJ.d.mts +263 -0
- package/package.json +28 -10
- package/src/atscript-db/auth-credential.as +16 -10
- package/src/atscript-db/auth-credential.as.d.ts +68 -0
- package/dist/store-B1t8KkfA.d.mts +0 -124
- package/dist/store-untAtWQz.d.cts +0 -124
package/dist/authz.d.cts
ADDED
|
@@ -0,0 +1,502 @@
|
|
|
1
|
+
import { t as Clock } from "./clock-BjXa0LXb.cjs";
|
|
2
|
+
import { JWK } from "jose";
|
|
3
|
+
|
|
4
|
+
//#region src/authz/token-policy.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* What the authorization server mints for a completed grant — forwarded verbatim
|
|
7
|
+
* into `AuthCredential.issue(userId, …)` at the token endpoint. Decided by the
|
|
8
|
+
* {@link import("./client-policy").ClientRedirectPolicy} (per client / scope),
|
|
9
|
+
* never by the client request, and recorded on the pending-authorization + the
|
|
10
|
+
* issued auth-code so the grant's authority is fixed at `/authorize` time, not
|
|
11
|
+
* `/token` time.
|
|
12
|
+
*
|
|
13
|
+
* Tier 1 (CLI / loopback) → a full-authority `cli-session` (`{ kind, ttl }`, no
|
|
14
|
+
* `payload`). A scoped service token (Tier 2) additionally sets `payload` with
|
|
15
|
+
* the consumer's attenuation root fields.
|
|
16
|
+
*/
|
|
17
|
+
interface TokenPolicy {
|
|
18
|
+
/** Semantic credential kind stamped on the token (e.g. `"cli-session"`). See `IssueOptions.kind`. */
|
|
19
|
+
kind?: string;
|
|
20
|
+
/** Per-mint access-token lifetime in ms (forwarded to `IssueOptions.ttl`). */
|
|
21
|
+
ttl?: number;
|
|
22
|
+
/**
|
|
23
|
+
* Extra root payload merged into `issue()` — e.g. `@arbac.attenuate.*` fields
|
|
24
|
+
* for a SCOPED token. Omit for a full-authority token. MUST be JSON-safe: it
|
|
25
|
+
* is persisted on the pending-authorization + auth-code records.
|
|
26
|
+
*/
|
|
27
|
+
payload?: Record<string, unknown>;
|
|
28
|
+
}
|
|
29
|
+
//#endregion
|
|
30
|
+
//#region src/authz/authz-errors.d.ts
|
|
31
|
+
/**
|
|
32
|
+
* Failure taxonomy for the authorization-server endpoints (AUTH-SERVER.md §7).
|
|
33
|
+
* The `/authorize` side fails SOFT (302 → an app error/login route, never a
|
|
34
|
+
* 500); the `/token` side returns the RFC-6749-shaped JSON error named by the
|
|
35
|
+
* code. Messages are benign — they must not disclose whether a failure was an
|
|
36
|
+
* unknown client vs. a bad redirect vs. an expired code.
|
|
37
|
+
*/
|
|
38
|
+
type AuthorizeErrorCode = /** Missing or malformed parameter at `/authorize` or `/token`. */"invalid_request" /** `redirect_uri` is not an allowed (loopback / registered) target. */ | "invalid_redirect" /** Code unknown / expired / already-redeemed, or PKCE verifier mismatch (`/token`). */ | "invalid_grant" /** Unknown or unauthenticated client (Tier 2). */ | "invalid_client" /** A known client used a grant/response it is not allowed (Tier 2). */ | "unauthorized_client" /** The user declined the authorization (consent). */ | "access_denied" /** An unexpected server-side failure. */ | "server_error";
|
|
39
|
+
/** A typed authorization-server failure. */
|
|
40
|
+
declare class AuthorizeError extends Error {
|
|
41
|
+
readonly code: AuthorizeErrorCode;
|
|
42
|
+
constructor(code: AuthorizeErrorCode, message: string);
|
|
43
|
+
}
|
|
44
|
+
//#endregion
|
|
45
|
+
//#region src/authz/client-policy.d.ts
|
|
46
|
+
/**
|
|
47
|
+
* The client + redirect resolved at `GET /auth/authorize` — what the grant is
|
|
48
|
+
* allowed to deliver and where.
|
|
49
|
+
*/
|
|
50
|
+
interface ResolvedClient {
|
|
51
|
+
/** Registered client id (Tier 2), absent for a public/loopback client. */
|
|
52
|
+
clientId?: string;
|
|
53
|
+
/** The validated `redirect_uri` the code will be delivered to. */
|
|
54
|
+
redirectUri: string;
|
|
55
|
+
/** What the grant mints (fixed here, recorded on the pending authorization). */
|
|
56
|
+
tokenPolicy: TokenPolicy;
|
|
57
|
+
/**
|
|
58
|
+
* Tier 2 (OIDC): mint an `id_token` with `aud` = {@link audience}. Omitted ⇒
|
|
59
|
+
* no `id_token` (Tier-1 loopback).
|
|
60
|
+
*/
|
|
61
|
+
idToken?: boolean;
|
|
62
|
+
/**
|
|
63
|
+
* Mint an access token. **Omitted ⇒ minted** (preserves Tier-1 loopback,
|
|
64
|
+
* where the CLI gets an access token); set `false` for a pure-OIDC sign-in
|
|
65
|
+
* client that should receive identity only.
|
|
66
|
+
*/
|
|
67
|
+
accessToken?: boolean;
|
|
68
|
+
/** The `id_token` `aud` (the registered `client_id`). */
|
|
69
|
+
audience?: string;
|
|
70
|
+
/** Granted scope (space-joined) — `requested ∩ allowed`; drives the `id_token` profile claims. */
|
|
71
|
+
scope?: string;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* The pluggable trust boundary of the authorization server (AUTH-SERVER.md §4.5):
|
|
75
|
+
* decide whether a client + `redirect_uri` may start a grant and what it may
|
|
76
|
+
* receive. The flow is otherwise identical across tiers — only this policy
|
|
77
|
+
* varies. Tier 1 ships {@link LoopbackClientPolicy}; a `RegisteredClientPolicy`
|
|
78
|
+
* (static client registry, exact/prefix redirect allowlist, `id_token`) is the
|
|
79
|
+
* Tier-2 addition.
|
|
80
|
+
*/
|
|
81
|
+
interface ClientRedirectPolicy {
|
|
82
|
+
/**
|
|
83
|
+
* Authorize the client + `redirect_uri` and resolve the token policy; THROW
|
|
84
|
+
* an {@link AuthorizeError} on a miss (`invalid_redirect` / `invalid_client`).
|
|
85
|
+
* This is THE open-redirect / token-theft gate — never reflect an unvalidated
|
|
86
|
+
* `redirect_uri`.
|
|
87
|
+
*/
|
|
88
|
+
resolveClient(args: {
|
|
89
|
+
clientId?: string;
|
|
90
|
+
redirectUri: string;
|
|
91
|
+
scope?: string;
|
|
92
|
+
}): ResolvedClient | Promise<ResolvedClient>;
|
|
93
|
+
/**
|
|
94
|
+
* Tier 2: authenticate the client at `POST /auth/token` — verify the
|
|
95
|
+
* `client_secret` of a confidential client (PKCE is the binding for a public
|
|
96
|
+
* one). THROW an {@link AuthorizeError} (`invalid_client`) on failure. Optional:
|
|
97
|
+
* a public-only policy (loopback) omits it.
|
|
98
|
+
*/
|
|
99
|
+
authenticateClient?(args: {
|
|
100
|
+
clientId?: string;
|
|
101
|
+
clientSecret?: string;
|
|
102
|
+
}): void | Promise<void>;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* `true` when `uri` is a syntactically valid http(s) URL whose host is a
|
|
106
|
+
* **loopback literal** — `127.0.0.1`, `::1`, or `localhost` — on any port (RFC
|
|
107
|
+
* 8252 §7.3). Rejects everything else, including the classic bypasses: a
|
|
108
|
+
* host-suffix (`127.0.0.1.evil.com`, `localhost.evil.com`), embedded credentials
|
|
109
|
+
* (`http://127.0.0.1@evil.com` → host `evil.com`), a non-http scheme, and a bare
|
|
110
|
+
* `0.0.0.0`. Only a local process can receive a loopback redirect, which is why
|
|
111
|
+
* an arbitrary port is safe — the binding is the loopback host + PKCE.
|
|
112
|
+
*/
|
|
113
|
+
declare function isLoopbackRedirectUri(uri: string): boolean;
|
|
114
|
+
interface LoopbackClientPolicyOptions {
|
|
115
|
+
/**
|
|
116
|
+
* What a loopback grant mints. Default: a full-authority `cli-session` with a
|
|
117
|
+
* 30-day TTL (un-attenuated — see {@link TokenPolicy}). Override to scope or
|
|
118
|
+
* re-label CLI tokens.
|
|
119
|
+
*/
|
|
120
|
+
tokenPolicy?: TokenPolicy;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Tier-1 policy: accept any **loopback** `redirect_uri`, treat the client as a
|
|
124
|
+
* public client (no `client_id` / secret — PKCE is the binding), and mint the
|
|
125
|
+
* configured CLI token policy. Rejects every non-loopback redirect.
|
|
126
|
+
*/
|
|
127
|
+
declare class LoopbackClientPolicy implements ClientRedirectPolicy {
|
|
128
|
+
private readonly tokenPolicy;
|
|
129
|
+
constructor(opts?: LoopbackClientPolicyOptions);
|
|
130
|
+
resolveClient(args: {
|
|
131
|
+
clientId?: string;
|
|
132
|
+
redirectUri: string;
|
|
133
|
+
scope?: string;
|
|
134
|
+
}): ResolvedClient;
|
|
135
|
+
}
|
|
136
|
+
//#endregion
|
|
137
|
+
//#region src/authz/registered-client-policy.d.ts
|
|
138
|
+
/**
|
|
139
|
+
* One registered first-party client (AUTH-SERVER.md §4.5). The registry is the
|
|
140
|
+
* open-redirect / token-theft boundary, so a client's `redirect_uri` allowlist
|
|
141
|
+
* and what it may receive are declared HERE, never inferred from the request.
|
|
142
|
+
*/
|
|
143
|
+
interface RegisteredClient {
|
|
144
|
+
/** Stable client identifier; the `id_token` `aud`. */
|
|
145
|
+
clientId: string;
|
|
146
|
+
/** Exact-match `redirect_uri` allowlist (the safe default). */
|
|
147
|
+
redirectUris?: string[];
|
|
148
|
+
/**
|
|
149
|
+
* Strict-prefix `redirect_uri` allowlist (an entry must be a non-empty prefix
|
|
150
|
+
* of the request). Looser than exact match — only use for a tightly-scoped
|
|
151
|
+
* path prefix on a trusted origin.
|
|
152
|
+
*/
|
|
153
|
+
redirectPrefixes?: string[];
|
|
154
|
+
/** `"public"` (PKCE only) or `"confidential"` (PKCE + `client_secret`). Default `"public"`. */
|
|
155
|
+
type?: "public" | "confidential";
|
|
156
|
+
/** Shared secret for a confidential client (compared in constant time at `/token`). */
|
|
157
|
+
clientSecret?: string;
|
|
158
|
+
/** Mint an `id_token` (requires the granted scope to include `openid`). Default `true`. */
|
|
159
|
+
idToken?: boolean;
|
|
160
|
+
/** Also mint an access token for the main API. Default `false` — a pure sign-in client gets identity only. */
|
|
161
|
+
accessToken?: boolean;
|
|
162
|
+
/** Allowed scopes; the granted scope is `requested ∩ allowed`. Omit to allow any requested scope. */
|
|
163
|
+
scopes?: string[];
|
|
164
|
+
/** Token policy for the access token, when `accessToken` is set. */
|
|
165
|
+
tokenPolicy?: TokenPolicy;
|
|
166
|
+
}
|
|
167
|
+
interface RegisteredClientPolicyOptions {
|
|
168
|
+
clients: RegisteredClient[];
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Tier-2 policy: a static registry of first-party clients. `resolveClient`
|
|
172
|
+
* authorizes the client + `redirect_uri` (against the registered allowlist) and
|
|
173
|
+
* resolves the granted scope + what the grant delivers (`id_token`/`access_token`,
|
|
174
|
+
* `aud = client_id`); `authenticateClient` authenticates the client at `/token`
|
|
175
|
+
* (`client_secret` for confidential clients; PKCE is the binding for public ones).
|
|
176
|
+
* An unregistered client or an unlisted redirect is rejected.
|
|
177
|
+
*/
|
|
178
|
+
declare class RegisteredClientPolicy implements ClientRedirectPolicy {
|
|
179
|
+
private readonly clients;
|
|
180
|
+
constructor(opts: RegisteredClientPolicyOptions);
|
|
181
|
+
resolveClient(args: {
|
|
182
|
+
clientId?: string;
|
|
183
|
+
redirectUri: string;
|
|
184
|
+
scope?: string;
|
|
185
|
+
}): ResolvedClient;
|
|
186
|
+
authenticateClient(args: {
|
|
187
|
+
clientId?: string;
|
|
188
|
+
clientSecret?: string;
|
|
189
|
+
}): void;
|
|
190
|
+
private requireClient;
|
|
191
|
+
private redirectAllowed;
|
|
192
|
+
private grantScope;
|
|
193
|
+
}
|
|
194
|
+
//#endregion
|
|
195
|
+
//#region src/authz/composite-client-policy.d.ts
|
|
196
|
+
interface CompositeClientPolicyOptions {
|
|
197
|
+
/** Used when the request carries NO `client_id` (Tier-1 public/loopback CLI). */
|
|
198
|
+
loopback: ClientRedirectPolicy;
|
|
199
|
+
/** Used when the request carries a `client_id` (Tier-2 registered service). */
|
|
200
|
+
registered: ClientRedirectPolicy;
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Runs Tier-1 and Tier-2 side by side, dispatching on the **presence of
|
|
204
|
+
* `client_id`** (AUTH-SERVER.md §10): a request with a `client_id` is a
|
|
205
|
+
* registered client, one without is a loopback CLI. The split is the safety
|
|
206
|
+
* boundary — a registered client is routed only to {@link RegisteredClientPolicy}
|
|
207
|
+
* (which enforces ITS redirect allowlist, so it cannot smuggle a loopback
|
|
208
|
+
* redirect), and a no-`client_id` request is routed only to the loopback policy
|
|
209
|
+
* (so it cannot claim to be a registered client). Each sub-policy still owns its
|
|
210
|
+
* own redirect validation; this only picks which one runs.
|
|
211
|
+
*/
|
|
212
|
+
declare class CompositeClientPolicy implements ClientRedirectPolicy {
|
|
213
|
+
private readonly loopback;
|
|
214
|
+
private readonly registered;
|
|
215
|
+
constructor(opts: CompositeClientPolicyOptions);
|
|
216
|
+
resolveClient(args: {
|
|
217
|
+
clientId?: string;
|
|
218
|
+
redirectUri: string;
|
|
219
|
+
scope?: string;
|
|
220
|
+
}): ResolvedClient | Promise<ResolvedClient>;
|
|
221
|
+
authenticateClient(args: {
|
|
222
|
+
clientId?: string;
|
|
223
|
+
clientSecret?: string;
|
|
224
|
+
}): void | Promise<void>;
|
|
225
|
+
}
|
|
226
|
+
//#endregion
|
|
227
|
+
//#region src/authz/id-token-signer.d.ts
|
|
228
|
+
/** Asymmetric signing algorithms an OIDC `id_token` may use (AUTH-SERVER.md §4.9). */
|
|
229
|
+
type IdTokenAlg = "RS256" | "ES256";
|
|
230
|
+
interface IdTokenSignerOptions {
|
|
231
|
+
/**
|
|
232
|
+
* The OIDC issuer — the `iss` claim AND the discovery `issuer`. A relying
|
|
233
|
+
* `OidcProvider` checks `id_token.iss` against this exactly, so it must match
|
|
234
|
+
* the value it was configured with (typically `{origin}/auth`).
|
|
235
|
+
*/
|
|
236
|
+
issuer: string;
|
|
237
|
+
/** Signature algorithm. Default `"RS256"` (most universally accepted; `OidcProvider` accepts RS256/ES256). */
|
|
238
|
+
alg?: IdTokenAlg;
|
|
239
|
+
/** Key id — stamped in the JWS header AND the published JWKS entry, so a verifier matches the right key. */
|
|
240
|
+
kid: string;
|
|
241
|
+
/** PKCS8 PEM private key (lazily imported + cached). */
|
|
242
|
+
privateKey: string;
|
|
243
|
+
/** SPKI PEM public key — published in the JWKS so verifiers fetch it (lazily imported + cached). */
|
|
244
|
+
publicKey: string;
|
|
245
|
+
/** `id_token` lifetime in seconds. Default 300 (5 min — it is exchanged immediately). */
|
|
246
|
+
ttlSec?: number;
|
|
247
|
+
/** Injectable clock for deterministic `iat`/`exp` in tests. Defaults to wall-clock. */
|
|
248
|
+
clock?: Clock;
|
|
249
|
+
}
|
|
250
|
+
/** The claims the authorization server controls per-mint; profile claims ride `extra`. */
|
|
251
|
+
interface IdTokenClaims {
|
|
252
|
+
/** Subject — the stable user id (the token subject). */
|
|
253
|
+
sub: string;
|
|
254
|
+
/** Audience — the requesting `client_id`. Binds the token to one client (§6 audience binding). */
|
|
255
|
+
aud: string;
|
|
256
|
+
/** Echoed from the `/authorize` request when present; a relying party checks it to defeat replay. */
|
|
257
|
+
nonce?: string;
|
|
258
|
+
/** Per-mint lifetime override (seconds). */
|
|
259
|
+
ttlSec?: number;
|
|
260
|
+
/** Profile/standard claims merged into the payload (e.g. `email`, `email_verified`, `name`). MUST be JSON-safe. */
|
|
261
|
+
extra?: Record<string, unknown>;
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Signs OIDC `id_token`s and publishes the matching JWKS (AUTH-SERVER.md §4.9).
|
|
265
|
+
* Holds one asymmetric keypair; mints short-lived RS256/ES256 tokens with the
|
|
266
|
+
* issuer + audience + subject a relying `OidcProvider` validates, and exports the
|
|
267
|
+
* public half as a JWKS for `GET /auth/jwks`. Keys are imported lazily and cached
|
|
268
|
+
* (the Apple-client-secret pattern), so construction is cheap and synchronous.
|
|
269
|
+
*
|
|
270
|
+
* Never used for the access token (that stays in `AuthCredential`'s store) — the
|
|
271
|
+
* `id_token` is a separate, audience-bound identity assertion.
|
|
272
|
+
*/
|
|
273
|
+
declare class IdTokenSigner {
|
|
274
|
+
/** The `iss` claim + discovery `issuer` (read by `/.well-known/openid-configuration`). */
|
|
275
|
+
readonly issuer: string;
|
|
276
|
+
/** Signature algorithm — published in discovery's `id_token_signing_alg_values_supported`. */
|
|
277
|
+
readonly alg: IdTokenAlg;
|
|
278
|
+
/** Key id — the JWS header + JWKS entry `kid`. */
|
|
279
|
+
readonly kid: string;
|
|
280
|
+
private readonly privateKeyPem;
|
|
281
|
+
private readonly publicKeyPem;
|
|
282
|
+
private readonly ttlSec;
|
|
283
|
+
private readonly clock;
|
|
284
|
+
private privateKeyPromise?;
|
|
285
|
+
private jwksPromise?;
|
|
286
|
+
constructor(opts: IdTokenSignerOptions);
|
|
287
|
+
/** Mint a signed `id_token` JWT. Iat/exp come from the injected clock. */
|
|
288
|
+
sign(claims: IdTokenClaims): Promise<string>;
|
|
289
|
+
/**
|
|
290
|
+
* The JWKS document served at `/auth/jwks` — the public key as a single
|
|
291
|
+
* `use: "sig"` entry tagged with the same `kid`/`alg` as the minted tokens, so
|
|
292
|
+
* a verifier selects it by `kid`. Computed once and cached.
|
|
293
|
+
*/
|
|
294
|
+
jwks(): Promise<{
|
|
295
|
+
keys: JWK[];
|
|
296
|
+
}>;
|
|
297
|
+
private importPrivateKey;
|
|
298
|
+
}
|
|
299
|
+
//#endregion
|
|
300
|
+
//#region src/authz/oidc-claims-resolver.d.ts
|
|
301
|
+
/**
|
|
302
|
+
* Resolves the OIDC profile claims to embed in an `id_token` for a given user +
|
|
303
|
+
* granted scope (AUTH-SERVER.md §4.9). The authorization server already controls
|
|
304
|
+
* the registered claims (`iss`/`aud`/`sub`/`iat`/`exp`/`nonce`) itself — this seam
|
|
305
|
+
* supplies the **profile** claims that depend on the consumer's user shape
|
|
306
|
+
* (`email`/`email_verified`/`name`/`picture`), which `@aooth/auth` cannot know.
|
|
307
|
+
*
|
|
308
|
+
* Pluggable like every other store/policy: the no-op default ({@link NoopOidcClaimsResolver},
|
|
309
|
+
* `sub`-only tokens) ships, and a consumer subclasses it to read its own user
|
|
310
|
+
* record. Bound in `@aooth/auth-moost` under `OIDC_CLAIMS_RESOLVER_TOKEN`.
|
|
311
|
+
*
|
|
312
|
+
* The map's keys are standard OIDC claim names; values MUST be JSON-safe. Honour
|
|
313
|
+
* the granted `scope` — only emit `email`/`email_verified` under `email`, and
|
|
314
|
+
* `name`/`picture`/etc. under `profile`, so a client receives only what it asked
|
|
315
|
+
* for (and was allowed).
|
|
316
|
+
*/
|
|
317
|
+
declare abstract class OidcClaimsResolver {
|
|
318
|
+
/**
|
|
319
|
+
* @param userId the authenticated subject (the `id_token` `sub`).
|
|
320
|
+
* @param scope the granted scope, space-joined (e.g. `"openid email profile"`), or undefined.
|
|
321
|
+
* @returns a flat map of standard OIDC profile claims to merge into the `id_token`.
|
|
322
|
+
*/
|
|
323
|
+
abstract resolveClaims(userId: string, scope: string | undefined): Record<string, unknown> | Promise<Record<string, unknown>>;
|
|
324
|
+
}
|
|
325
|
+
/** Default resolver: emits no profile claims, so the `id_token` carries only `sub` + the registered claims. */
|
|
326
|
+
declare class NoopOidcClaimsResolver extends OidcClaimsResolver {
|
|
327
|
+
resolveClaims(): Record<string, unknown>;
|
|
328
|
+
}
|
|
329
|
+
/** `true` when `scope` (space-joined) grants `claim` — `"email"`/`"profile"` etc. */
|
|
330
|
+
declare function scopeGrants(scope: string | undefined, claim: string): boolean;
|
|
331
|
+
//#endregion
|
|
332
|
+
//#region src/authz/pending-authorization-store.d.ts
|
|
333
|
+
/**
|
|
334
|
+
* One in-flight authorization request, recorded at `GET /auth/authorize` and
|
|
335
|
+
* read once at the login-workflow terminal that mints the auth code. Keyed by an
|
|
336
|
+
* opaque `handle` that rides the login-wf ctx (and, across a "Continue with
|
|
337
|
+
* Google" detour, the federated signed `state`) — so nothing secret leaves the
|
|
338
|
+
* server.
|
|
339
|
+
*/
|
|
340
|
+
interface PendingAuthorization {
|
|
341
|
+
/** Opaque server-side handle (the only thing that rides the URL / wf state). */
|
|
342
|
+
handle: string;
|
|
343
|
+
/** Registered client id (Tier 2), absent for a public/loopback client. */
|
|
344
|
+
clientId?: string;
|
|
345
|
+
/** The client's validated `redirect_uri` — where the code is delivered. */
|
|
346
|
+
redirectUri: string;
|
|
347
|
+
/** PKCE S256 challenge (client-generated); verified against the verifier at `/token`. */
|
|
348
|
+
codeChallenge: string;
|
|
349
|
+
/** The client's `state`, echoed back on the redirect so the client can correlate. */
|
|
350
|
+
clientState?: string;
|
|
351
|
+
/** Granted scope (space-joined) — `requested ∩ allowed`; drives the `id_token` profile claims. */
|
|
352
|
+
scope?: string;
|
|
353
|
+
/** OIDC `nonce` from the authorize request — echoed into the `id_token` (Tier 2). */
|
|
354
|
+
nonce?: string;
|
|
355
|
+
/** Mint an `id_token` at `/token` (Tier 2). */
|
|
356
|
+
idToken?: boolean;
|
|
357
|
+
/** Mint an access token at `/token`. Omitted ⇒ minted (Tier-1 loopback). */
|
|
358
|
+
accessToken?: boolean;
|
|
359
|
+
/** The `id_token` `aud` (the registered `client_id`). */
|
|
360
|
+
audience?: string;
|
|
361
|
+
/** What the grant will mint (fixed at authorize time). */
|
|
362
|
+
tokenPolicy: TokenPolicy;
|
|
363
|
+
createdAt: number;
|
|
364
|
+
expiresAt: number;
|
|
365
|
+
}
|
|
366
|
+
/** Input to {@link PendingAuthorizationStore.create} — `handle`/timestamps are store-assigned. */
|
|
367
|
+
interface NewPendingAuthorization {
|
|
368
|
+
clientId?: string;
|
|
369
|
+
redirectUri: string;
|
|
370
|
+
codeChallenge: string;
|
|
371
|
+
clientState?: string;
|
|
372
|
+
scope?: string;
|
|
373
|
+
nonce?: string;
|
|
374
|
+
idToken?: boolean;
|
|
375
|
+
accessToken?: boolean;
|
|
376
|
+
audience?: string;
|
|
377
|
+
tokenPolicy: TokenPolicy;
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Storage seam for in-flight authorizations (AUTH-SERVER.md §4.3). Short-lived
|
|
381
|
+
* (≈ the login-session ceiling): created at `/authorize`, read+deleted at the
|
|
382
|
+
* wf terminal. An in-memory impl ships for single-process apps + tests; a
|
|
383
|
+
* multi-pod deployment provides a durable (e.g. Redis) impl under the same
|
|
384
|
+
* `PENDING_AUTHORIZATION_STORE_TOKEN` (from `@aooth/auth-moost`).
|
|
385
|
+
*/
|
|
386
|
+
declare abstract class PendingAuthorizationStore {
|
|
387
|
+
/** Record a new pending authorization; returns its opaque `handle`. */
|
|
388
|
+
abstract create(rec: NewPendingAuthorization): Promise<{
|
|
389
|
+
handle: string;
|
|
390
|
+
}>;
|
|
391
|
+
/** Fetch by handle, or `null` when unknown/expired. */
|
|
392
|
+
abstract get(handle: string): Promise<PendingAuthorization | null>;
|
|
393
|
+
/** Drop a handle once consumed. Returns `true` when a row was removed. */
|
|
394
|
+
abstract delete(handle: string): Promise<boolean>;
|
|
395
|
+
}
|
|
396
|
+
interface PendingAuthorizationStoreMemoryOptions {
|
|
397
|
+
/** Injectable clock for deterministic expiry. Defaults to {@link defaultClock}. */
|
|
398
|
+
clock?: Clock;
|
|
399
|
+
/** How long a pending authorization stays valid. Default 15 min. */
|
|
400
|
+
ttlMs?: number;
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* In-memory {@link PendingAuthorizationStore} — the reference impl for a
|
|
404
|
+
* single-process app + tests. `structuredClone` on read/write isolates callers.
|
|
405
|
+
*/
|
|
406
|
+
declare class PendingAuthorizationStoreMemory extends PendingAuthorizationStore {
|
|
407
|
+
private store;
|
|
408
|
+
private clock;
|
|
409
|
+
private ttlMs;
|
|
410
|
+
constructor(opts?: PendingAuthorizationStoreMemoryOptions);
|
|
411
|
+
create(rec: NewPendingAuthorization): Promise<{
|
|
412
|
+
handle: string;
|
|
413
|
+
}>;
|
|
414
|
+
get(handle: string): Promise<PendingAuthorization | null>;
|
|
415
|
+
delete(handle: string): Promise<boolean>;
|
|
416
|
+
}
|
|
417
|
+
//#endregion
|
|
418
|
+
//#region src/authz/auth-code-store.d.ts
|
|
419
|
+
/**
|
|
420
|
+
* A minted, single-use authorization code, bound to the user the login resolved
|
|
421
|
+
* plus the PKCE challenge / redirect / token policy recorded at `/authorize`.
|
|
422
|
+
* Consumed atomically at `POST /auth/token` — the token is minted there, off the
|
|
423
|
+
* browser, so nothing long-lived ever rides a redirect URL.
|
|
424
|
+
*/
|
|
425
|
+
interface AuthCode {
|
|
426
|
+
/** Opaque single-use code (delivered to the client in the redirect query). */
|
|
427
|
+
code: string;
|
|
428
|
+
/** The user the login workflow authenticated. */
|
|
429
|
+
userId: string;
|
|
430
|
+
/** PKCE S256 challenge from the originating authorize request. */
|
|
431
|
+
codeChallenge: string;
|
|
432
|
+
/** The client's `redirect_uri` (bound; the code is meaningless elsewhere). */
|
|
433
|
+
redirectUri: string;
|
|
434
|
+
/** Registered client id (Tier 2), absent for a public/loopback client. */
|
|
435
|
+
clientId?: string;
|
|
436
|
+
/** Granted scope (space-joined) — drives the `id_token` profile claims. */
|
|
437
|
+
scope?: string;
|
|
438
|
+
/** OIDC `nonce` from the authorize request — echoed into the `id_token` (Tier 2). */
|
|
439
|
+
nonce?: string;
|
|
440
|
+
/** Mint an `id_token` at `/token` (Tier 2). */
|
|
441
|
+
idToken?: boolean;
|
|
442
|
+
/** Mint an access token at `/token`. Omitted ⇒ minted (Tier-1 loopback). */
|
|
443
|
+
accessToken?: boolean;
|
|
444
|
+
/** The `id_token` `aud` (the registered `client_id`). */
|
|
445
|
+
audience?: string;
|
|
446
|
+
/** What `/token` mints when this code is redeemed. */
|
|
447
|
+
tokenPolicy: TokenPolicy;
|
|
448
|
+
expiresAt: number;
|
|
449
|
+
}
|
|
450
|
+
/** Input to {@link AuthCodeStore.mint} — `code`/`expiresAt` are store-assigned. */
|
|
451
|
+
interface NewAuthCode {
|
|
452
|
+
userId: string;
|
|
453
|
+
codeChallenge: string;
|
|
454
|
+
redirectUri: string;
|
|
455
|
+
clientId?: string;
|
|
456
|
+
scope?: string;
|
|
457
|
+
nonce?: string;
|
|
458
|
+
idToken?: boolean;
|
|
459
|
+
accessToken?: boolean;
|
|
460
|
+
audience?: string;
|
|
461
|
+
tokenPolicy: TokenPolicy;
|
|
462
|
+
}
|
|
463
|
+
/**
|
|
464
|
+
* Storage seam for issued authorization codes (AUTH-SERVER.md §4.3). Very
|
|
465
|
+
* short-lived (≈ 30–60 s) and **single-use**: {@link consume} returns the row
|
|
466
|
+
* AND invalidates it in one atomic step, so a concurrent double-redeem (or a
|
|
467
|
+
* back-button replay) yields the code to exactly one caller. An in-memory impl
|
|
468
|
+
* ships (atomic for free in single-threaded JS); a durable impl must implement
|
|
469
|
+
* `consume` as an atomic check-and-delete (e.g. `withCas` / `@db.column.version`,
|
|
470
|
+
* or a Redis `GETDEL`).
|
|
471
|
+
*/
|
|
472
|
+
declare abstract class AuthCodeStore {
|
|
473
|
+
/** Mint + store a single-use code; returns the opaque code string. */
|
|
474
|
+
abstract mint(rec: NewAuthCode): Promise<{
|
|
475
|
+
code: string;
|
|
476
|
+
}>;
|
|
477
|
+
/** Atomically claim + return the code's row, or `null` on miss / reuse / expiry. */
|
|
478
|
+
abstract consume(code: string): Promise<AuthCode | null>;
|
|
479
|
+
}
|
|
480
|
+
interface AuthCodeStoreMemoryOptions {
|
|
481
|
+
/** Injectable clock for deterministic expiry. Defaults to {@link defaultClock}. */
|
|
482
|
+
clock?: Clock;
|
|
483
|
+
/** Code lifetime. Default 60 s. */
|
|
484
|
+
ttlMs?: number;
|
|
485
|
+
}
|
|
486
|
+
/**
|
|
487
|
+
* In-memory {@link AuthCodeStore} — the reference impl. `consume` is atomic
|
|
488
|
+
* because the `get` + `delete` run with no intervening `await`, so a second
|
|
489
|
+
* concurrent `consume` of the same code always misses.
|
|
490
|
+
*/
|
|
491
|
+
declare class AuthCodeStoreMemory extends AuthCodeStore {
|
|
492
|
+
private store;
|
|
493
|
+
private clock;
|
|
494
|
+
private ttlMs;
|
|
495
|
+
constructor(opts?: AuthCodeStoreMemoryOptions);
|
|
496
|
+
mint(rec: NewAuthCode): Promise<{
|
|
497
|
+
code: string;
|
|
498
|
+
}>;
|
|
499
|
+
consume(code: string): Promise<AuthCode | null>;
|
|
500
|
+
}
|
|
501
|
+
//#endregion
|
|
502
|
+
export { type AuthCode, AuthCodeStore, AuthCodeStoreMemory, type AuthCodeStoreMemoryOptions, AuthorizeError, type AuthorizeErrorCode, type ClientRedirectPolicy, CompositeClientPolicy, type CompositeClientPolicyOptions, type IdTokenAlg, type IdTokenClaims, IdTokenSigner, type IdTokenSignerOptions, LoopbackClientPolicy, type LoopbackClientPolicyOptions, type NewAuthCode, type NewPendingAuthorization, NoopOidcClaimsResolver, OidcClaimsResolver, type PendingAuthorization, PendingAuthorizationStore, PendingAuthorizationStoreMemory, type PendingAuthorizationStoreMemoryOptions, type RegisteredClient, RegisteredClientPolicy, type RegisteredClientPolicyOptions, type ResolvedClient, type TokenPolicy, isLoopbackRedirectUri, scopeGrants };
|