@crvouga/mockingbird-service-oauth 1.3.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/README.md +22 -2
- package/dist/{chunk-MKBPAVYU.js → chunk-D4IAZYMT.js} +2 -2
- package/dist/{chunk-WHI7Y2UA.js → chunk-SYFF4UFD.js} +219 -37
- package/dist/{chunk-WHI7Y2UA.js.map → chunk-SYFF4UFD.js.map} +3 -3
- package/dist/cli.js +2 -2
- package/dist/index.d.ts +28 -4
- package/dist/index.js +1 -1
- package/dist/server.d.ts +27 -3
- package/dist/server.js +2 -2
- package/package.json +1 -1
- /package/dist/{chunk-MKBPAVYU.js.map → chunk-D4IAZYMT.js.map} +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog — @crvouga/mockingbird-service-oauth
|
|
2
2
|
|
|
3
|
+
## 1.4.0 (2026-09-29)
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
|
|
7
|
+
- opt-in seeded credentials so a seeded run replays byte-for-byte ([7a41166](https://github.com/crvouga/mockingbird/commit/7a41166326791b4fc06627fb544d150d3eaab99c))
|
|
8
|
+
|
|
9
|
+
### Fixes and improvements
|
|
10
|
+
|
|
11
|
+
- make the seeded-credentials example self-contained ([64eef9c](https://github.com/crvouga/mockingbird/commit/64eef9ccbaa555f3bc2420961b7f8fc2a92f5058))
|
|
12
|
+
|
|
3
13
|
## 1.3.0 (2026-09-29)
|
|
4
14
|
|
|
5
15
|
### Features
|
package/README.md
CHANGED
|
@@ -150,7 +150,7 @@ tapping `oauth-mock-form-post-continue`; with scripts, the page submits the form
|
|
|
150
150
|
|
|
151
151
|
### Reproducible provider edge cases
|
|
152
152
|
|
|
153
|
-
Behavioral randomness is **off by default**. Configure exact scenarios or probabilities; these are test frequencies you choose, not estimates of vendor incidence. OAuth credentials, authorization codes and signing keys
|
|
153
|
+
Behavioral randomness is **off by default**. Configure exact scenarios or probabilities; these are test frequencies you choose, not estimates of vendor incidence. OAuth credentials, authorization codes and signing keys use cryptographic randomness unless you opt in to [seeded credentials](#seeded-credentials).
|
|
154
154
|
|
|
155
155
|
```ts
|
|
156
156
|
import { createRuntime } from "@crvouga/mockingbird-service-oauth"
|
|
@@ -224,6 +224,26 @@ Register an Apple client with `apple.notificationUrl` (an HTTP(S) URL; requires
|
|
|
224
224
|
|
|
225
225
|
Delivery is awaited by the call that caused it, through `webhooks.fetch` (`createRuntime({ webhooks: { fetch } })`, default global `fetch`), so an in-process suite can route it through its own network model. The admin routes and methods return one `{ clientId, type, delivered, status?, error? }` per notification; a non-2xx response or a thrown error is reported there and never fails the call, and Apple's retries are not modelled. Without an `issuer` option, direct method calls need an `issuer` argument (admin routes derive it from the request). Unknown accounts return 404 from the admin routes and reject from the methods.
|
|
226
226
|
|
|
227
|
+
### Seeded credentials
|
|
228
|
+
|
|
229
|
+
For deterministic simulation tests, opt in so every opaque value the provider mints comes from the seed instead of `crypto`, and the same seed with the same requests replays byte-for-byte. It is **off by default**: without it, codes, tokens, transaction ids, nonces, `kid` and signing keys stay cryptographically random.
|
|
230
|
+
|
|
231
|
+
```ts
|
|
232
|
+
import { createRuntime } from "@crvouga/mockingbird-service-oauth"
|
|
233
|
+
|
|
234
|
+
const runtime = createRuntime({
|
|
235
|
+
seed: "s1",
|
|
236
|
+
deterministicCredentials: true,
|
|
237
|
+
clients: [{ id: "app", name: "App", redirectUris: ["https://app.test/callback"], secret: "fixture-secret" }],
|
|
238
|
+
accounts: [{ id: "ada", name: "Ada Lovelace", email: "ada@example.test" }],
|
|
239
|
+
})
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
- Seeded: authorization codes, access and refresh tokens, `transaction` ids, session ids, grant families, Apple notification `jti`s, CSP nonces (on every page and the `form_post` response), and the RS256 signing key with its `kid`, including the never-published key behind `tokens.idTokenSigningKey: "unpublished"`. Values are guessable; use only in tests. Pin the clock (`clock`) as well when you compare ID tokens, whose `iat` / `exp` come from it.
|
|
243
|
+
- Signing keys are a pure function of the seed and the rotation index, so JWKS, the ID-token header and (RS256 being deterministic) the signature replay too. Deriving a 2048-bit RSA key costs roughly half a second to a couple of seconds once per seed per process; it is cached afterwards.
|
|
244
|
+
- The draw counter is part of the namespace state: it is snapshotted with it, and `reset` restarts both the counter and the key sequence, so a reset replays the run. The seed is not mixed with the namespace, so two namespaces or mounts with one seed mint the same values.
|
|
245
|
+
- `createRuntime({ randomBytes: (length) => bytes })` replaces `crypto.getRandomValues` for every minted value instead (it wins over the seeded source), and `signingKeys: [{ privateJwk, publicJwk, kid }, ...]` supplies RS256 keys used in order: the first is current and each `POST /__admin/keys/rotate` takes the next. When the list runs out, rotation falls back to the seeded or random source. The `nonce` option still takes precedence for the `form_post` response.
|
|
246
|
+
|
|
227
247
|
### Shared service controls
|
|
228
248
|
|
|
229
249
|
The runtime supplies `/health`, `/__admin/reset`, snapshots, mock clock, request journal, metrics, fault injection and namespace isolation. Use `x-mockingbird-namespace` for in-process tests or `/ns/<name>/…` for complete browser flows. Header-selected namespaces alone cannot persist across ordinary browser navigation. State, grants, sessions and consent live in the shared SQLite abstraction; there are no filesystem or Node imports in the main entry.
|
|
@@ -260,7 +280,7 @@ This is a ready-to-use local/test identity provider, **not a production authenti
|
|
|
260
280
|
|
|
261
281
|
Vendor-hosted Google Identity Services/One Tap, native Apple AuthenticationServices, passkeys, MFA, CAPTCHA, password recovery, email delivery/relay forwarding (only Apple's notification of a forwarding change is sent), app-transfer migration, vendor risk engines, tokeninfo/introspection, logout, GitHub Apps installation/device flows, and Microsoft Graph/tenant administration are not implemented. Other configurable OIDC providers can use the generic profile, but their proprietary scopes and claims are not emulated. Scopes are limited to each profile plus explicitly configured additional scopes. Microsoft uses the configured mock issuer, not real Entra tenant routing. GitHub is the OAuth app login surface, not the full REST API.
|
|
262
282
|
|
|
263
|
-
No implicit flow, dynamic client registration, wildcard redirect matching, persistent signing-key
|
|
283
|
+
No implicit flow, dynamic client registration, wildcard redirect matching, persistent signing-key storage (keys are held in memory; supply them through `signingKeys` or derive them from a seed), or distributed-session coordination is provided. Private-use redirect schemes work only when their complete URI is explicitly registered; executable/local schemes such as `javascript:`, `data:` and `file:` are rejected. Sign-in and consent pages extend CSP `form-action` with the transaction's redirect origin (or custom scheme) so browsers follow the redirect back to the client. Discovery, JWKS, token, userinfo and revoke answer `OPTIONS` preflights and reflect the request `Origin`, so browser (SPA + PKCE) clients can redeem codes directly; the interaction pages send no CORS headers. Snapshot restore is for the same runtime/instance; signing keys are not serialized. The default backing store is in-memory and state disappears when the process exits. A secure browser context and Web Crypto, Fetch and standard Web APIs are required; Node 22+, Bun and modern browsers provide them.
|
|
264
284
|
|
|
265
285
|
References: [Google OpenID Connect](https://developers.google.com/identity/openid-connect/openid-connect), [Apple authorization request](https://developer.apple.com/documentation/signinwithapplerestapi/request-an-authorization-to-the-sign-in-with-apple-server.), and [OpenID Connect Core](https://openid.net/specs/openid-connect-core-1_0.html).
|
|
266
286
|
|
|
@@ -2,7 +2,7 @@ import {
|
|
|
2
2
|
OAUTH_SCENARIOS,
|
|
3
3
|
createMultiRuntime,
|
|
4
4
|
createRuntime
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-SYFF4UFD.js";
|
|
6
6
|
|
|
7
7
|
// src/server.ts
|
|
8
8
|
import { readFile as readFile2 } from "node:fs/promises";
|
|
@@ -377,4 +377,4 @@ export {
|
|
|
377
377
|
createMultiServer,
|
|
378
378
|
serveTarget
|
|
379
379
|
};
|
|
380
|
-
//# sourceMappingURL=chunk-
|
|
380
|
+
//# sourceMappingURL=chunk-D4IAZYMT.js.map
|
|
@@ -1384,7 +1384,7 @@ var restoreNamespace = (sqlite, namespace, snapshot) => {
|
|
|
1384
1384
|
};
|
|
1385
1385
|
|
|
1386
1386
|
// ../core/dist/version.js
|
|
1387
|
-
var PACKAGE_VERSION = true ? "1.
|
|
1387
|
+
var PACKAGE_VERSION = true ? "1.4.0" : UNRELEASED_VERSION;
|
|
1388
1388
|
|
|
1389
1389
|
// ../core/dist/webhooks.js
|
|
1390
1390
|
var json2 = (status, body) => new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" } });
|
|
@@ -2246,18 +2246,22 @@ var encoder = new TextEncoder();
|
|
|
2246
2246
|
var base64url = (bytes) => btoa(String.fromCharCode(...bytes)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
2247
2247
|
var random = () => base64url(crypto.getRandomValues(new Uint8Array(32)));
|
|
2248
2248
|
var hash = async (value) => base64url(new Uint8Array(await crypto.subtle.digest("SHA-256", encoder.encode(value))));
|
|
2249
|
+
var RSA = { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" };
|
|
2249
2250
|
var Signer = class {
|
|
2250
|
-
pair
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
[
|
|
2259
|
-
|
|
2260
|
-
|
|
2251
|
+
pair;
|
|
2252
|
+
kid;
|
|
2253
|
+
/** Generates a fresh key pair unless `key` supplies one; `kid` names a generated pair. */
|
|
2254
|
+
constructor(key, kid = random()) {
|
|
2255
|
+
this.kid = key?.kid ?? kid;
|
|
2256
|
+
this.pair = key ? Promise.all([
|
|
2257
|
+
crypto.subtle.importKey("jwk", key.privateJwk, RSA, true, ["sign"]),
|
|
2258
|
+
crypto.subtle.importKey("jwk", key.publicJwk, RSA, true, ["verify"])
|
|
2259
|
+
]).then(([privateKey, publicKey]) => ({ privateKey, publicKey })) : crypto.subtle.generateKey(
|
|
2260
|
+
{ ...RSA, modulusLength: 2048, publicExponent: new Uint8Array([1, 0, 1]) },
|
|
2261
|
+
true,
|
|
2262
|
+
["sign", "verify"]
|
|
2263
|
+
);
|
|
2264
|
+
}
|
|
2261
2265
|
async jwks() {
|
|
2262
2266
|
const pair = await this.pair;
|
|
2263
2267
|
return {
|
|
@@ -2317,6 +2321,152 @@ async function verifyAppleSecret(token, clientId, apple, now) {
|
|
|
2317
2321
|
}
|
|
2318
2322
|
}
|
|
2319
2323
|
|
|
2324
|
+
// src/credentials.ts
|
|
2325
|
+
var seededBytes = (label, length) => {
|
|
2326
|
+
let a = seedFrom(`${label}:0`);
|
|
2327
|
+
let b = seedFrom(`${label}:1`);
|
|
2328
|
+
let c = seedFrom(`${label}:2`);
|
|
2329
|
+
let d = seedFrom(`${label}:3`);
|
|
2330
|
+
const next = () => {
|
|
2331
|
+
const t = (a + b | 0) + d | 0;
|
|
2332
|
+
d = d + 1 | 0;
|
|
2333
|
+
a = b ^ b >>> 9;
|
|
2334
|
+
b = c + (c << 3) | 0;
|
|
2335
|
+
c = (c << 21 | c >>> 11) + t | 0;
|
|
2336
|
+
return t >>> 0;
|
|
2337
|
+
};
|
|
2338
|
+
for (let i = 0; i < 12; i++) next();
|
|
2339
|
+
const out = new Uint8Array(length);
|
|
2340
|
+
for (let i = 0; i < length; i += 4) {
|
|
2341
|
+
const word = next();
|
|
2342
|
+
for (let j = 0; j < 4 && i + j < length; j++) out[i + j] = word >>> 8 * j & 255;
|
|
2343
|
+
}
|
|
2344
|
+
return out;
|
|
2345
|
+
};
|
|
2346
|
+
var toBigInt = (bytes) => BigInt(`0x${[...bytes].map((b) => b.toString(16).padStart(2, "0")).join("")}`);
|
|
2347
|
+
var fromBigInt = (value) => {
|
|
2348
|
+
const hex = value.toString(16);
|
|
2349
|
+
const padded = hex.length % 2 ? `0${hex}` : hex;
|
|
2350
|
+
return Uint8Array.from(padded.match(/../g) ?? [], (h) => Number.parseInt(h, 16));
|
|
2351
|
+
};
|
|
2352
|
+
var modPow = (base, exponent, modulus) => {
|
|
2353
|
+
let result = 1n;
|
|
2354
|
+
let b = base % modulus;
|
|
2355
|
+
for (let e = exponent; e > 0n; e >>= 1n) {
|
|
2356
|
+
if (e & 1n) result = result * b % modulus;
|
|
2357
|
+
b = b * b % modulus;
|
|
2358
|
+
}
|
|
2359
|
+
return result;
|
|
2360
|
+
};
|
|
2361
|
+
var modInverse = (value, modulus) => {
|
|
2362
|
+
let [r0, r1, t0, t1] = [modulus, value % modulus, 0n, 1n];
|
|
2363
|
+
while (r1) {
|
|
2364
|
+
const q = r0 / r1;
|
|
2365
|
+
[r0, r1] = [r1, r0 - q * r1];
|
|
2366
|
+
[t0, t1] = [t1, t0 - q * t1];
|
|
2367
|
+
}
|
|
2368
|
+
return (t0 % modulus + modulus) % modulus;
|
|
2369
|
+
};
|
|
2370
|
+
var SMALL_PRIMES = Array.from({ length: 2e3 }, (_, i) => i + 2).filter((n, _, all) => all.every((m) => m * m > n || n % m)).map(BigInt);
|
|
2371
|
+
var probablePrime = (n) => {
|
|
2372
|
+
if (SMALL_PRIMES.some((p) => n % p === 0n)) return false;
|
|
2373
|
+
let d = n - 1n;
|
|
2374
|
+
let s = 0;
|
|
2375
|
+
while (!(d & 1n)) {
|
|
2376
|
+
d >>= 1n;
|
|
2377
|
+
s++;
|
|
2378
|
+
}
|
|
2379
|
+
return SMALL_PRIMES.slice(0, 16).every((a) => {
|
|
2380
|
+
let x = modPow(a, d, n);
|
|
2381
|
+
if (x === 1n || x === n - 1n) return true;
|
|
2382
|
+
for (let i = 1; i < s; i++) {
|
|
2383
|
+
x = x * x % n;
|
|
2384
|
+
if (x === n - 1n) return true;
|
|
2385
|
+
}
|
|
2386
|
+
return false;
|
|
2387
|
+
});
|
|
2388
|
+
};
|
|
2389
|
+
var E = 65537n;
|
|
2390
|
+
var seededPrime = (label) => {
|
|
2391
|
+
for (let attempt = 0; ; attempt++) {
|
|
2392
|
+
const candidate = toBigInt(seededBytes(`${label}:${attempt}`, 128)) | 3n << 1022n | 1n;
|
|
2393
|
+
if ((candidate - 1n) % E !== 0n && probablePrime(candidate)) return candidate;
|
|
2394
|
+
}
|
|
2395
|
+
};
|
|
2396
|
+
var keyCache = /* @__PURE__ */ new Map();
|
|
2397
|
+
var seededRsa = (label) => {
|
|
2398
|
+
const cached = keyCache.get(label);
|
|
2399
|
+
if (cached) return cached;
|
|
2400
|
+
let p = seededPrime(`${label}:p`);
|
|
2401
|
+
let q = seededPrime(`${label}:q`);
|
|
2402
|
+
for (let i = 0; q === p; i++) q = seededPrime(`${label}:q${i}`);
|
|
2403
|
+
if (p < q) [p, q] = [q, p];
|
|
2404
|
+
const d = modInverse(E, (p - 1n) * (q - 1n));
|
|
2405
|
+
const enc = (v) => base64url(fromBigInt(v));
|
|
2406
|
+
const publicJwk = { kty: "RSA", n: enc(p * q), e: enc(E) };
|
|
2407
|
+
const privateJwk = {
|
|
2408
|
+
...publicJwk,
|
|
2409
|
+
d: enc(d),
|
|
2410
|
+
p: enc(p),
|
|
2411
|
+
q: enc(q),
|
|
2412
|
+
dp: enc(d % (p - 1n)),
|
|
2413
|
+
dq: enc(d % (q - 1n)),
|
|
2414
|
+
qi: enc(modInverse(q, p))
|
|
2415
|
+
};
|
|
2416
|
+
const pair = { privateJwk, publicJwk };
|
|
2417
|
+
keyCache.set(label, pair);
|
|
2418
|
+
return pair;
|
|
2419
|
+
};
|
|
2420
|
+
var Credentials = class {
|
|
2421
|
+
constructor(sqlite, namespace, seed, options) {
|
|
2422
|
+
this.options = options;
|
|
2423
|
+
this.draws = new Collection(sqlite, namespace, "oauth_credential_draws");
|
|
2424
|
+
this.seed = String(seed ?? 0);
|
|
2425
|
+
}
|
|
2426
|
+
options;
|
|
2427
|
+
draws;
|
|
2428
|
+
seed;
|
|
2429
|
+
keyIndex = 0;
|
|
2430
|
+
bytes(length) {
|
|
2431
|
+
if (this.options.randomBytes) return this.options.randomBytes(length);
|
|
2432
|
+
if (this.options.deterministicCredentials)
|
|
2433
|
+
return seededBytes(`${this.seed}:${this.draws.nextSequence()}`, length);
|
|
2434
|
+
return crypto.getRandomValues(new Uint8Array(length));
|
|
2435
|
+
}
|
|
2436
|
+
/** Codes, access/refresh tokens, transaction ids, session ids and grant families. */
|
|
2437
|
+
token() {
|
|
2438
|
+
return base64url(this.bytes(32));
|
|
2439
|
+
}
|
|
2440
|
+
/** CSP nonce for interaction pages; a UUID unless credentials are controlled. */
|
|
2441
|
+
nonce() {
|
|
2442
|
+
return this.options.randomBytes || this.options.deterministicCredentials ? base64url(this.bytes(16)) : crypto.randomUUID();
|
|
2443
|
+
}
|
|
2444
|
+
/** The next signing key: injected, else derived from the seed, else freshly generated. */
|
|
2445
|
+
signer() {
|
|
2446
|
+
const index = this.keyIndex++;
|
|
2447
|
+
const injected = this.options.signingKeys?.[index];
|
|
2448
|
+
if (injected) return new Signer(injected);
|
|
2449
|
+
if (!this.options.deterministicCredentials || this.options.randomBytes)
|
|
2450
|
+
return new Signer(void 0, this.token());
|
|
2451
|
+
const kid = this.token();
|
|
2452
|
+
return new Signer({ ...seededRsa(`${this.seed}:key:${index}`), kid });
|
|
2453
|
+
}
|
|
2454
|
+
/**
|
|
2455
|
+
* The key that signs ID tokens but is never published. Never taken from `signingKeys` (those are
|
|
2456
|
+
* published); under a seed it is derived without touching the draw counter.
|
|
2457
|
+
*/
|
|
2458
|
+
unpublishedSigner() {
|
|
2459
|
+
if (!this.options.deterministicCredentials || this.options.randomBytes)
|
|
2460
|
+
return new Signer(void 0, this.token());
|
|
2461
|
+
const label = `${this.seed}:unpublished`;
|
|
2462
|
+
return new Signer({ ...seededRsa(label), kid: base64url(seededBytes(`${label}:kid`, 32)) });
|
|
2463
|
+
}
|
|
2464
|
+
/** Start the signing-key sequence over; the draw counter is cleared with the namespace. */
|
|
2465
|
+
restart() {
|
|
2466
|
+
this.keyIndex = 0;
|
|
2467
|
+
}
|
|
2468
|
+
};
|
|
2469
|
+
|
|
2320
2470
|
// src/ui.ts
|
|
2321
2471
|
var escapeHtml = (value) => value.replace(
|
|
2322
2472
|
/[&<>"']/g,
|
|
@@ -2346,7 +2496,7 @@ function page(title, body, status = 200, action = "'self'", nonce = crypto.rando
|
|
|
2346
2496
|
}
|
|
2347
2497
|
);
|
|
2348
2498
|
}
|
|
2349
|
-
function loginPage(transaction, client, accounts, base, action, signup = false, error = "") {
|
|
2499
|
+
function loginPage(transaction, client, accounts, base, action, signup = false, error = "", nonce) {
|
|
2350
2500
|
const hidden = `<input type="hidden" name="transaction" value="${escapeHtml(transaction)}">`;
|
|
2351
2501
|
const endpoint = escapeHtml(`${base}/interaction`);
|
|
2352
2502
|
const intro = `<span class="eyebrow">Sign in</span><h1 id="title">${signup ? "Create your account" : "Choose an account"}</h1><p>${signup ? "Create an account to continue to" : "Choose an account to continue to"}<br><span class="app">${escapeHtml(client)}</span></p>${error ? `<div class="error" role="alert" data-testid="oauth-mock-${signup ? "signup" : "chooser"}-error">${escapeHtml(error)}</div>` : ""}`;
|
|
@@ -2356,11 +2506,11 @@ function loginPage(transaction, client, accounts, base, action, signup = false,
|
|
|
2356
2506
|
`${intro}${body}<div class="divider"></div><form method="post" action="${endpoint}">${hidden}<button class="secondary" data-testid="oauth-mock-deny" name="action" value="deny">Cancel sign-in</button></form>`,
|
|
2357
2507
|
error ? 400 : 200,
|
|
2358
2508
|
action,
|
|
2359
|
-
|
|
2509
|
+
nonce,
|
|
2360
2510
|
signup ? "oauth-mock-signup" : "oauth-mock-chooser"
|
|
2361
2511
|
);
|
|
2362
2512
|
}
|
|
2363
|
-
function consentPage(transaction, client, account, scopes2, base, action, privacy) {
|
|
2513
|
+
function consentPage(transaction, client, account, scopes2, base, action, privacy, nonce) {
|
|
2364
2514
|
const labels = {
|
|
2365
2515
|
openid: "Confirm your identity",
|
|
2366
2516
|
email: "View your email address",
|
|
@@ -2378,7 +2528,7 @@ function consentPage(transaction, client, account, scopes2, base, action, privac
|
|
|
2378
2528
|
)}</ul><p>You can cancel now without sharing anything.</p><form method="post" action="${escapeHtml(base)}/interaction"><input type="hidden" name="transaction" value="${escapeHtml(transaction)}">${privacyFields}<button class="primary" data-testid="oauth-mock-allow" name="action" value="allow">Allow & continue</button><button class="secondary" data-testid="oauth-mock-deny" name="action" value="deny">Cancel</button></form>`,
|
|
2379
2529
|
200,
|
|
2380
2530
|
action,
|
|
2381
|
-
|
|
2531
|
+
nonce,
|
|
2382
2532
|
"oauth-mock-consent"
|
|
2383
2533
|
);
|
|
2384
2534
|
}
|
|
@@ -2688,6 +2838,8 @@ var OAuthAPI = class {
|
|
|
2688
2838
|
options.seed ?? 0,
|
|
2689
2839
|
options.behavior ?? {}
|
|
2690
2840
|
);
|
|
2841
|
+
this.credentials = new Credentials(this.sqlite, this.namespace, options.seed, options);
|
|
2842
|
+
this.signer = this.credentials.signer();
|
|
2691
2843
|
this.seed();
|
|
2692
2844
|
}
|
|
2693
2845
|
options;
|
|
@@ -2698,7 +2850,8 @@ var OAuthAPI = class {
|
|
|
2698
2850
|
tokens;
|
|
2699
2851
|
sessions;
|
|
2700
2852
|
consents;
|
|
2701
|
-
signer
|
|
2853
|
+
signer;
|
|
2854
|
+
credentials;
|
|
2702
2855
|
previousSigners = [];
|
|
2703
2856
|
/** Never published: signs ID tokens for `tokens.idTokenSigningKey: "unpublished"`. */
|
|
2704
2857
|
unpublishedSigner;
|
|
@@ -2755,6 +2908,12 @@ var OAuthAPI = class {
|
|
|
2755
2908
|
async reset() {
|
|
2756
2909
|
clearNamespace(this.sqlite, this.namespace);
|
|
2757
2910
|
this.behavior.configure(this.options.behavior ?? {});
|
|
2911
|
+
if (this.options.deterministicCredentials || this.options.signingKeys) {
|
|
2912
|
+
this.credentials.restart();
|
|
2913
|
+
this.previousSigners = [];
|
|
2914
|
+
this.unpublishedSigner = void 0;
|
|
2915
|
+
this.signer = this.credentials.signer();
|
|
2916
|
+
}
|
|
2758
2917
|
this.seed();
|
|
2759
2918
|
}
|
|
2760
2919
|
issuer(request) {
|
|
@@ -2766,7 +2925,7 @@ var OAuthAPI = class {
|
|
|
2766
2925
|
}
|
|
2767
2926
|
rotateSigningKey(retainPrevious = true) {
|
|
2768
2927
|
this.previousSigners = retainPrevious ? [this.signer, ...this.previousSigners].slice(0, 4) : [];
|
|
2769
|
-
this.signer =
|
|
2928
|
+
this.signer = this.credentials.signer();
|
|
2770
2929
|
return { kid: this.signer.kid };
|
|
2771
2930
|
}
|
|
2772
2931
|
/**
|
|
@@ -2828,7 +2987,7 @@ var OAuthAPI = class {
|
|
|
2828
2987
|
iss: iss.replace(/\/$/, ""),
|
|
2829
2988
|
aud: client.id,
|
|
2830
2989
|
iat: Math.floor(this.now() / 1e3),
|
|
2831
|
-
jti:
|
|
2990
|
+
jti: this.credentials.token(),
|
|
2832
2991
|
events: JSON.stringify({
|
|
2833
2992
|
type,
|
|
2834
2993
|
sub,
|
|
@@ -3037,7 +3196,7 @@ var OAuthAPI = class {
|
|
|
3037
3196
|
const skew = tokens?.idTokenClockSkewSeconds ?? 0;
|
|
3038
3197
|
const iat = Math.floor(this.now() / 1e3) + skew;
|
|
3039
3198
|
const unpublished = tokens?.idTokenSigningKey === "unpublished";
|
|
3040
|
-
if (unpublished) this.unpublishedSigner ??=
|
|
3199
|
+
if (unpublished) this.unpublishedSigner ??= this.credentials.unpublishedSigner();
|
|
3041
3200
|
return (unpublished && this.unpublishedSigner || this.signer).sign({
|
|
3042
3201
|
...this.profile(account, grant),
|
|
3043
3202
|
iss: issuer,
|
|
@@ -3240,7 +3399,10 @@ var OAuthAPI = class {
|
|
|
3240
3399
|
if (request.method === "GET" && path === "/")
|
|
3241
3400
|
return page(
|
|
3242
3401
|
"Identity sandbox",
|
|
3243
|
-
'<span class="eyebrow">Mockingbird Identity</span><h1 id="title">Make sign-in<br>feel real.</h1><p>Your identity sandbox is ready. Start sign-in from your application to choose an account, create a new identity, and review access.</p><div class="account"><span class="avatar" aria-hidden="true">\u2713</span><span class="identity"><strong>Ready when you are</strong><small>OAuth 2.0 \xB7 OpenID Connect</small></span></div>'
|
|
3402
|
+
'<span class="eyebrow">Mockingbird Identity</span><h1 id="title">Make sign-in<br>feel real.</h1><p>Your identity sandbox is ready. Start sign-in from your application to choose an account, create a new identity, and review access.</p><div class="account"><span class="avatar" aria-hidden="true">\u2713</span><span class="identity"><strong>Ready when you are</strong><small>OAuth 2.0 \xB7 OpenID Connect</small></span></div>',
|
|
3403
|
+
200,
|
|
3404
|
+
void 0,
|
|
3405
|
+
this.credentials.nonce()
|
|
3244
3406
|
);
|
|
3245
3407
|
return fail("not_found", "Unknown endpoint", 404);
|
|
3246
3408
|
}
|
|
@@ -3336,7 +3498,7 @@ var OAuthAPI = class {
|
|
|
3336
3498
|
if (!granted) return error("consent_required", "Consent is required");
|
|
3337
3499
|
return this.finish({ ...auth, accountId: current.id, authTime: session.authTime }, issuer);
|
|
3338
3500
|
}
|
|
3339
|
-
const id =
|
|
3501
|
+
const id = this.credentials.token();
|
|
3340
3502
|
if (current && session && !prompt.has("login") && !prompt.has("select_account")) {
|
|
3341
3503
|
auth.accountId = current.id;
|
|
3342
3504
|
auth.authTime = session.authTime;
|
|
@@ -3351,7 +3513,16 @@ var OAuthAPI = class {
|
|
|
3351
3513
|
accounts.sort(
|
|
3352
3514
|
(a, b) => Number(b.email === hint || b.id === hint) - Number(a.email === hint || a.id === hint)
|
|
3353
3515
|
);
|
|
3354
|
-
return loginPage(
|
|
3516
|
+
return loginPage(
|
|
3517
|
+
id,
|
|
3518
|
+
client.name,
|
|
3519
|
+
accounts,
|
|
3520
|
+
issuer,
|
|
3521
|
+
formAction(auth.redirectUri),
|
|
3522
|
+
false,
|
|
3523
|
+
"",
|
|
3524
|
+
this.credentials.nonce()
|
|
3525
|
+
);
|
|
3355
3526
|
}
|
|
3356
3527
|
consent(id, client, account, auth, issuer) {
|
|
3357
3528
|
const existing = this.identities.get(this.identityKey(client, account.id));
|
|
@@ -3367,7 +3538,8 @@ var OAuthAPI = class {
|
|
|
3367
3538
|
this.provider === "apple" && scopes(auth.scope).has("email") && !existing ? {
|
|
3368
3539
|
hideEmail: account.privateEmail ?? (this.behavior.config.apple?.emailMode === "hide" || (this.behavior.config.apple?.emailMode ?? "choose") === "choose" && auth.decisions.hideEmail),
|
|
3369
3540
|
choice: (this.behavior.config.apple?.emailMode ?? "choose") === "choose"
|
|
3370
|
-
} : void 0
|
|
3541
|
+
} : void 0,
|
|
3542
|
+
this.credentials.nonce()
|
|
3371
3543
|
);
|
|
3372
3544
|
}
|
|
3373
3545
|
async interact(request, issuer) {
|
|
@@ -3384,7 +3556,7 @@ var OAuthAPI = class {
|
|
|
3384
3556
|
'<span class="eyebrow">Let\u2019s try again</span><h1 id="title">This sign-in expired</h1><p>Return to your application and start sign-in again.</p>',
|
|
3385
3557
|
400,
|
|
3386
3558
|
void 0,
|
|
3387
|
-
|
|
3559
|
+
this.credentials.nonce(),
|
|
3388
3560
|
"oauth-mock-error"
|
|
3389
3561
|
);
|
|
3390
3562
|
const accounts = () => this.accounts.list({ order: "oldest", where: (a) => !a.disabled }).map((a) => a.value);
|
|
@@ -3395,7 +3567,9 @@ var OAuthAPI = class {
|
|
|
3395
3567
|
accounts(),
|
|
3396
3568
|
issuer,
|
|
3397
3569
|
formAction(auth.redirectUri),
|
|
3398
|
-
p.get("screen") === "signup"
|
|
3570
|
+
p.get("screen") === "signup",
|
|
3571
|
+
"",
|
|
3572
|
+
this.credentials.nonce()
|
|
3399
3573
|
);
|
|
3400
3574
|
const action = p.get("action");
|
|
3401
3575
|
if (action === "deny") {
|
|
@@ -3415,7 +3589,8 @@ var OAuthAPI = class {
|
|
|
3415
3589
|
issuer,
|
|
3416
3590
|
formAction(auth.redirectUri),
|
|
3417
3591
|
true,
|
|
3418
|
-
"Enter a full name and a valid email address."
|
|
3592
|
+
"Enter a full name and a valid email address.",
|
|
3593
|
+
this.credentials.nonce()
|
|
3419
3594
|
);
|
|
3420
3595
|
if (accounts().some((a) => a.email === email) || this.accounts.list().some((a) => a.value.email === email))
|
|
3421
3596
|
return loginPage(
|
|
@@ -3425,9 +3600,15 @@ var OAuthAPI = class {
|
|
|
3425
3600
|
issuer,
|
|
3426
3601
|
formAction(auth.redirectUri),
|
|
3427
3602
|
true,
|
|
3428
|
-
"This email already has an account. Go back to choose it."
|
|
3603
|
+
"This email already has an account. Go back to choose it.",
|
|
3604
|
+
this.credentials.nonce()
|
|
3429
3605
|
);
|
|
3430
|
-
const account = this.seedAccount({
|
|
3606
|
+
const account = this.seedAccount({
|
|
3607
|
+
id: this.credentials.token(),
|
|
3608
|
+
email,
|
|
3609
|
+
name,
|
|
3610
|
+
emailVerified: true
|
|
3611
|
+
});
|
|
3431
3612
|
auth.accountId = account.id;
|
|
3432
3613
|
auth.authTime = this.now();
|
|
3433
3614
|
this.transactions.update(id, auth);
|
|
@@ -3443,7 +3624,8 @@ var OAuthAPI = class {
|
|
|
3443
3624
|
issuer,
|
|
3444
3625
|
formAction(auth.redirectUri),
|
|
3445
3626
|
false,
|
|
3446
|
-
"Choose an available account."
|
|
3627
|
+
"Choose an available account.",
|
|
3628
|
+
this.credentials.nonce()
|
|
3447
3629
|
);
|
|
3448
3630
|
auth.accountId = account.id;
|
|
3449
3631
|
auth.authTime = this.now();
|
|
@@ -3479,7 +3661,7 @@ var OAuthAPI = class {
|
|
|
3479
3661
|
return fail("invalid_request", "Invalid interaction action");
|
|
3480
3662
|
}
|
|
3481
3663
|
startSession(result, accountId, authTime, issuer) {
|
|
3482
|
-
const session =
|
|
3664
|
+
const session = this.credentials.token();
|
|
3483
3665
|
this.sessions.insert(session, { accountId, authTime, expires: this.now() + 864e5 });
|
|
3484
3666
|
result.headers.append(
|
|
3485
3667
|
this.options.cookieHeaders?.response ?? "set-cookie",
|
|
@@ -3516,14 +3698,14 @@ var OAuthAPI = class {
|
|
|
3516
3698
|
});
|
|
3517
3699
|
const googleRefresh = behavior.google?.refreshToken ?? "first-consent";
|
|
3518
3700
|
const issueRefresh = this.provider === "google" ? auth.offline && googleRefresh !== "never" && (googleRefresh === "always" || auth.forceConsent || !previous) : this.provider === "apple" || scopes(scope).has("offline_access");
|
|
3519
|
-
const code =
|
|
3701
|
+
const code = this.credentials.token();
|
|
3520
3702
|
const grant = {
|
|
3521
3703
|
...auth,
|
|
3522
3704
|
scope,
|
|
3523
3705
|
identity,
|
|
3524
3706
|
issueRefresh,
|
|
3525
3707
|
expires: this.now() + (behavior.tokens?.codeTtlSeconds ?? 300) * 1e3,
|
|
3526
|
-
family:
|
|
3708
|
+
family: this.credentials.token()
|
|
3527
3709
|
};
|
|
3528
3710
|
this.codes.insert(code, grant);
|
|
3529
3711
|
this.consents.insert(key, { scope: [...scopes(`${previous?.scope ?? ""} ${scope}`)].join(" ") });
|
|
@@ -3552,7 +3734,7 @@ var OAuthAPI = class {
|
|
|
3552
3734
|
if (auth.state) values.state = auth.state;
|
|
3553
3735
|
const redirect = new URL(auth.redirectUri);
|
|
3554
3736
|
if (auth.responseMode === "form_post") {
|
|
3555
|
-
const nonce = this.options.nonce?.() ??
|
|
3737
|
+
const nonce = this.options.nonce?.() ?? this.credentials.nonce();
|
|
3556
3738
|
const result = page(
|
|
3557
3739
|
"Continue to your app",
|
|
3558
3740
|
`<span class="eyebrow">All set</span><h1 id="title">Back to your app</h1><p>Your sign-in response is ready.</p><form id="callback" method="post" action="${escapeHtml(auth.redirectUri)}">${Object.entries(
|
|
@@ -3696,7 +3878,7 @@ var OAuthAPI = class {
|
|
|
3696
3878
|
if (type === "refresh_token")
|
|
3697
3879
|
this.tokens.update(key, { ...stored, kind: "refresh", lastUsed: this.now() });
|
|
3698
3880
|
const accessTtl = behavior.tokens?.accessTtlSeconds ?? 3600;
|
|
3699
|
-
const access =
|
|
3881
|
+
const access = this.credentials.token();
|
|
3700
3882
|
const result = {
|
|
3701
3883
|
access_token: access,
|
|
3702
3884
|
token_type: "Bearer",
|
|
@@ -3725,7 +3907,7 @@ var OAuthAPI = class {
|
|
|
3725
3907
|
const refreshKey = JSON.stringify([grant.clientId, grant.accountId]);
|
|
3726
3908
|
const googleEligible = this.provider !== "google" || behavior.google?.refreshToken === "always" || grant.forceConsent || !this.refreshIssued.has(refreshKey);
|
|
3727
3909
|
if (type === "authorization_code" && grant.issueRefresh && googleEligible || rotating || microsoftRefresh) {
|
|
3728
|
-
const refresh =
|
|
3910
|
+
const refresh = this.credentials.token();
|
|
3729
3911
|
const basicScopes = [
|
|
3730
3912
|
"openid",
|
|
3731
3913
|
"email",
|
|
@@ -3786,4 +3968,4 @@ export {
|
|
|
3786
3968
|
createMultiRuntime,
|
|
3787
3969
|
OAuthAPI
|
|
3788
3970
|
};
|
|
3789
|
-
//# sourceMappingURL=chunk-
|
|
3971
|
+
//# sourceMappingURL=chunk-SYFF4UFD.js.map
|