@crvouga/mockingbird-service-oauth 1.2.1 → 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 +16 -0
- package/README.md +28 -4
- package/dist/{chunk-2JIADQOV.js → chunk-D4IAZYMT.js} +2 -2
- package/dist/{chunk-XP2AEPEJ.js → chunk-SYFF4UFD.js} +270 -37
- package/dist/{chunk-XP2AEPEJ.js.map → chunk-SYFF4UFD.js.map} +3 -3
- package/dist/cli.js +2 -2
- package/dist/index.d.ts +48 -4
- package/dist/index.js +1 -1
- package/dist/server.d.ts +47 -3
- package/dist/server.js +2 -2
- package/package.json +1 -1
- /package/dist/{chunk-2JIADQOV.js.map → chunk-D4IAZYMT.js.map} +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
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
|
+
|
|
13
|
+
## 1.3.0 (2026-09-29)
|
|
14
|
+
|
|
15
|
+
### Features
|
|
16
|
+
|
|
17
|
+
- admin read of an account's grant with a client ([90b0279](https://github.com/crvouga/mockingbird/commit/90b0279f53cb4959b18c8992ae64bb00b5de4f96))
|
|
18
|
+
|
|
3
19
|
## 1.2.1 (2026-09-29)
|
|
4
20
|
|
|
5
21
|
### Fixes and improvements
|
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"
|
|
@@ -198,6 +198,8 @@ curl -X PUT http://localhost:8810/__admin/behavior -H 'content-type: application
|
|
|
198
198
|
curl http://localhost:8810/__admin/behavior
|
|
199
199
|
curl -X POST http://localhost:8810/__admin/consents/revoke -H 'content-type: application/json' \
|
|
200
200
|
-d '{"clientId":"app","accountId":"ada"}'
|
|
201
|
+
curl 'http://localhost:8810/__admin/grants?clientId=app&accountId=ada'
|
|
202
|
+
curl 'http://localhost:8810/__admin/grants?clientId=app'
|
|
201
203
|
curl -X POST http://localhost:8810/__admin/accounts/delete -H 'content-type: application/json' \
|
|
202
204
|
-d '{"accountId":"ada"}'
|
|
203
205
|
curl -X POST http://localhost:8810/__admin/relay-forwarding -H 'content-type: application/json' \
|
|
@@ -206,6 +208,8 @@ curl -X POST http://localhost:8810/__admin/keys/rotate -H 'content-type: applica
|
|
|
206
208
|
-d '{"retainPrevious":true}'
|
|
207
209
|
```
|
|
208
210
|
|
|
211
|
+
`GET /__admin/grants?clientId=<id>&accountId=<id>` (or `&email=<address>`) reads what that client sees for the account, without running a sign-in: `{ subject, granted, scopes, emailChoice, email, isPrivateEmail, userDisclosed }`. `subject` is the id token `sub` the client gets, or would get on a first authorization (it survives a consent revocation). `granted` says whether a consent exists, so whether the consent card is shown next time. `scopes` are the granted scopes, including Google `include_granted_scopes` merges. `emailChoice` is Apple's `"share"` or `"hide"`, and `null` before the first authorization, after a revocation, and on every other profile. `email` is the address the client receives: the relay for Hide My Email, the real address otherwise. `userDisclosed` is whether Apple's first-use `user` payload was already sent. Without `accountId` or `email` it returns `{ "grants": [...] }`, one row (with `accountId`) per account that currently has a grant with the client. An unknown client or account is `404`. Programmatically, `await runtime.instance().grant(clientId, accountId)` returns the same view, or `null`, and `grants(clientId)` the list. The view is read-only and follows namespaces.
|
|
212
|
+
|
|
209
213
|
These routes use the shared admin-key and namespace controls. `revokeConsent(clientId, accountId)` removes that client's grants and resets first-use disclosure; it does not disable the account. It is async because it can deliver a [server-to-server notification](#sign-in-with-apple-server-to-server-notifications). `rotateSigningKey(true)` retains up to four previous public keys so existing tokens still verify; `false` withdraws them to test stale JWKS caches. Keys themselves are not included in snapshots, so restoring state does not undo a key rotation.
|
|
210
214
|
|
|
211
215
|
### Sign in with Apple server-to-server notifications
|
|
@@ -220,6 +224,26 @@ Register an Apple client with `apple.notificationUrl` (an HTTP(S) URL; requires
|
|
|
220
224
|
|
|
221
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.
|
|
222
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
|
+
|
|
223
247
|
### Shared service controls
|
|
224
248
|
|
|
225
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.
|
|
@@ -230,14 +254,14 @@ The runtime supplies `/health`, `/__admin/reset`, snapshots, mock clock, request
|
|
|
230
254
|
|
|
231
255
|
- `createRuntime(options?)`: shared service runtime; `fetch`, `instance`, `reset`, `snapshot`, `restore`, clock, faults and journals.
|
|
232
256
|
- `createMultiRuntime({ mounts })`: exact-path dispatcher for isolated provider runtimes on one origin, with aggregate health and admin controls.
|
|
233
|
-
- `OAuthAPI`: standalone portable handler with `fetch`, `reset`, `seedAccount`, `registerClient`, `accounts`, `clients`, `provider`, `configureBehavior`, `behavior`, `revokeConsent`, `rotateSigningKey`.
|
|
257
|
+
- `OAuthAPI`: standalone portable handler with `fetch`, `reset`, `seedAccount`, `registerClient`, `accounts`, `clients`, `provider`, `configureBehavior`, `behavior`, `revokeConsent`, `rotateSigningKey`, `grant`, `grants`.
|
|
234
258
|
- `OAUTH_PRESETS`: named transport fault presets.
|
|
235
259
|
- `OAUTH_SCENARIOS`: named provider-behavior scenarios.
|
|
236
260
|
- `document`, `operationIds`, `supportedOperationIds`: generated OpenAPI metadata.
|
|
237
261
|
- `createServer(options?)` from `./server`: Node HTTP adapter, returning `url`, `close` and `runtime`.
|
|
238
262
|
- `createMultiServer({ mounts })` from `./server`: Node HTTP adapter for a multi-provider runtime.
|
|
239
263
|
- `DEFAULT_PORT`, `serveTarget` from `./server`: CLI defaults and multi-service launcher integration.
|
|
240
|
-
- Types: `Account`, `Client`, `Provider`, `OAuthAPIOptions`, `OAuthRuntimeOptions`, `OAuthRuntime`, `OAuthServerOptions`, `OAuthMount`, `OAuthMultiRuntimeOptions`, `OAuthMultiRuntime`, `OAuthMultiServerOptions`, `OAuthBehavior`, `BehaviorInput`, `OAuthScenario`, `EdgeCase`, `BehaviorEvent`.
|
|
264
|
+
- Types: `Account`, `Client`, `GrantView`, `Provider`, `OAuthAPIOptions`, `OAuthRuntimeOptions`, `OAuthRuntime`, `OAuthServerOptions`, `OAuthMount`, `OAuthMultiRuntimeOptions`, `OAuthMultiRuntime`, `OAuthMultiServerOptions`, `OAuthBehavior`, `BehaviorInput`, `OAuthScenario`, `EdgeCase`, `BehaviorEvent`.
|
|
241
265
|
|
|
242
266
|
## Verification
|
|
243
267
|
|
|
@@ -256,7 +280,7 @@ This is a ready-to-use local/test identity provider, **not a production authenti
|
|
|
256
280
|
|
|
257
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.
|
|
258
282
|
|
|
259
|
-
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.
|
|
260
284
|
|
|
261
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).
|
|
262
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
|
}
|
|
@@ -2472,6 +2622,24 @@ function createRuntime2(options = {}) {
|
|
|
2472
2622
|
);
|
|
2473
2623
|
return Response.json({ forwarding: input.forwarding, notifications });
|
|
2474
2624
|
},
|
|
2625
|
+
"GET /grants": async ({ namespace, url }) => {
|
|
2626
|
+
const api = runtime.instance(namespace);
|
|
2627
|
+
const clientId = url.searchParams.get("clientId");
|
|
2628
|
+
const accountId = url.searchParams.get("accountId");
|
|
2629
|
+
const email = url.searchParams.get("email")?.toLowerCase().trim();
|
|
2630
|
+
if (!clientId || accountId !== null && email !== void 0)
|
|
2631
|
+
return Response.json(
|
|
2632
|
+
{ error: "clientId is required, with at most one of accountId or email" },
|
|
2633
|
+
{ status: 400 }
|
|
2634
|
+
);
|
|
2635
|
+
if (accountId === null && email === void 0) {
|
|
2636
|
+
const grants = await api.grants(clientId);
|
|
2637
|
+
return grants ? Response.json({ grants }) : Response.json({ error: "Unknown client" }, { status: 404 });
|
|
2638
|
+
}
|
|
2639
|
+
const id = accountId ?? api.accounts.list({ where: (a) => a.email.toLowerCase() === email })[0]?.value.id ?? "";
|
|
2640
|
+
const grant = await api.grant(clientId, id);
|
|
2641
|
+
return grant ? Response.json(grant) : Response.json({ error: "Unknown client or account" }, { status: 404 });
|
|
2642
|
+
},
|
|
2475
2643
|
"POST /keys/rotate": ({ namespace, body }) => {
|
|
2476
2644
|
const input = body;
|
|
2477
2645
|
if (input?.retainPrevious !== void 0 && typeof input.retainPrevious !== "boolean")
|
|
@@ -2670,6 +2838,8 @@ var OAuthAPI = class {
|
|
|
2670
2838
|
options.seed ?? 0,
|
|
2671
2839
|
options.behavior ?? {}
|
|
2672
2840
|
);
|
|
2841
|
+
this.credentials = new Credentials(this.sqlite, this.namespace, options.seed, options);
|
|
2842
|
+
this.signer = this.credentials.signer();
|
|
2673
2843
|
this.seed();
|
|
2674
2844
|
}
|
|
2675
2845
|
options;
|
|
@@ -2680,7 +2850,8 @@ var OAuthAPI = class {
|
|
|
2680
2850
|
tokens;
|
|
2681
2851
|
sessions;
|
|
2682
2852
|
consents;
|
|
2683
|
-
signer
|
|
2853
|
+
signer;
|
|
2854
|
+
credentials;
|
|
2684
2855
|
previousSigners = [];
|
|
2685
2856
|
/** Never published: signs ID tokens for `tokens.idTokenSigningKey: "unpublished"`. */
|
|
2686
2857
|
unpublishedSigner;
|
|
@@ -2737,6 +2908,12 @@ var OAuthAPI = class {
|
|
|
2737
2908
|
async reset() {
|
|
2738
2909
|
clearNamespace(this.sqlite, this.namespace);
|
|
2739
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
|
+
}
|
|
2740
2917
|
this.seed();
|
|
2741
2918
|
}
|
|
2742
2919
|
issuer(request) {
|
|
@@ -2748,7 +2925,7 @@ var OAuthAPI = class {
|
|
|
2748
2925
|
}
|
|
2749
2926
|
rotateSigningKey(retainPrevious = true) {
|
|
2750
2927
|
this.previousSigners = retainPrevious ? [this.signer, ...this.previousSigners].slice(0, 4) : [];
|
|
2751
|
-
this.signer =
|
|
2928
|
+
this.signer = this.credentials.signer();
|
|
2752
2929
|
return { kid: this.signer.kid };
|
|
2753
2930
|
}
|
|
2754
2931
|
/**
|
|
@@ -2810,7 +2987,7 @@ var OAuthAPI = class {
|
|
|
2810
2987
|
iss: iss.replace(/\/$/, ""),
|
|
2811
2988
|
aud: client.id,
|
|
2812
2989
|
iat: Math.floor(this.now() / 1e3),
|
|
2813
|
-
jti:
|
|
2990
|
+
jti: this.credentials.token(),
|
|
2814
2991
|
events: JSON.stringify({
|
|
2815
2992
|
type,
|
|
2816
2993
|
sub,
|
|
@@ -2857,6 +3034,39 @@ var OAuthAPI = class {
|
|
|
2857
3034
|
}))
|
|
2858
3035
|
this.tokens.delete(row.id);
|
|
2859
3036
|
}
|
|
3037
|
+
/**
|
|
3038
|
+
* What `clientId` sees for `accountId` (a test control mirroring private provider state), or
|
|
3039
|
+
* `null` for an unknown client or account. For a client that never authorized the account,
|
|
3040
|
+
* `subject` is the id token `sub` it would receive and `email` the real address.
|
|
3041
|
+
*/
|
|
3042
|
+
async grant(clientId, accountId) {
|
|
3043
|
+
const client = this.clients.get(clientId);
|
|
3044
|
+
const account = this.accounts.get(accountId);
|
|
3045
|
+
if (!client || !account) return null;
|
|
3046
|
+
const key = this.identityKey(client, account.id);
|
|
3047
|
+
const identity = this.identities.get(key);
|
|
3048
|
+
const consent = this.consents.get(JSON.stringify([clientId, accountId]));
|
|
3049
|
+
return {
|
|
3050
|
+
subject: identity?.sub ?? (this.provider === "apple" || this.provider === "microsoft" ? await hash(key) : account.id),
|
|
3051
|
+
granted: consent !== void 0,
|
|
3052
|
+
scopes: [...scopes(consent?.scope ?? "")],
|
|
3053
|
+
emailChoice: this.provider === "apple" && identity ? identity.privateEmail ? "hide" : "share" : null,
|
|
3054
|
+
email: identity?.email ?? account.email,
|
|
3055
|
+
isPrivateEmail: identity?.privateEmail ?? false,
|
|
3056
|
+
userDisclosed: this.appleDisclosures.has(key)
|
|
3057
|
+
};
|
|
3058
|
+
}
|
|
3059
|
+
/** The grant view of every account that has authorized `clientId`, oldest account first. */
|
|
3060
|
+
async grants(clientId) {
|
|
3061
|
+
if (!this.clients.has(clientId)) return null;
|
|
3062
|
+
const rows = [];
|
|
3063
|
+
for (const { id, value } of this.accounts.list({ order: "oldest" })) {
|
|
3064
|
+
if (!this.consents.has(JSON.stringify([clientId, id]))) continue;
|
|
3065
|
+
const view = await this.grant(clientId, value.id);
|
|
3066
|
+
if (view) rows.push({ accountId: value.id, ...view });
|
|
3067
|
+
}
|
|
3068
|
+
return rows;
|
|
3069
|
+
}
|
|
2860
3070
|
paths() {
|
|
2861
3071
|
const paths = {
|
|
2862
3072
|
google: {
|
|
@@ -2986,7 +3196,7 @@ var OAuthAPI = class {
|
|
|
2986
3196
|
const skew = tokens?.idTokenClockSkewSeconds ?? 0;
|
|
2987
3197
|
const iat = Math.floor(this.now() / 1e3) + skew;
|
|
2988
3198
|
const unpublished = tokens?.idTokenSigningKey === "unpublished";
|
|
2989
|
-
if (unpublished) this.unpublishedSigner ??=
|
|
3199
|
+
if (unpublished) this.unpublishedSigner ??= this.credentials.unpublishedSigner();
|
|
2990
3200
|
return (unpublished && this.unpublishedSigner || this.signer).sign({
|
|
2991
3201
|
...this.profile(account, grant),
|
|
2992
3202
|
iss: issuer,
|
|
@@ -3189,7 +3399,10 @@ var OAuthAPI = class {
|
|
|
3189
3399
|
if (request.method === "GET" && path === "/")
|
|
3190
3400
|
return page(
|
|
3191
3401
|
"Identity sandbox",
|
|
3192
|
-
'<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()
|
|
3193
3406
|
);
|
|
3194
3407
|
return fail("not_found", "Unknown endpoint", 404);
|
|
3195
3408
|
}
|
|
@@ -3285,7 +3498,7 @@ var OAuthAPI = class {
|
|
|
3285
3498
|
if (!granted) return error("consent_required", "Consent is required");
|
|
3286
3499
|
return this.finish({ ...auth, accountId: current.id, authTime: session.authTime }, issuer);
|
|
3287
3500
|
}
|
|
3288
|
-
const id =
|
|
3501
|
+
const id = this.credentials.token();
|
|
3289
3502
|
if (current && session && !prompt.has("login") && !prompt.has("select_account")) {
|
|
3290
3503
|
auth.accountId = current.id;
|
|
3291
3504
|
auth.authTime = session.authTime;
|
|
@@ -3300,7 +3513,16 @@ var OAuthAPI = class {
|
|
|
3300
3513
|
accounts.sort(
|
|
3301
3514
|
(a, b) => Number(b.email === hint || b.id === hint) - Number(a.email === hint || a.id === hint)
|
|
3302
3515
|
);
|
|
3303
|
-
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
|
+
);
|
|
3304
3526
|
}
|
|
3305
3527
|
consent(id, client, account, auth, issuer) {
|
|
3306
3528
|
const existing = this.identities.get(this.identityKey(client, account.id));
|
|
@@ -3316,7 +3538,8 @@ var OAuthAPI = class {
|
|
|
3316
3538
|
this.provider === "apple" && scopes(auth.scope).has("email") && !existing ? {
|
|
3317
3539
|
hideEmail: account.privateEmail ?? (this.behavior.config.apple?.emailMode === "hide" || (this.behavior.config.apple?.emailMode ?? "choose") === "choose" && auth.decisions.hideEmail),
|
|
3318
3540
|
choice: (this.behavior.config.apple?.emailMode ?? "choose") === "choose"
|
|
3319
|
-
} : void 0
|
|
3541
|
+
} : void 0,
|
|
3542
|
+
this.credentials.nonce()
|
|
3320
3543
|
);
|
|
3321
3544
|
}
|
|
3322
3545
|
async interact(request, issuer) {
|
|
@@ -3333,7 +3556,7 @@ var OAuthAPI = class {
|
|
|
3333
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>',
|
|
3334
3557
|
400,
|
|
3335
3558
|
void 0,
|
|
3336
|
-
|
|
3559
|
+
this.credentials.nonce(),
|
|
3337
3560
|
"oauth-mock-error"
|
|
3338
3561
|
);
|
|
3339
3562
|
const accounts = () => this.accounts.list({ order: "oldest", where: (a) => !a.disabled }).map((a) => a.value);
|
|
@@ -3344,7 +3567,9 @@ var OAuthAPI = class {
|
|
|
3344
3567
|
accounts(),
|
|
3345
3568
|
issuer,
|
|
3346
3569
|
formAction(auth.redirectUri),
|
|
3347
|
-
p.get("screen") === "signup"
|
|
3570
|
+
p.get("screen") === "signup",
|
|
3571
|
+
"",
|
|
3572
|
+
this.credentials.nonce()
|
|
3348
3573
|
);
|
|
3349
3574
|
const action = p.get("action");
|
|
3350
3575
|
if (action === "deny") {
|
|
@@ -3364,7 +3589,8 @@ var OAuthAPI = class {
|
|
|
3364
3589
|
issuer,
|
|
3365
3590
|
formAction(auth.redirectUri),
|
|
3366
3591
|
true,
|
|
3367
|
-
"Enter a full name and a valid email address."
|
|
3592
|
+
"Enter a full name and a valid email address.",
|
|
3593
|
+
this.credentials.nonce()
|
|
3368
3594
|
);
|
|
3369
3595
|
if (accounts().some((a) => a.email === email) || this.accounts.list().some((a) => a.value.email === email))
|
|
3370
3596
|
return loginPage(
|
|
@@ -3374,9 +3600,15 @@ var OAuthAPI = class {
|
|
|
3374
3600
|
issuer,
|
|
3375
3601
|
formAction(auth.redirectUri),
|
|
3376
3602
|
true,
|
|
3377
|
-
"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()
|
|
3378
3605
|
);
|
|
3379
|
-
const account = this.seedAccount({
|
|
3606
|
+
const account = this.seedAccount({
|
|
3607
|
+
id: this.credentials.token(),
|
|
3608
|
+
email,
|
|
3609
|
+
name,
|
|
3610
|
+
emailVerified: true
|
|
3611
|
+
});
|
|
3380
3612
|
auth.accountId = account.id;
|
|
3381
3613
|
auth.authTime = this.now();
|
|
3382
3614
|
this.transactions.update(id, auth);
|
|
@@ -3392,7 +3624,8 @@ var OAuthAPI = class {
|
|
|
3392
3624
|
issuer,
|
|
3393
3625
|
formAction(auth.redirectUri),
|
|
3394
3626
|
false,
|
|
3395
|
-
"Choose an available account."
|
|
3627
|
+
"Choose an available account.",
|
|
3628
|
+
this.credentials.nonce()
|
|
3396
3629
|
);
|
|
3397
3630
|
auth.accountId = account.id;
|
|
3398
3631
|
auth.authTime = this.now();
|
|
@@ -3428,7 +3661,7 @@ var OAuthAPI = class {
|
|
|
3428
3661
|
return fail("invalid_request", "Invalid interaction action");
|
|
3429
3662
|
}
|
|
3430
3663
|
startSession(result, accountId, authTime, issuer) {
|
|
3431
|
-
const session =
|
|
3664
|
+
const session = this.credentials.token();
|
|
3432
3665
|
this.sessions.insert(session, { accountId, authTime, expires: this.now() + 864e5 });
|
|
3433
3666
|
result.headers.append(
|
|
3434
3667
|
this.options.cookieHeaders?.response ?? "set-cookie",
|
|
@@ -3465,14 +3698,14 @@ var OAuthAPI = class {
|
|
|
3465
3698
|
});
|
|
3466
3699
|
const googleRefresh = behavior.google?.refreshToken ?? "first-consent";
|
|
3467
3700
|
const issueRefresh = this.provider === "google" ? auth.offline && googleRefresh !== "never" && (googleRefresh === "always" || auth.forceConsent || !previous) : this.provider === "apple" || scopes(scope).has("offline_access");
|
|
3468
|
-
const code =
|
|
3701
|
+
const code = this.credentials.token();
|
|
3469
3702
|
const grant = {
|
|
3470
3703
|
...auth,
|
|
3471
3704
|
scope,
|
|
3472
3705
|
identity,
|
|
3473
3706
|
issueRefresh,
|
|
3474
3707
|
expires: this.now() + (behavior.tokens?.codeTtlSeconds ?? 300) * 1e3,
|
|
3475
|
-
family:
|
|
3708
|
+
family: this.credentials.token()
|
|
3476
3709
|
};
|
|
3477
3710
|
this.codes.insert(code, grant);
|
|
3478
3711
|
this.consents.insert(key, { scope: [...scopes(`${previous?.scope ?? ""} ${scope}`)].join(" ") });
|
|
@@ -3501,7 +3734,7 @@ var OAuthAPI = class {
|
|
|
3501
3734
|
if (auth.state) values.state = auth.state;
|
|
3502
3735
|
const redirect = new URL(auth.redirectUri);
|
|
3503
3736
|
if (auth.responseMode === "form_post") {
|
|
3504
|
-
const nonce = this.options.nonce?.() ??
|
|
3737
|
+
const nonce = this.options.nonce?.() ?? this.credentials.nonce();
|
|
3505
3738
|
const result = page(
|
|
3506
3739
|
"Continue to your app",
|
|
3507
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(
|
|
@@ -3645,7 +3878,7 @@ var OAuthAPI = class {
|
|
|
3645
3878
|
if (type === "refresh_token")
|
|
3646
3879
|
this.tokens.update(key, { ...stored, kind: "refresh", lastUsed: this.now() });
|
|
3647
3880
|
const accessTtl = behavior.tokens?.accessTtlSeconds ?? 3600;
|
|
3648
|
-
const access =
|
|
3881
|
+
const access = this.credentials.token();
|
|
3649
3882
|
const result = {
|
|
3650
3883
|
access_token: access,
|
|
3651
3884
|
token_type: "Bearer",
|
|
@@ -3674,7 +3907,7 @@ var OAuthAPI = class {
|
|
|
3674
3907
|
const refreshKey = JSON.stringify([grant.clientId, grant.accountId]);
|
|
3675
3908
|
const googleEligible = this.provider !== "google" || behavior.google?.refreshToken === "always" || grant.forceConsent || !this.refreshIssued.has(refreshKey);
|
|
3676
3909
|
if (type === "authorization_code" && grant.issueRefresh && googleEligible || rotating || microsoftRefresh) {
|
|
3677
|
-
const refresh =
|
|
3910
|
+
const refresh = this.credentials.token();
|
|
3678
3911
|
const basicScopes = [
|
|
3679
3912
|
"openid",
|
|
3680
3913
|
"email",
|
|
@@ -3735,4 +3968,4 @@ export {
|
|
|
3735
3968
|
createMultiRuntime,
|
|
3736
3969
|
OAuthAPI
|
|
3737
3970
|
};
|
|
3738
|
-
//# sourceMappingURL=chunk-
|
|
3971
|
+
//# sourceMappingURL=chunk-SYFF4UFD.js.map
|