@aooth/auth 0.1.10 → 0.1.12
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 +136 -0
- package/dist/atscript-db.d.cts +129 -1
- package/dist/atscript-db.d.mts +129 -1
- package/dist/atscript-db.mjs +135 -1
- package/dist/auth-code-store-CmUhRrwG.cjs +151 -0
- package/dist/auth-code-store-HOHlz_3z.d.cts +199 -0
- package/dist/auth-code-store-NqEDfqrf.d.mts +199 -0
- package/dist/auth-code-store-uaoL7dvU.mjs +116 -0
- package/dist/authz.cjs +5 -115
- package/dist/authz.d.cts +1 -196
- package/dist/authz.d.mts +1 -196
- package/dist/authz.mjs +2 -112
- package/package.json +23 -3
- package/src/atscript-db/auth-code.as +41 -0
- package/src/atscript-db/auth-code.as.d.ts +68 -0
- package/src/atscript-db/pending-authorization.as +42 -0
- package/src/atscript-db/pending-authorization.as.d.ts +71 -0
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
const require_clock = require("./clock-Bl-H3eqE.cjs");
|
|
2
|
+
let node_crypto = require("node:crypto");
|
|
3
|
+
//#region src/authz/pending-authorization-store.ts
|
|
4
|
+
/**
|
|
5
|
+
* Storage seam for in-flight authorizations (AUTH-SERVER.md §4.3). Short-lived
|
|
6
|
+
* (≈ the login-session ceiling): created at `/authorize`, read+deleted at the
|
|
7
|
+
* wf terminal. An in-memory impl ships for single-process apps + tests; a
|
|
8
|
+
* multi-pod deployment provides a durable (e.g. Redis) impl under the same
|
|
9
|
+
* `PENDING_AUTHORIZATION_STORE_TOKEN` (from `@aooth/auth-moost`).
|
|
10
|
+
*/
|
|
11
|
+
var PendingAuthorizationStore = class {};
|
|
12
|
+
/** Default pending-authorization lifetime (15 min). Shared by the memory + atscript-db stores. */
|
|
13
|
+
const DEFAULT_PENDING_TTL_MS = 15 * 6e4;
|
|
14
|
+
/**
|
|
15
|
+
* In-memory {@link PendingAuthorizationStore} — the reference impl for a
|
|
16
|
+
* single-process app + tests. `structuredClone` on read/write isolates callers.
|
|
17
|
+
*/
|
|
18
|
+
var PendingAuthorizationStoreMemory = class extends PendingAuthorizationStore {
|
|
19
|
+
store = /* @__PURE__ */ new Map();
|
|
20
|
+
clock;
|
|
21
|
+
ttlMs;
|
|
22
|
+
constructor(opts) {
|
|
23
|
+
super();
|
|
24
|
+
this.clock = opts?.clock ?? require_clock.defaultClock;
|
|
25
|
+
this.ttlMs = opts?.ttlMs ?? 9e5;
|
|
26
|
+
}
|
|
27
|
+
async create(rec) {
|
|
28
|
+
const now = this.clock.now();
|
|
29
|
+
const row = {
|
|
30
|
+
handle: (0, node_crypto.randomUUID)(),
|
|
31
|
+
redirectUri: rec.redirectUri,
|
|
32
|
+
codeChallenge: rec.codeChallenge,
|
|
33
|
+
tokenPolicy: structuredClone(rec.tokenPolicy),
|
|
34
|
+
createdAt: now,
|
|
35
|
+
expiresAt: now + this.ttlMs,
|
|
36
|
+
...rec.clientId !== void 0 && { clientId: rec.clientId },
|
|
37
|
+
...rec.clientState !== void 0 && { clientState: rec.clientState },
|
|
38
|
+
...rec.scope !== void 0 && { scope: rec.scope },
|
|
39
|
+
...rec.nonce !== void 0 && { nonce: rec.nonce },
|
|
40
|
+
...rec.idToken !== void 0 && { idToken: rec.idToken },
|
|
41
|
+
...rec.accessToken !== void 0 && { accessToken: rec.accessToken },
|
|
42
|
+
...rec.audience !== void 0 && { audience: rec.audience }
|
|
43
|
+
};
|
|
44
|
+
this.store.set(row.handle, structuredClone(row));
|
|
45
|
+
return { handle: row.handle };
|
|
46
|
+
}
|
|
47
|
+
async get(handle) {
|
|
48
|
+
const row = this.store.get(handle);
|
|
49
|
+
if (!row) return null;
|
|
50
|
+
if (row.expiresAt <= this.clock.now()) {
|
|
51
|
+
this.store.delete(handle);
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
return structuredClone(row);
|
|
55
|
+
}
|
|
56
|
+
async delete(handle) {
|
|
57
|
+
return this.store.delete(handle);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
//#endregion
|
|
61
|
+
//#region src/authz/auth-code-store.ts
|
|
62
|
+
/**
|
|
63
|
+
* Storage seam for issued authorization codes (AUTH-SERVER.md §4.3). Very
|
|
64
|
+
* short-lived (≈ 30–60 s) and **single-use**: {@link consume} returns the row
|
|
65
|
+
* AND invalidates it in one atomic step, so a concurrent double-redeem (or a
|
|
66
|
+
* back-button replay) yields the code to exactly one caller. An in-memory impl
|
|
67
|
+
* ships (atomic for free in single-threaded JS); a durable impl must implement
|
|
68
|
+
* `consume` as an atomic check-and-delete (e.g. `withCas` / `@db.column.version`,
|
|
69
|
+
* or a Redis `GETDEL`).
|
|
70
|
+
*/
|
|
71
|
+
var AuthCodeStore = class {};
|
|
72
|
+
/** Default auth-code lifetime (60 s). Shared by the memory + atscript-db stores. */
|
|
73
|
+
const DEFAULT_CODE_TTL_MS = 6e4;
|
|
74
|
+
/**
|
|
75
|
+
* In-memory {@link AuthCodeStore} — the reference impl. `consume` is atomic
|
|
76
|
+
* because the `get` + `delete` run with no intervening `await`, so a second
|
|
77
|
+
* concurrent `consume` of the same code always misses.
|
|
78
|
+
*/
|
|
79
|
+
var AuthCodeStoreMemory = class extends AuthCodeStore {
|
|
80
|
+
store = /* @__PURE__ */ new Map();
|
|
81
|
+
clock;
|
|
82
|
+
ttlMs;
|
|
83
|
+
constructor(opts) {
|
|
84
|
+
super();
|
|
85
|
+
this.clock = opts?.clock ?? require_clock.defaultClock;
|
|
86
|
+
this.ttlMs = opts?.ttlMs ?? 6e4;
|
|
87
|
+
}
|
|
88
|
+
async mint(rec) {
|
|
89
|
+
const code = (0, node_crypto.randomUUID)();
|
|
90
|
+
const row = {
|
|
91
|
+
code,
|
|
92
|
+
userId: rec.userId,
|
|
93
|
+
codeChallenge: rec.codeChallenge,
|
|
94
|
+
redirectUri: rec.redirectUri,
|
|
95
|
+
tokenPolicy: structuredClone(rec.tokenPolicy),
|
|
96
|
+
expiresAt: this.clock.now() + this.ttlMs,
|
|
97
|
+
...rec.clientId !== void 0 && { clientId: rec.clientId },
|
|
98
|
+
...rec.scope !== void 0 && { scope: rec.scope },
|
|
99
|
+
...rec.nonce !== void 0 && { nonce: rec.nonce },
|
|
100
|
+
...rec.idToken !== void 0 && { idToken: rec.idToken },
|
|
101
|
+
...rec.accessToken !== void 0 && { accessToken: rec.accessToken },
|
|
102
|
+
...rec.audience !== void 0 && { audience: rec.audience }
|
|
103
|
+
};
|
|
104
|
+
this.store.set(code, structuredClone(row));
|
|
105
|
+
return { code };
|
|
106
|
+
}
|
|
107
|
+
async consume(code) {
|
|
108
|
+
const row = this.store.get(code);
|
|
109
|
+
if (!row) return null;
|
|
110
|
+
this.store.delete(code);
|
|
111
|
+
if (row.expiresAt <= this.clock.now()) return null;
|
|
112
|
+
return structuredClone(row);
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
//#endregion
|
|
116
|
+
Object.defineProperty(exports, "AuthCodeStore", {
|
|
117
|
+
enumerable: true,
|
|
118
|
+
get: function() {
|
|
119
|
+
return AuthCodeStore;
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
Object.defineProperty(exports, "AuthCodeStoreMemory", {
|
|
123
|
+
enumerable: true,
|
|
124
|
+
get: function() {
|
|
125
|
+
return AuthCodeStoreMemory;
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
Object.defineProperty(exports, "DEFAULT_CODE_TTL_MS", {
|
|
129
|
+
enumerable: true,
|
|
130
|
+
get: function() {
|
|
131
|
+
return DEFAULT_CODE_TTL_MS;
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
Object.defineProperty(exports, "DEFAULT_PENDING_TTL_MS", {
|
|
135
|
+
enumerable: true,
|
|
136
|
+
get: function() {
|
|
137
|
+
return DEFAULT_PENDING_TTL_MS;
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
Object.defineProperty(exports, "PendingAuthorizationStore", {
|
|
141
|
+
enumerable: true,
|
|
142
|
+
get: function() {
|
|
143
|
+
return PendingAuthorizationStore;
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
Object.defineProperty(exports, "PendingAuthorizationStoreMemory", {
|
|
147
|
+
enumerable: true,
|
|
148
|
+
get: function() {
|
|
149
|
+
return PendingAuthorizationStoreMemory;
|
|
150
|
+
}
|
|
151
|
+
});
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { t as Clock } from "./clock-BjXa0LXb.cjs";
|
|
2
|
+
|
|
3
|
+
//#region src/authz/token-policy.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* What the authorization server mints for a completed grant — forwarded verbatim
|
|
6
|
+
* into `AuthCredential.issue(userId, …)` at the token endpoint. Decided by the
|
|
7
|
+
* {@link import("./client-policy").ClientRedirectPolicy} (per client / scope),
|
|
8
|
+
* never by the client request, and recorded on the pending-authorization + the
|
|
9
|
+
* issued auth-code so the grant's authority is fixed at `/authorize` time, not
|
|
10
|
+
* `/token` time.
|
|
11
|
+
*
|
|
12
|
+
* Tier 1 (CLI / loopback) → a full-authority `cli-session` (`{ kind, ttl }`, no
|
|
13
|
+
* `payload`). A scoped service token (Tier 2) additionally sets `payload` with
|
|
14
|
+
* the consumer's attenuation root fields.
|
|
15
|
+
*/
|
|
16
|
+
interface TokenPolicy {
|
|
17
|
+
/** Semantic credential kind stamped on the token (e.g. `"cli-session"`). See `IssueOptions.kind`. */
|
|
18
|
+
kind?: string;
|
|
19
|
+
/** Per-mint access-token lifetime in ms (forwarded to `IssueOptions.ttl`). */
|
|
20
|
+
ttl?: number;
|
|
21
|
+
/**
|
|
22
|
+
* Extra root payload merged into `issue()` — e.g. `@arbac.attenuate.*` fields
|
|
23
|
+
* for a SCOPED token. Omit for a full-authority token. MUST be JSON-safe: it
|
|
24
|
+
* is persisted on the pending-authorization + auth-code records.
|
|
25
|
+
*/
|
|
26
|
+
payload?: Record<string, unknown>;
|
|
27
|
+
}
|
|
28
|
+
//#endregion
|
|
29
|
+
//#region src/authz/pending-authorization-store.d.ts
|
|
30
|
+
/**
|
|
31
|
+
* One in-flight authorization request, recorded at `GET /auth/authorize` and
|
|
32
|
+
* read once at the login-workflow terminal that mints the auth code. Keyed by an
|
|
33
|
+
* opaque `handle` that rides the login-wf ctx (and, across a "Continue with
|
|
34
|
+
* Google" detour, the federated signed `state`) — so nothing secret leaves the
|
|
35
|
+
* server.
|
|
36
|
+
*/
|
|
37
|
+
interface PendingAuthorization {
|
|
38
|
+
/** Opaque server-side handle (the only thing that rides the URL / wf state). */
|
|
39
|
+
handle: string;
|
|
40
|
+
/** Registered client id (Tier 2), absent for a public/loopback client. */
|
|
41
|
+
clientId?: string;
|
|
42
|
+
/** The client's validated `redirect_uri` — where the code is delivered. */
|
|
43
|
+
redirectUri: string;
|
|
44
|
+
/** PKCE S256 challenge (client-generated); verified against the verifier at `/token`. */
|
|
45
|
+
codeChallenge: string;
|
|
46
|
+
/** The client's `state`, echoed back on the redirect so the client can correlate. */
|
|
47
|
+
clientState?: string;
|
|
48
|
+
/** Granted scope (space-joined) — `requested ∩ allowed`; drives the `id_token` profile claims. */
|
|
49
|
+
scope?: string;
|
|
50
|
+
/** OIDC `nonce` from the authorize request — echoed into the `id_token` (Tier 2). */
|
|
51
|
+
nonce?: string;
|
|
52
|
+
/** Mint an `id_token` at `/token` (Tier 2). */
|
|
53
|
+
idToken?: boolean;
|
|
54
|
+
/** Mint an access token at `/token`. Omitted ⇒ minted (Tier-1 loopback). */
|
|
55
|
+
accessToken?: boolean;
|
|
56
|
+
/** The `id_token` `aud` (the registered `client_id`). */
|
|
57
|
+
audience?: string;
|
|
58
|
+
/** What the grant will mint (fixed at authorize time). */
|
|
59
|
+
tokenPolicy: TokenPolicy;
|
|
60
|
+
createdAt: number;
|
|
61
|
+
expiresAt: number;
|
|
62
|
+
}
|
|
63
|
+
/** Input to {@link PendingAuthorizationStore.create} — `handle`/timestamps are store-assigned. */
|
|
64
|
+
interface NewPendingAuthorization {
|
|
65
|
+
clientId?: string;
|
|
66
|
+
redirectUri: string;
|
|
67
|
+
codeChallenge: string;
|
|
68
|
+
clientState?: string;
|
|
69
|
+
scope?: string;
|
|
70
|
+
nonce?: string;
|
|
71
|
+
idToken?: boolean;
|
|
72
|
+
accessToken?: boolean;
|
|
73
|
+
audience?: string;
|
|
74
|
+
tokenPolicy: TokenPolicy;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Storage seam for in-flight authorizations (AUTH-SERVER.md §4.3). Short-lived
|
|
78
|
+
* (≈ the login-session ceiling): created at `/authorize`, read+deleted at the
|
|
79
|
+
* wf terminal. An in-memory impl ships for single-process apps + tests; a
|
|
80
|
+
* multi-pod deployment provides a durable (e.g. Redis) impl under the same
|
|
81
|
+
* `PENDING_AUTHORIZATION_STORE_TOKEN` (from `@aooth/auth-moost`).
|
|
82
|
+
*/
|
|
83
|
+
declare abstract class PendingAuthorizationStore {
|
|
84
|
+
/** Record a new pending authorization; returns its opaque `handle`. */
|
|
85
|
+
abstract create(rec: NewPendingAuthorization): Promise<{
|
|
86
|
+
handle: string;
|
|
87
|
+
}>;
|
|
88
|
+
/** Fetch by handle, or `null` when unknown/expired. */
|
|
89
|
+
abstract get(handle: string): Promise<PendingAuthorization | null>;
|
|
90
|
+
/** Drop a handle once consumed. Returns `true` when a row was removed. */
|
|
91
|
+
abstract delete(handle: string): Promise<boolean>;
|
|
92
|
+
}
|
|
93
|
+
interface PendingAuthorizationStoreMemoryOptions {
|
|
94
|
+
/** Injectable clock for deterministic expiry. Defaults to {@link defaultClock}. */
|
|
95
|
+
clock?: Clock;
|
|
96
|
+
/** How long a pending authorization stays valid. Default 15 min. */
|
|
97
|
+
ttlMs?: number;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* In-memory {@link PendingAuthorizationStore} — the reference impl for a
|
|
101
|
+
* single-process app + tests. `structuredClone` on read/write isolates callers.
|
|
102
|
+
*/
|
|
103
|
+
declare class PendingAuthorizationStoreMemory extends PendingAuthorizationStore {
|
|
104
|
+
private store;
|
|
105
|
+
private clock;
|
|
106
|
+
private ttlMs;
|
|
107
|
+
constructor(opts?: PendingAuthorizationStoreMemoryOptions);
|
|
108
|
+
create(rec: NewPendingAuthorization): Promise<{
|
|
109
|
+
handle: string;
|
|
110
|
+
}>;
|
|
111
|
+
get(handle: string): Promise<PendingAuthorization | null>;
|
|
112
|
+
delete(handle: string): Promise<boolean>;
|
|
113
|
+
}
|
|
114
|
+
//#endregion
|
|
115
|
+
//#region src/authz/auth-code-store.d.ts
|
|
116
|
+
/**
|
|
117
|
+
* A minted, single-use authorization code, bound to the user the login resolved
|
|
118
|
+
* plus the PKCE challenge / redirect / token policy recorded at `/authorize`.
|
|
119
|
+
* Consumed atomically at `POST /auth/token` — the token is minted there, off the
|
|
120
|
+
* browser, so nothing long-lived ever rides a redirect URL.
|
|
121
|
+
*/
|
|
122
|
+
interface AuthCode {
|
|
123
|
+
/** Opaque single-use code (delivered to the client in the redirect query). */
|
|
124
|
+
code: string;
|
|
125
|
+
/** The user the login workflow authenticated. */
|
|
126
|
+
userId: string;
|
|
127
|
+
/** PKCE S256 challenge from the originating authorize request. */
|
|
128
|
+
codeChallenge: string;
|
|
129
|
+
/** The client's `redirect_uri` (bound; the code is meaningless elsewhere). */
|
|
130
|
+
redirectUri: string;
|
|
131
|
+
/** Registered client id (Tier 2), absent for a public/loopback client. */
|
|
132
|
+
clientId?: string;
|
|
133
|
+
/** Granted scope (space-joined) — drives the `id_token` profile claims. */
|
|
134
|
+
scope?: string;
|
|
135
|
+
/** OIDC `nonce` from the authorize request — echoed into the `id_token` (Tier 2). */
|
|
136
|
+
nonce?: string;
|
|
137
|
+
/** Mint an `id_token` at `/token` (Tier 2). */
|
|
138
|
+
idToken?: boolean;
|
|
139
|
+
/** Mint an access token at `/token`. Omitted ⇒ minted (Tier-1 loopback). */
|
|
140
|
+
accessToken?: boolean;
|
|
141
|
+
/** The `id_token` `aud` (the registered `client_id`). */
|
|
142
|
+
audience?: string;
|
|
143
|
+
/** What `/token` mints when this code is redeemed. */
|
|
144
|
+
tokenPolicy: TokenPolicy;
|
|
145
|
+
expiresAt: number;
|
|
146
|
+
}
|
|
147
|
+
/** Input to {@link AuthCodeStore.mint} — `code`/`expiresAt` are store-assigned. */
|
|
148
|
+
interface NewAuthCode {
|
|
149
|
+
userId: string;
|
|
150
|
+
codeChallenge: string;
|
|
151
|
+
redirectUri: string;
|
|
152
|
+
clientId?: string;
|
|
153
|
+
scope?: string;
|
|
154
|
+
nonce?: string;
|
|
155
|
+
idToken?: boolean;
|
|
156
|
+
accessToken?: boolean;
|
|
157
|
+
audience?: string;
|
|
158
|
+
tokenPolicy: TokenPolicy;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Storage seam for issued authorization codes (AUTH-SERVER.md §4.3). Very
|
|
162
|
+
* short-lived (≈ 30–60 s) and **single-use**: {@link consume} returns the row
|
|
163
|
+
* AND invalidates it in one atomic step, so a concurrent double-redeem (or a
|
|
164
|
+
* back-button replay) yields the code to exactly one caller. An in-memory impl
|
|
165
|
+
* ships (atomic for free in single-threaded JS); a durable impl must implement
|
|
166
|
+
* `consume` as an atomic check-and-delete (e.g. `withCas` / `@db.column.version`,
|
|
167
|
+
* or a Redis `GETDEL`).
|
|
168
|
+
*/
|
|
169
|
+
declare abstract class AuthCodeStore {
|
|
170
|
+
/** Mint + store a single-use code; returns the opaque code string. */
|
|
171
|
+
abstract mint(rec: NewAuthCode): Promise<{
|
|
172
|
+
code: string;
|
|
173
|
+
}>;
|
|
174
|
+
/** Atomically claim + return the code's row, or `null` on miss / reuse / expiry. */
|
|
175
|
+
abstract consume(code: string): Promise<AuthCode | null>;
|
|
176
|
+
}
|
|
177
|
+
interface AuthCodeStoreMemoryOptions {
|
|
178
|
+
/** Injectable clock for deterministic expiry. Defaults to {@link defaultClock}. */
|
|
179
|
+
clock?: Clock;
|
|
180
|
+
/** Code lifetime. Default 60 s. */
|
|
181
|
+
ttlMs?: number;
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* In-memory {@link AuthCodeStore} — the reference impl. `consume` is atomic
|
|
185
|
+
* because the `get` + `delete` run with no intervening `await`, so a second
|
|
186
|
+
* concurrent `consume` of the same code always misses.
|
|
187
|
+
*/
|
|
188
|
+
declare class AuthCodeStoreMemory extends AuthCodeStore {
|
|
189
|
+
private store;
|
|
190
|
+
private clock;
|
|
191
|
+
private ttlMs;
|
|
192
|
+
constructor(opts?: AuthCodeStoreMemoryOptions);
|
|
193
|
+
mint(rec: NewAuthCode): Promise<{
|
|
194
|
+
code: string;
|
|
195
|
+
}>;
|
|
196
|
+
consume(code: string): Promise<AuthCode | null>;
|
|
197
|
+
}
|
|
198
|
+
//#endregion
|
|
199
|
+
export { NewAuthCode as a, PendingAuthorizationStore as c, TokenPolicy as d, AuthCodeStoreMemoryOptions as i, PendingAuthorizationStoreMemory as l, AuthCodeStore as n, NewPendingAuthorization as o, AuthCodeStoreMemory as r, PendingAuthorization as s, AuthCode as t, PendingAuthorizationStoreMemoryOptions as u };
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { t as Clock } from "./clock-BjXa0LXb.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/authz/token-policy.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* What the authorization server mints for a completed grant — forwarded verbatim
|
|
6
|
+
* into `AuthCredential.issue(userId, …)` at the token endpoint. Decided by the
|
|
7
|
+
* {@link import("./client-policy").ClientRedirectPolicy} (per client / scope),
|
|
8
|
+
* never by the client request, and recorded on the pending-authorization + the
|
|
9
|
+
* issued auth-code so the grant's authority is fixed at `/authorize` time, not
|
|
10
|
+
* `/token` time.
|
|
11
|
+
*
|
|
12
|
+
* Tier 1 (CLI / loopback) → a full-authority `cli-session` (`{ kind, ttl }`, no
|
|
13
|
+
* `payload`). A scoped service token (Tier 2) additionally sets `payload` with
|
|
14
|
+
* the consumer's attenuation root fields.
|
|
15
|
+
*/
|
|
16
|
+
interface TokenPolicy {
|
|
17
|
+
/** Semantic credential kind stamped on the token (e.g. `"cli-session"`). See `IssueOptions.kind`. */
|
|
18
|
+
kind?: string;
|
|
19
|
+
/** Per-mint access-token lifetime in ms (forwarded to `IssueOptions.ttl`). */
|
|
20
|
+
ttl?: number;
|
|
21
|
+
/**
|
|
22
|
+
* Extra root payload merged into `issue()` — e.g. `@arbac.attenuate.*` fields
|
|
23
|
+
* for a SCOPED token. Omit for a full-authority token. MUST be JSON-safe: it
|
|
24
|
+
* is persisted on the pending-authorization + auth-code records.
|
|
25
|
+
*/
|
|
26
|
+
payload?: Record<string, unknown>;
|
|
27
|
+
}
|
|
28
|
+
//#endregion
|
|
29
|
+
//#region src/authz/pending-authorization-store.d.ts
|
|
30
|
+
/**
|
|
31
|
+
* One in-flight authorization request, recorded at `GET /auth/authorize` and
|
|
32
|
+
* read once at the login-workflow terminal that mints the auth code. Keyed by an
|
|
33
|
+
* opaque `handle` that rides the login-wf ctx (and, across a "Continue with
|
|
34
|
+
* Google" detour, the federated signed `state`) — so nothing secret leaves the
|
|
35
|
+
* server.
|
|
36
|
+
*/
|
|
37
|
+
interface PendingAuthorization {
|
|
38
|
+
/** Opaque server-side handle (the only thing that rides the URL / wf state). */
|
|
39
|
+
handle: string;
|
|
40
|
+
/** Registered client id (Tier 2), absent for a public/loopback client. */
|
|
41
|
+
clientId?: string;
|
|
42
|
+
/** The client's validated `redirect_uri` — where the code is delivered. */
|
|
43
|
+
redirectUri: string;
|
|
44
|
+
/** PKCE S256 challenge (client-generated); verified against the verifier at `/token`. */
|
|
45
|
+
codeChallenge: string;
|
|
46
|
+
/** The client's `state`, echoed back on the redirect so the client can correlate. */
|
|
47
|
+
clientState?: string;
|
|
48
|
+
/** Granted scope (space-joined) — `requested ∩ allowed`; drives the `id_token` profile claims. */
|
|
49
|
+
scope?: string;
|
|
50
|
+
/** OIDC `nonce` from the authorize request — echoed into the `id_token` (Tier 2). */
|
|
51
|
+
nonce?: string;
|
|
52
|
+
/** Mint an `id_token` at `/token` (Tier 2). */
|
|
53
|
+
idToken?: boolean;
|
|
54
|
+
/** Mint an access token at `/token`. Omitted ⇒ minted (Tier-1 loopback). */
|
|
55
|
+
accessToken?: boolean;
|
|
56
|
+
/** The `id_token` `aud` (the registered `client_id`). */
|
|
57
|
+
audience?: string;
|
|
58
|
+
/** What the grant will mint (fixed at authorize time). */
|
|
59
|
+
tokenPolicy: TokenPolicy;
|
|
60
|
+
createdAt: number;
|
|
61
|
+
expiresAt: number;
|
|
62
|
+
}
|
|
63
|
+
/** Input to {@link PendingAuthorizationStore.create} — `handle`/timestamps are store-assigned. */
|
|
64
|
+
interface NewPendingAuthorization {
|
|
65
|
+
clientId?: string;
|
|
66
|
+
redirectUri: string;
|
|
67
|
+
codeChallenge: string;
|
|
68
|
+
clientState?: string;
|
|
69
|
+
scope?: string;
|
|
70
|
+
nonce?: string;
|
|
71
|
+
idToken?: boolean;
|
|
72
|
+
accessToken?: boolean;
|
|
73
|
+
audience?: string;
|
|
74
|
+
tokenPolicy: TokenPolicy;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Storage seam for in-flight authorizations (AUTH-SERVER.md §4.3). Short-lived
|
|
78
|
+
* (≈ the login-session ceiling): created at `/authorize`, read+deleted at the
|
|
79
|
+
* wf terminal. An in-memory impl ships for single-process apps + tests; a
|
|
80
|
+
* multi-pod deployment provides a durable (e.g. Redis) impl under the same
|
|
81
|
+
* `PENDING_AUTHORIZATION_STORE_TOKEN` (from `@aooth/auth-moost`).
|
|
82
|
+
*/
|
|
83
|
+
declare abstract class PendingAuthorizationStore {
|
|
84
|
+
/** Record a new pending authorization; returns its opaque `handle`. */
|
|
85
|
+
abstract create(rec: NewPendingAuthorization): Promise<{
|
|
86
|
+
handle: string;
|
|
87
|
+
}>;
|
|
88
|
+
/** Fetch by handle, or `null` when unknown/expired. */
|
|
89
|
+
abstract get(handle: string): Promise<PendingAuthorization | null>;
|
|
90
|
+
/** Drop a handle once consumed. Returns `true` when a row was removed. */
|
|
91
|
+
abstract delete(handle: string): Promise<boolean>;
|
|
92
|
+
}
|
|
93
|
+
interface PendingAuthorizationStoreMemoryOptions {
|
|
94
|
+
/** Injectable clock for deterministic expiry. Defaults to {@link defaultClock}. */
|
|
95
|
+
clock?: Clock;
|
|
96
|
+
/** How long a pending authorization stays valid. Default 15 min. */
|
|
97
|
+
ttlMs?: number;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* In-memory {@link PendingAuthorizationStore} — the reference impl for a
|
|
101
|
+
* single-process app + tests. `structuredClone` on read/write isolates callers.
|
|
102
|
+
*/
|
|
103
|
+
declare class PendingAuthorizationStoreMemory extends PendingAuthorizationStore {
|
|
104
|
+
private store;
|
|
105
|
+
private clock;
|
|
106
|
+
private ttlMs;
|
|
107
|
+
constructor(opts?: PendingAuthorizationStoreMemoryOptions);
|
|
108
|
+
create(rec: NewPendingAuthorization): Promise<{
|
|
109
|
+
handle: string;
|
|
110
|
+
}>;
|
|
111
|
+
get(handle: string): Promise<PendingAuthorization | null>;
|
|
112
|
+
delete(handle: string): Promise<boolean>;
|
|
113
|
+
}
|
|
114
|
+
//#endregion
|
|
115
|
+
//#region src/authz/auth-code-store.d.ts
|
|
116
|
+
/**
|
|
117
|
+
* A minted, single-use authorization code, bound to the user the login resolved
|
|
118
|
+
* plus the PKCE challenge / redirect / token policy recorded at `/authorize`.
|
|
119
|
+
* Consumed atomically at `POST /auth/token` — the token is minted there, off the
|
|
120
|
+
* browser, so nothing long-lived ever rides a redirect URL.
|
|
121
|
+
*/
|
|
122
|
+
interface AuthCode {
|
|
123
|
+
/** Opaque single-use code (delivered to the client in the redirect query). */
|
|
124
|
+
code: string;
|
|
125
|
+
/** The user the login workflow authenticated. */
|
|
126
|
+
userId: string;
|
|
127
|
+
/** PKCE S256 challenge from the originating authorize request. */
|
|
128
|
+
codeChallenge: string;
|
|
129
|
+
/** The client's `redirect_uri` (bound; the code is meaningless elsewhere). */
|
|
130
|
+
redirectUri: string;
|
|
131
|
+
/** Registered client id (Tier 2), absent for a public/loopback client. */
|
|
132
|
+
clientId?: string;
|
|
133
|
+
/** Granted scope (space-joined) — drives the `id_token` profile claims. */
|
|
134
|
+
scope?: string;
|
|
135
|
+
/** OIDC `nonce` from the authorize request — echoed into the `id_token` (Tier 2). */
|
|
136
|
+
nonce?: string;
|
|
137
|
+
/** Mint an `id_token` at `/token` (Tier 2). */
|
|
138
|
+
idToken?: boolean;
|
|
139
|
+
/** Mint an access token at `/token`. Omitted ⇒ minted (Tier-1 loopback). */
|
|
140
|
+
accessToken?: boolean;
|
|
141
|
+
/** The `id_token` `aud` (the registered `client_id`). */
|
|
142
|
+
audience?: string;
|
|
143
|
+
/** What `/token` mints when this code is redeemed. */
|
|
144
|
+
tokenPolicy: TokenPolicy;
|
|
145
|
+
expiresAt: number;
|
|
146
|
+
}
|
|
147
|
+
/** Input to {@link AuthCodeStore.mint} — `code`/`expiresAt` are store-assigned. */
|
|
148
|
+
interface NewAuthCode {
|
|
149
|
+
userId: string;
|
|
150
|
+
codeChallenge: string;
|
|
151
|
+
redirectUri: string;
|
|
152
|
+
clientId?: string;
|
|
153
|
+
scope?: string;
|
|
154
|
+
nonce?: string;
|
|
155
|
+
idToken?: boolean;
|
|
156
|
+
accessToken?: boolean;
|
|
157
|
+
audience?: string;
|
|
158
|
+
tokenPolicy: TokenPolicy;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Storage seam for issued authorization codes (AUTH-SERVER.md §4.3). Very
|
|
162
|
+
* short-lived (≈ 30–60 s) and **single-use**: {@link consume} returns the row
|
|
163
|
+
* AND invalidates it in one atomic step, so a concurrent double-redeem (or a
|
|
164
|
+
* back-button replay) yields the code to exactly one caller. An in-memory impl
|
|
165
|
+
* ships (atomic for free in single-threaded JS); a durable impl must implement
|
|
166
|
+
* `consume` as an atomic check-and-delete (e.g. `withCas` / `@db.column.version`,
|
|
167
|
+
* or a Redis `GETDEL`).
|
|
168
|
+
*/
|
|
169
|
+
declare abstract class AuthCodeStore {
|
|
170
|
+
/** Mint + store a single-use code; returns the opaque code string. */
|
|
171
|
+
abstract mint(rec: NewAuthCode): Promise<{
|
|
172
|
+
code: string;
|
|
173
|
+
}>;
|
|
174
|
+
/** Atomically claim + return the code's row, or `null` on miss / reuse / expiry. */
|
|
175
|
+
abstract consume(code: string): Promise<AuthCode | null>;
|
|
176
|
+
}
|
|
177
|
+
interface AuthCodeStoreMemoryOptions {
|
|
178
|
+
/** Injectable clock for deterministic expiry. Defaults to {@link defaultClock}. */
|
|
179
|
+
clock?: Clock;
|
|
180
|
+
/** Code lifetime. Default 60 s. */
|
|
181
|
+
ttlMs?: number;
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* In-memory {@link AuthCodeStore} — the reference impl. `consume` is atomic
|
|
185
|
+
* because the `get` + `delete` run with no intervening `await`, so a second
|
|
186
|
+
* concurrent `consume` of the same code always misses.
|
|
187
|
+
*/
|
|
188
|
+
declare class AuthCodeStoreMemory extends AuthCodeStore {
|
|
189
|
+
private store;
|
|
190
|
+
private clock;
|
|
191
|
+
private ttlMs;
|
|
192
|
+
constructor(opts?: AuthCodeStoreMemoryOptions);
|
|
193
|
+
mint(rec: NewAuthCode): Promise<{
|
|
194
|
+
code: string;
|
|
195
|
+
}>;
|
|
196
|
+
consume(code: string): Promise<AuthCode | null>;
|
|
197
|
+
}
|
|
198
|
+
//#endregion
|
|
199
|
+
export { NewAuthCode as a, PendingAuthorizationStore as c, TokenPolicy as d, AuthCodeStoreMemoryOptions as i, PendingAuthorizationStoreMemory as l, AuthCodeStore as n, NewPendingAuthorization as o, AuthCodeStoreMemory as r, PendingAuthorization as s, AuthCode as t, PendingAuthorizationStoreMemoryOptions as u };
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { t as defaultClock } from "./clock-Bdsep_1j.mjs";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
//#region src/authz/pending-authorization-store.ts
|
|
4
|
+
/**
|
|
5
|
+
* Storage seam for in-flight authorizations (AUTH-SERVER.md §4.3). Short-lived
|
|
6
|
+
* (≈ the login-session ceiling): created at `/authorize`, read+deleted at the
|
|
7
|
+
* wf terminal. An in-memory impl ships for single-process apps + tests; a
|
|
8
|
+
* multi-pod deployment provides a durable (e.g. Redis) impl under the same
|
|
9
|
+
* `PENDING_AUTHORIZATION_STORE_TOKEN` (from `@aooth/auth-moost`).
|
|
10
|
+
*/
|
|
11
|
+
var PendingAuthorizationStore = class {};
|
|
12
|
+
/** Default pending-authorization lifetime (15 min). Shared by the memory + atscript-db stores. */
|
|
13
|
+
const DEFAULT_PENDING_TTL_MS = 15 * 6e4;
|
|
14
|
+
/**
|
|
15
|
+
* In-memory {@link PendingAuthorizationStore} — the reference impl for a
|
|
16
|
+
* single-process app + tests. `structuredClone` on read/write isolates callers.
|
|
17
|
+
*/
|
|
18
|
+
var PendingAuthorizationStoreMemory = class extends PendingAuthorizationStore {
|
|
19
|
+
store = /* @__PURE__ */ new Map();
|
|
20
|
+
clock;
|
|
21
|
+
ttlMs;
|
|
22
|
+
constructor(opts) {
|
|
23
|
+
super();
|
|
24
|
+
this.clock = opts?.clock ?? defaultClock;
|
|
25
|
+
this.ttlMs = opts?.ttlMs ?? 9e5;
|
|
26
|
+
}
|
|
27
|
+
async create(rec) {
|
|
28
|
+
const now = this.clock.now();
|
|
29
|
+
const row = {
|
|
30
|
+
handle: randomUUID(),
|
|
31
|
+
redirectUri: rec.redirectUri,
|
|
32
|
+
codeChallenge: rec.codeChallenge,
|
|
33
|
+
tokenPolicy: structuredClone(rec.tokenPolicy),
|
|
34
|
+
createdAt: now,
|
|
35
|
+
expiresAt: now + this.ttlMs,
|
|
36
|
+
...rec.clientId !== void 0 && { clientId: rec.clientId },
|
|
37
|
+
...rec.clientState !== void 0 && { clientState: rec.clientState },
|
|
38
|
+
...rec.scope !== void 0 && { scope: rec.scope },
|
|
39
|
+
...rec.nonce !== void 0 && { nonce: rec.nonce },
|
|
40
|
+
...rec.idToken !== void 0 && { idToken: rec.idToken },
|
|
41
|
+
...rec.accessToken !== void 0 && { accessToken: rec.accessToken },
|
|
42
|
+
...rec.audience !== void 0 && { audience: rec.audience }
|
|
43
|
+
};
|
|
44
|
+
this.store.set(row.handle, structuredClone(row));
|
|
45
|
+
return { handle: row.handle };
|
|
46
|
+
}
|
|
47
|
+
async get(handle) {
|
|
48
|
+
const row = this.store.get(handle);
|
|
49
|
+
if (!row) return null;
|
|
50
|
+
if (row.expiresAt <= this.clock.now()) {
|
|
51
|
+
this.store.delete(handle);
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
return structuredClone(row);
|
|
55
|
+
}
|
|
56
|
+
async delete(handle) {
|
|
57
|
+
return this.store.delete(handle);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
//#endregion
|
|
61
|
+
//#region src/authz/auth-code-store.ts
|
|
62
|
+
/**
|
|
63
|
+
* Storage seam for issued authorization codes (AUTH-SERVER.md §4.3). Very
|
|
64
|
+
* short-lived (≈ 30–60 s) and **single-use**: {@link consume} returns the row
|
|
65
|
+
* AND invalidates it in one atomic step, so a concurrent double-redeem (or a
|
|
66
|
+
* back-button replay) yields the code to exactly one caller. An in-memory impl
|
|
67
|
+
* ships (atomic for free in single-threaded JS); a durable impl must implement
|
|
68
|
+
* `consume` as an atomic check-and-delete (e.g. `withCas` / `@db.column.version`,
|
|
69
|
+
* or a Redis `GETDEL`).
|
|
70
|
+
*/
|
|
71
|
+
var AuthCodeStore = class {};
|
|
72
|
+
/** Default auth-code lifetime (60 s). Shared by the memory + atscript-db stores. */
|
|
73
|
+
const DEFAULT_CODE_TTL_MS = 6e4;
|
|
74
|
+
/**
|
|
75
|
+
* In-memory {@link AuthCodeStore} — the reference impl. `consume` is atomic
|
|
76
|
+
* because the `get` + `delete` run with no intervening `await`, so a second
|
|
77
|
+
* concurrent `consume` of the same code always misses.
|
|
78
|
+
*/
|
|
79
|
+
var AuthCodeStoreMemory = class extends AuthCodeStore {
|
|
80
|
+
store = /* @__PURE__ */ new Map();
|
|
81
|
+
clock;
|
|
82
|
+
ttlMs;
|
|
83
|
+
constructor(opts) {
|
|
84
|
+
super();
|
|
85
|
+
this.clock = opts?.clock ?? defaultClock;
|
|
86
|
+
this.ttlMs = opts?.ttlMs ?? 6e4;
|
|
87
|
+
}
|
|
88
|
+
async mint(rec) {
|
|
89
|
+
const code = randomUUID();
|
|
90
|
+
const row = {
|
|
91
|
+
code,
|
|
92
|
+
userId: rec.userId,
|
|
93
|
+
codeChallenge: rec.codeChallenge,
|
|
94
|
+
redirectUri: rec.redirectUri,
|
|
95
|
+
tokenPolicy: structuredClone(rec.tokenPolicy),
|
|
96
|
+
expiresAt: this.clock.now() + this.ttlMs,
|
|
97
|
+
...rec.clientId !== void 0 && { clientId: rec.clientId },
|
|
98
|
+
...rec.scope !== void 0 && { scope: rec.scope },
|
|
99
|
+
...rec.nonce !== void 0 && { nonce: rec.nonce },
|
|
100
|
+
...rec.idToken !== void 0 && { idToken: rec.idToken },
|
|
101
|
+
...rec.accessToken !== void 0 && { accessToken: rec.accessToken },
|
|
102
|
+
...rec.audience !== void 0 && { audience: rec.audience }
|
|
103
|
+
};
|
|
104
|
+
this.store.set(code, structuredClone(row));
|
|
105
|
+
return { code };
|
|
106
|
+
}
|
|
107
|
+
async consume(code) {
|
|
108
|
+
const row = this.store.get(code);
|
|
109
|
+
if (!row) return null;
|
|
110
|
+
this.store.delete(code);
|
|
111
|
+
if (row.expiresAt <= this.clock.now()) return null;
|
|
112
|
+
return structuredClone(row);
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
//#endregion
|
|
116
|
+
export { PendingAuthorizationStore as a, DEFAULT_PENDING_TTL_MS as i, AuthCodeStoreMemory as n, PendingAuthorizationStoreMemory as o, DEFAULT_CODE_TTL_MS as r, AuthCodeStore as t };
|