@open-everywhere/adapters 0.6.0 → 0.7.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/agent/directory/okta.ts +41 -2
- package/agent/oe-oauth/config.ts +71 -3
- package/agent/oe-oauth/errors.ts +17 -0
- package/agent/oe-oauth/oauth-client.ts +118 -2
- package/agent/oe-oauth/token-vault.ts +359 -8
- package/dist/agent/directory/okta.d.ts +10 -0
- package/dist/agent/directory/okta.d.ts.map +1 -1
- package/dist/agent/directory/okta.js +34 -2
- package/dist/agent/directory/okta.js.map +1 -1
- package/dist/agent/oe-oauth/config.d.ts +28 -2
- package/dist/agent/oe-oauth/config.d.ts.map +1 -1
- package/dist/agent/oe-oauth/config.js +40 -1
- package/dist/agent/oe-oauth/config.js.map +1 -1
- package/dist/agent/oe-oauth/errors.d.ts +14 -0
- package/dist/agent/oe-oauth/errors.d.ts.map +1 -1
- package/dist/agent/oe-oauth/errors.js +15 -0
- package/dist/agent/oe-oauth/errors.js.map +1 -1
- package/dist/agent/oe-oauth/oauth-client.d.ts +46 -0
- package/dist/agent/oe-oauth/oauth-client.d.ts.map +1 -1
- package/dist/agent/oe-oauth/oauth-client.js +82 -1
- package/dist/agent/oe-oauth/oauth-client.js.map +1 -1
- package/dist/agent/oe-oauth/token-vault.d.ts +15 -0
- package/dist/agent/oe-oauth/token-vault.d.ts.map +1 -1
- package/dist/agent/oe-oauth/token-vault.js +317 -7
- package/dist/agent/oe-oauth/token-vault.js.map +1 -1
- package/package.json +3 -2
|
@@ -2,8 +2,25 @@
|
|
|
2
2
|
// encrypted KV. Refresh is single-flight in-process AND cross-process, because the provider ROTATES tokens.
|
|
3
3
|
|
|
4
4
|
import type { EncryptedKvVault } from "@open-everywhere/core/agent/adapter-api.js";
|
|
5
|
-
import {
|
|
6
|
-
|
|
5
|
+
import {
|
|
6
|
+
agentDisplayName,
|
|
7
|
+
checkXaaIdentityBinding,
|
|
8
|
+
encryptedKvVault,
|
|
9
|
+
recordXaaRefusal,
|
|
10
|
+
getSecretVia,
|
|
11
|
+
OE_OAUTH_CALLBACK_PATH,
|
|
12
|
+
verifyOidcIdToken,
|
|
13
|
+
type XaaBindingVerdict,
|
|
14
|
+
} from "@open-everywhere/core/agent/adapter-api.js";
|
|
15
|
+
import {
|
|
16
|
+
isStaticConfidential,
|
|
17
|
+
isXaa,
|
|
18
|
+
OE_OAUTH_MODE_STATIC,
|
|
19
|
+
OE_OAUTH_MODE_XAA,
|
|
20
|
+
type OeOauthProviderConfig,
|
|
21
|
+
type OeOauthStaticProviderConfig,
|
|
22
|
+
type OeOauthXaaProviderConfig,
|
|
23
|
+
} from "./config.js";
|
|
7
24
|
import {
|
|
8
25
|
discoverEndpoints,
|
|
9
26
|
registerClient,
|
|
@@ -15,13 +32,18 @@ import {
|
|
|
15
32
|
refreshTokenConfidential,
|
|
16
33
|
revokeToken,
|
|
17
34
|
revokeTokenConfidential,
|
|
35
|
+
discoverOidc,
|
|
36
|
+
fetchJwks,
|
|
37
|
+
requestIdJag,
|
|
38
|
+
jwtBearerGrant,
|
|
18
39
|
type ConfidentialClient,
|
|
40
|
+
type OidcEndpoints,
|
|
19
41
|
type OAuthFetch,
|
|
20
42
|
type OAuthTokenResponse,
|
|
21
43
|
} from "./oauth-client.js";
|
|
22
|
-
import { randomUUID } from "node:crypto";
|
|
44
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
23
45
|
import { makePkce, makeState } from "./pkce.js";
|
|
24
|
-
import { OeOauthNeedsConsentError, OeOauthNeedsReconsentError, OeOauthRefreshContendedError, isInvalidGrant } from "./errors.js";
|
|
46
|
+
import { OeOauthAccessRemovedError, OeOauthNeedsConsentError, OeOauthNeedsReconsentError, OeOauthRefreshContendedError, isInvalidGrant } from "./errors.js";
|
|
25
47
|
|
|
26
48
|
/** A subject a token is minted for — the service identity, or a specific user principal. */
|
|
27
49
|
export type OeOauthSubject = { readonly type: "user"; readonly id: string } | { readonly type: "app" };
|
|
@@ -77,9 +99,11 @@ interface PendingRecord {
|
|
|
77
99
|
/** The RFC 8707 resource indicator. ABSENT on a `static-confidential` row — that dialect sends
|
|
78
100
|
* none — and present on every discovery row, including every row written before OPE-1086. */
|
|
79
101
|
readonly resource?: string;
|
|
80
|
-
/** Stamped ONLY on a `static-confidential` row, from the config's declared mode at mint time. Its
|
|
102
|
+
/** Stamped ONLY on a `static-confidential` or an `xaa` row, from the config's declared mode at mint time. Its
|
|
81
103
|
* absence is the discovery dialect, which is what every pre-OPE-1086 row means. */
|
|
82
|
-
readonly mode?: typeof OE_OAUTH_MODE_STATIC;
|
|
104
|
+
readonly mode?: typeof OE_OAUTH_MODE_STATIC | typeof OE_OAUTH_MODE_XAA;
|
|
105
|
+
/** The OIDC `nonce` of an `xaa` sign-in (OPE-2453): the id token must carry it back. Absent on other rows. */
|
|
106
|
+
readonly nonce?: string;
|
|
83
107
|
readonly redirectUri: string;
|
|
84
108
|
readonly scopes?: readonly string[];
|
|
85
109
|
readonly createdAt: number;
|
|
@@ -106,6 +130,31 @@ interface PendingSweepReport {
|
|
|
106
130
|
readonly unreadable: number;
|
|
107
131
|
}
|
|
108
132
|
|
|
133
|
+
/**
|
|
134
|
+
* A person's IdP grant for Cross App Access (OPE-2453): the IdP refresh token their one sign-in yielded, and the
|
|
135
|
+
* IdP account (`sub`) the identity-link plane confirmed as theirs. ONE row per (IdP client, person), shared by every
|
|
136
|
+
* `xaa` connection that names the same IdP client — so one sign-in serves every paired app.
|
|
137
|
+
*/
|
|
138
|
+
interface IdpGrantRecord {
|
|
139
|
+
readonly refreshToken: string;
|
|
140
|
+
readonly idpSubject: string;
|
|
141
|
+
readonly obtainedAt: number;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** The IdP scopes the Cross App Access sign-in asks for: an id token (`openid`) and a refresh token to exchange
|
|
145
|
+
* (`offline_access`). Fixed — the app's own scopes ride leg 1 and leg 2, never this sign-in. */
|
|
146
|
+
const XAA_IDP_SCOPES = ["openid", "offline_access"] as const;
|
|
147
|
+
|
|
148
|
+
/** RFC 6749 / 8693 error codes that mean the IdP REFUSED this person's ID-JAG on policy (the admin removed the
|
|
149
|
+
* pairing or the assignment). `invalid_grant` is NOT here: it means the IdP sign-in itself lapsed, and a new
|
|
150
|
+
* sign-in fixes that. */
|
|
151
|
+
const XAA_IDP_REFUSALS: ReadonlySet<string> = new Set(["access_denied", "unauthorized_client", "invalid_target"]);
|
|
152
|
+
/** Error codes that mean the APP refused the ID-JAG for this person. No new sign-in fixes these. */
|
|
153
|
+
const XAA_APP_REFUSALS: ReadonlySet<string> = new Set(["invalid_grant", "access_denied", "unauthorized_client"]);
|
|
154
|
+
|
|
155
|
+
/** How long a discovered OIDC document is reused before it is fetched again. */
|
|
156
|
+
const OIDC_DISCOVERY_TTL_MS = 60 * 60 * 1000;
|
|
157
|
+
|
|
109
158
|
/** A persisted token record. `expiresAt` absent ⇒ no known expiry (never proactively refreshed). */
|
|
110
159
|
interface TokenRecord {
|
|
111
160
|
readonly accessToken: string;
|
|
@@ -156,6 +205,20 @@ export interface OeOauthVaultDeps {
|
|
|
156
205
|
* every other stored secret takes. Read PER TOKEN CALL, so a rotated secret takes effect at once.
|
|
157
206
|
*/
|
|
158
207
|
readonly resolveSecret?: (broker: string | undefined, ref: string) => Promise<string>;
|
|
208
|
+
/** Mints the OIDC nonce of an `xaa` sign-in. Default: the same generator as the opaque state. */
|
|
209
|
+
readonly newNonce?: () => string;
|
|
210
|
+
/** Verify an `xaa` sign-in's id token. Default: core's `verifyOidcIdToken` (jose, full signature check). */
|
|
211
|
+
readonly verifyIdToken?: typeof verifyOidcIdToken;
|
|
212
|
+
/** Decide whether the IdP account that signed in may complete this requester's `xaa` link. Default: core's
|
|
213
|
+
* `checkXaaIdentityBinding` — a confirmed `okta` account on the requester's link-set, refusal by default. */
|
|
214
|
+
readonly bindIdentity?: (input: {
|
|
215
|
+
readonly principalSubject: string;
|
|
216
|
+
readonly signerSubject: string | undefined;
|
|
217
|
+
readonly connection: string;
|
|
218
|
+
readonly stage?: "sign-in" | "mint";
|
|
219
|
+
}) => Promise<XaaBindingVerdict>;
|
|
220
|
+
/** Audit an `xaa` sign-in refused before the binding ran. Default: core's `recordXaaRefusal`. */
|
|
221
|
+
readonly recordRefusal?: typeof recordXaaRefusal;
|
|
159
222
|
}
|
|
160
223
|
|
|
161
224
|
function subjectKey(subject: OeOauthSubject): string {
|
|
@@ -191,10 +254,18 @@ export function makeOeOauthTokenVault(source: OeOauthProviderSource, deps: OeOau
|
|
|
191
254
|
const newClaimId = deps.newClaimId ?? (() => randomUUID());
|
|
192
255
|
const sleep = deps.sleep ?? realSleep;
|
|
193
256
|
const resolveSecret = deps.resolveSecret ?? ((broker: string | undefined, ref: string) => getSecretVia(broker, ref));
|
|
257
|
+
const newNonce = deps.newNonce ?? makeState;
|
|
258
|
+
const verifyIdToken = deps.verifyIdToken ?? verifyOidcIdToken;
|
|
259
|
+
const bindIdentity = deps.bindIdentity ?? checkXaaIdentityBinding;
|
|
260
|
+
const recordRefusal = deps.recordRefusal ?? recordXaaRefusal;
|
|
194
261
|
|
|
195
262
|
const dcrRef = (providerRef: string) => `oe-oauth:dcr:${providerRef}`;
|
|
196
263
|
const pendRef = (state: string) => `oe-oauth:pend:${state}`;
|
|
197
264
|
const tokRef = (providerRef: string, subject: OeOauthSubject) => `oe-oauth:tok:${providerRef}:${subjectKey(subject)}`;
|
|
265
|
+
// The IdP grant is keyed by the IdP CLIENT (issuer + client id), not by the connection: every `xaa` connection
|
|
266
|
+
// that names the same IdP client shares one person's sign-in. Hashed so the ref carries no URL punctuation.
|
|
267
|
+
const idpKey = (cfg: OeOauthXaaProviderConfig) => createHash("sha256").update(`${cfg.idpIssuer}\n${cfg.idpClientId}`).digest("hex").slice(0, 32);
|
|
268
|
+
const idpRef = (cfg: OeOauthXaaProviderConfig, subject: OeOauthSubject) => `oe-oauth:xaa-idp:${idpKey(cfg)}:${subjectKey(subject)}`;
|
|
198
269
|
|
|
199
270
|
function providerConfig(providerRef: string): OeOauthProviderConfig {
|
|
200
271
|
const cfg = providersNow().get(providerRef);
|
|
@@ -234,6 +305,7 @@ export function makeOeOauthTokenVault(source: OeOauthProviderSource, deps: OeOau
|
|
|
234
305
|
// function entirely. Stated as a refusal rather than a cast so a future caller that reaches it
|
|
235
306
|
// directly gets an error instead of a discovery request built from an absent `resource`.
|
|
236
307
|
if (isStaticConfidential(cfg)) throw new Error(`connection "${providerRef}": declares the "${OE_OAUTH_MODE_STATIC}" mode, which registers no dynamic client.`);
|
|
308
|
+
if (isXaa(cfg)) throw new Error(`connection "${providerRef}": declares the "${OE_OAUTH_MODE_XAA}" mode, which registers no dynamic client.`);
|
|
237
309
|
return vault.withLock(dcrRef(providerRef), async () => {
|
|
238
310
|
const again = await readJson<DcrClientRecord>(dcrRef(providerRef));
|
|
239
311
|
if (again && !dcrRecordNeedsBackfill(again)) return again; // another caller already completed it
|
|
@@ -324,6 +396,230 @@ export function makeOeOauthTokenVault(source: OeOauthProviderSource, deps: OeOau
|
|
|
324
396
|
};
|
|
325
397
|
}
|
|
326
398
|
|
|
399
|
+
/** A confidential client for one `xaa` leg, secret read through its broker PER CALL. The error names the ref. */
|
|
400
|
+
async function xaaClient(cfg: OeOauthXaaProviderConfig, leg: "idp" | "app"): Promise<ConfidentialClient> {
|
|
401
|
+
const [clientId, secretRef, broker] =
|
|
402
|
+
leg === "idp" ? [cfg.idpClientId, cfg.idpClientSecretRef, cfg.idpClientSecretBroker] : [cfg.resourceClientId, cfg.resourceClientSecretRef, cfg.resourceClientSecretBroker];
|
|
403
|
+
const secret = await resolveSecret(broker, secretRef);
|
|
404
|
+
if (typeof secret !== "string" || secret === "") {
|
|
405
|
+
throw new Error(
|
|
406
|
+
`connection "${cfg.ref}": the ${leg === "idp" ? "identity-provider" : "app"} client secret at ref "${secretRef}" (broker "${broker}") has no stored value — ` +
|
|
407
|
+
`store it with \`oe secret set ${secretRef}\` before this connection can mint a token.`,
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
return { clientId, clientSecret: secret, bodyEncoding: "form" };
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// The IdP's discovered endpoints, per issuer, reused for an hour: every mint's leg 1 needs the token endpoint,
|
|
414
|
+
// and a discovery hop per mint would double the IdP traffic for nothing.
|
|
415
|
+
const oidcCache = new Map<string, { readonly at: number; readonly endpoints: OidcEndpoints }>();
|
|
416
|
+
async function oidcFor(cfg: OeOauthXaaProviderConfig): Promise<OidcEndpoints> {
|
|
417
|
+
const hit = oidcCache.get(cfg.idpIssuer);
|
|
418
|
+
if (hit && now() - hit.at < OIDC_DISCOVERY_TTL_MS) return hit.endpoints;
|
|
419
|
+
const endpoints = await discoverOidc(cfg.idpIssuer, doFetch);
|
|
420
|
+
oidcCache.set(cfg.idpIssuer, { at: now(), endpoints });
|
|
421
|
+
return endpoints;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/** An `xaa` connection acts only AS A PERSON: the ID-JAG names a human. There is no service account to mint. */
|
|
425
|
+
function requireXaaPerson(providerRef: string, subject: OeOauthSubject): asserts subject is { readonly type: "user"; readonly id: string } {
|
|
426
|
+
if (subject.type !== "user") {
|
|
427
|
+
throw new Error(`connection "${providerRef}": the "${OE_OAUTH_MODE_XAA}" sign-in acts only as a person — it has no service account to mint for.`);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Start the person's IdP sign-in for an `xaa` connection: OIDC auth code with PKCE and a nonce, `openid` +
|
|
433
|
+
* `offline_access`. The pending row is the same shape as the other dialects', stamped `mode: "xaa"`.
|
|
434
|
+
*/
|
|
435
|
+
async function xaaStartAuthorization(
|
|
436
|
+
cfg: OeOauthXaaProviderConfig,
|
|
437
|
+
subject: OeOauthSubject,
|
|
438
|
+
options: { readonly expiresInMs?: number; readonly consent?: unknown },
|
|
439
|
+
): Promise<{ url: string }> {
|
|
440
|
+
requireXaaPerson(cfg.ref, subject);
|
|
441
|
+
const oidc = await oidcFor(cfg);
|
|
442
|
+
const redirectUri = redirectUriOf();
|
|
443
|
+
if (!redirectUri) throw new Error(`connection "${cfg.ref}": OE_PUBLIC_BASE_URL is unset — cannot build a redirect_uri.`);
|
|
444
|
+
const state = newState();
|
|
445
|
+
const pkce = newPkce();
|
|
446
|
+
const nonce = newNonce();
|
|
447
|
+
const pending: PendingRecord = {
|
|
448
|
+
providerRef: cfg.ref,
|
|
449
|
+
subject,
|
|
450
|
+
pkceVerifier: pkce.verifier,
|
|
451
|
+
tokenEndpoint: oidc.tokenEndpoint,
|
|
452
|
+
clientId: cfg.idpClientId,
|
|
453
|
+
mode: OE_OAUTH_MODE_XAA,
|
|
454
|
+
nonce,
|
|
455
|
+
redirectUri,
|
|
456
|
+
scopes: XAA_IDP_SCOPES,
|
|
457
|
+
createdAt: now(),
|
|
458
|
+
expiresAt: now() + (options.expiresInMs ?? pendingTtl),
|
|
459
|
+
...(options.consent ? { consent: options.consent } : {}),
|
|
460
|
+
};
|
|
461
|
+
await vault.write(pendRef(state), JSON.stringify(pending), { connector: cfg.ref });
|
|
462
|
+
// The PKCE pair and the nonce ride `extraParams`: this function's own parameters (client_id, redirect_uri,
|
|
463
|
+
// state, scope) are applied after them and win, so nothing here can be overridden.
|
|
464
|
+
const url = buildStaticAuthorizationUrl({
|
|
465
|
+
authorizationEndpoint: oidc.authorizationEndpoint,
|
|
466
|
+
clientId: cfg.idpClientId,
|
|
467
|
+
redirectUri,
|
|
468
|
+
state,
|
|
469
|
+
scopes: XAA_IDP_SCOPES,
|
|
470
|
+
extraParams: { nonce, code_challenge: pkce.challenge, code_challenge_method: "S256" },
|
|
471
|
+
});
|
|
472
|
+
return { url };
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* Complete an `xaa` sign-in: exchange the code at the IdP, VERIFY the id token (signature, iss, aud, nonce), and
|
|
477
|
+
* admit the sign-in only when the identity-link plane confirms the IdP account is the requester's own. Then store
|
|
478
|
+
* the person's IdP refresh token as their IdP grant. A refusal SPENDS the link (the pending row is consumed).
|
|
479
|
+
*/
|
|
480
|
+
async function xaaCompleteAuthorization(state: string, pending: PendingRecord, code: string): Promise<{ rebound: boolean }> {
|
|
481
|
+
const cfg = providersNow().get(pending.providerRef);
|
|
482
|
+
if (cfg === undefined || !isXaa(cfg) || cfg.idpClientId !== pending.clientId) {
|
|
483
|
+
throw new Error(`connection "${pending.providerRef}": this sign-in started under an "${OE_OAUTH_MODE_XAA}" declaration that no longer stands — start again.`);
|
|
484
|
+
}
|
|
485
|
+
const spend = () => vault.write(pendRef(state), JSON.stringify({ ...pending, consumed: true, pkceVerifier: "", nonce: "", consent: undefined }), { connector: pending.providerRef });
|
|
486
|
+
const tok = await exchangeCodeConfidential(pending.tokenEndpoint, { code, redirectUri: pending.redirectUri, codeVerifier: pending.pkceVerifier }, await xaaClient(cfg, "idp"), doFetch);
|
|
487
|
+
let signer: string | undefined;
|
|
488
|
+
try {
|
|
489
|
+
if (!tok.id_token) throw new Error("the IdP returned no id token");
|
|
490
|
+
const oidc = await oidcFor(cfg);
|
|
491
|
+
signer = (await verifyIdToken(tok.id_token, { jwks: (await fetchJwks(oidc.jwksUri, doFetch)) as Parameters<typeof verifyOidcIdToken>[1]["jwks"], issuer: cfg.idpIssuer, audience: cfg.idpClientId, nonce: pending.nonce ?? "" })).sub;
|
|
492
|
+
} catch (e) {
|
|
493
|
+
await spend();
|
|
494
|
+
const reason = e instanceof Error ? e.message : String(e);
|
|
495
|
+
// Every refused attempt leaves one audit row — a forged or replayed id token most of all.
|
|
496
|
+
if (pending.subject.type === "user") await recordRefusal({ principalSubject: pending.subject.id, connection: pending.providerRef, reason });
|
|
497
|
+
throw new Error(`connection "${pending.providerRef}": the identity-provider sign-in was refused — ${reason}.`);
|
|
498
|
+
}
|
|
499
|
+
if (pending.subject.type !== "user") {
|
|
500
|
+
await spend();
|
|
501
|
+
throw new Error(`connection "${pending.providerRef}": the "${OE_OAUTH_MODE_XAA}" sign-in acts only as a person.`);
|
|
502
|
+
}
|
|
503
|
+
// 🔴 THE BINDING. Whoever opened the link signed in; the grant is stored for the requester only when that IdP
|
|
504
|
+
// account is a CONFIRMED account of theirs. Otherwise one person could mint another person's app tokens.
|
|
505
|
+
const verdict = await bindIdentity({ principalSubject: pending.subject.id, signerSubject: signer, connection: pending.providerRef });
|
|
506
|
+
if (!verdict.ok) {
|
|
507
|
+
await spend();
|
|
508
|
+
throw new Error(`connection "${pending.providerRef}": the identity-provider sign-in was refused — ${verdict.reason}.`);
|
|
509
|
+
}
|
|
510
|
+
if (!tok.refresh_token) {
|
|
511
|
+
await spend();
|
|
512
|
+
await recordRefusal({ principalSubject: pending.subject.id, connection: pending.providerRef, reason: "the IdP returned no refresh token", signerSubject: verdict.idpNativeId });
|
|
513
|
+
throw new Error(`connection "${pending.providerRef}": the IdP returned no refresh token — the OE app at the IdP must allow the refresh-token grant and the offline_access scope.`);
|
|
514
|
+
}
|
|
515
|
+
const gRef = idpRef(cfg, pending.subject);
|
|
516
|
+
const rebound = (await readJson<IdpGrantRecord>(gRef)) !== null;
|
|
517
|
+
const grant: IdpGrantRecord = { refreshToken: tok.refresh_token, idpSubject: verdict.idpNativeId, obtainedAt: now() };
|
|
518
|
+
await vault.write(gRef, JSON.stringify(grant), { connector: pending.providerRef });
|
|
519
|
+
await spend();
|
|
520
|
+
return { rebound };
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/** The RFC 6749 `error` code an upstream failure carried, if any. */
|
|
524
|
+
function oauthErrorOf(e: unknown): string | undefined {
|
|
525
|
+
const code = (e as { oauthError?: unknown } | null)?.oauthError;
|
|
526
|
+
return typeof code === "string" ? code.toLowerCase() : undefined;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/** ONE `xaa` mint, run by the claim winner: leg 1 at the IdP, leg 2 at the app, then cache the app token for its
|
|
530
|
+
* lifetime. Never a refresh token from the app — a re-mint is the only refresh. */
|
|
531
|
+
async function xaaMint(cfg: OeOauthXaaProviderConfig, providerRef: string, subject: OeOauthSubject, grant: IdpGrantRecord): Promise<string> {
|
|
532
|
+
const oidc = await oidcFor(cfg);
|
|
533
|
+
const legParams = { ...(cfg.resource ? { resource: cfg.resource } : {}), ...(cfg.scopes ? { scopes: cfg.scopes } : {}) };
|
|
534
|
+
let idJag: string;
|
|
535
|
+
try {
|
|
536
|
+
idJag = await requestIdJag(oidc.tokenEndpoint, { subjectToken: grant.refreshToken, audience: cfg.audience, ...legParams }, await xaaClient(cfg, "idp"), doFetch);
|
|
537
|
+
} catch (e) {
|
|
538
|
+
const code = oauthErrorOf(e);
|
|
539
|
+
if (code === "invalid_grant") throw new OeOauthNeedsReconsentError(providerRef, subject, "the identity-provider sign-in is no longer valid (invalid_grant)");
|
|
540
|
+
if (code !== undefined && XAA_IDP_REFUSALS.has(code)) throw new OeOauthAccessRemovedError(providerRef, subject, "idp", code);
|
|
541
|
+
throw e;
|
|
542
|
+
}
|
|
543
|
+
let tok: OAuthTokenResponse;
|
|
544
|
+
try {
|
|
545
|
+
tok = await jwtBearerGrant(cfg.resourceTokenEndpoint, { assertion: idJag, ...legParams }, await xaaClient(cfg, "app"), doFetch);
|
|
546
|
+
} catch (e) {
|
|
547
|
+
const code = oauthErrorOf(e);
|
|
548
|
+
if (code !== undefined && XAA_APP_REFUSALS.has(code)) throw new OeOauthAccessRemovedError(providerRef, subject, "app", code);
|
|
549
|
+
throw e;
|
|
550
|
+
}
|
|
551
|
+
const { refresh_token: _ignored, ...withoutRefresh } = tok;
|
|
552
|
+
return (await storeToken(providerRef, subject, withoutRefresh)).accessToken;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/**
|
|
556
|
+
* Read-or-mint an `xaa` connection's app token. A cached token serves until its expiry (less the skew), so a
|
|
557
|
+
* person costs ONE mint per token lifetime — the IdP caps ID-JAGs per person per app per month. The CROSS-PROCESS
|
|
558
|
+
* claim sits on the person's IdP GRANT row (it always exists here; the app-token row may not yet), and a loser
|
|
559
|
+
* polls for the winner's token or for the claim to come free, bounded like the refresh path.
|
|
560
|
+
*/
|
|
561
|
+
async function xaaAccessTokenFor(cfg: OeOauthXaaProviderConfig, providerRef: string, subject: OeOauthSubject): Promise<string> {
|
|
562
|
+
requireXaaPerson(providerRef, subject);
|
|
563
|
+
const ref = tokRef(providerRef, subject);
|
|
564
|
+
const fast = await readJson<TokenRecord>(ref);
|
|
565
|
+
if (fast && !isExpired(fast)) return fast.accessToken;
|
|
566
|
+
const gRef = idpRef(cfg, subject);
|
|
567
|
+
return vault.withLock(ref, async () => {
|
|
568
|
+
const deadline = now() + LOSER_WAIT_BUDGET_MS;
|
|
569
|
+
for (;;) {
|
|
570
|
+
const rec = await readJson<TokenRecord>(ref);
|
|
571
|
+
if (rec && !isExpired(rec)) return rec.accessToken;
|
|
572
|
+
const grant = await readJson<IdpGrantRecord>(gRef);
|
|
573
|
+
if (!grant) throw new OeOauthNeedsConsentError(providerRef, subject);
|
|
574
|
+
const claimId = newClaimId();
|
|
575
|
+
if (await vault.claim(gRef, claimId, now(), CLAIM_LEASE_TTL_MS)) {
|
|
576
|
+
try {
|
|
577
|
+
// Another process may have minted and released between our read and our claim: adopt its token.
|
|
578
|
+
const won = await readJson<TokenRecord>(ref);
|
|
579
|
+
if (won && !isExpired(won)) return won.accessToken;
|
|
580
|
+
// 🔴 RE-CHECK THE BINDING AT EACH MINT, not only at sign-in. An operator who revokes the `okta` link or
|
|
581
|
+
// suspends the set must stop the minting at once; the stored grant alone would keep minting until the
|
|
582
|
+
// IdP refresh token dies. The check runs per MINT (once per token lifetime), never per cached read.
|
|
583
|
+
const still = await bindIdentity({ principalSubject: subject.id, signerSubject: grant.idpSubject, connection: providerRef, stage: "mint" });
|
|
584
|
+
// An UNREADABLE link plane says nothing about the person: refuse this mint, keep the grant, and let a
|
|
585
|
+
// retry succeed. Only a definite refusal (the link is gone) deletes the grant.
|
|
586
|
+
if (!still.ok && still.unreadable) {
|
|
587
|
+
throw new Error(`connection "${providerRef}": the identity-link plane could not be read, so the Okta grant could not be re-checked — retry.`);
|
|
588
|
+
}
|
|
589
|
+
if (!still.ok) {
|
|
590
|
+
await vault.delete(gRef);
|
|
591
|
+
throw new OeOauthNeedsConsentError(providerRef, subject);
|
|
592
|
+
}
|
|
593
|
+
return await xaaMint(cfg, providerRef, subject, grant);
|
|
594
|
+
} finally {
|
|
595
|
+
await vault.releaseClaim(gRef, claimId);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
if (now() >= deadline) throw new OeOauthRefreshContendedError(providerRef, subject);
|
|
599
|
+
await sleep(LOSER_POLL_MS);
|
|
600
|
+
}
|
|
601
|
+
});
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/** Purge a person's IdP grant: revoke the IdP refresh token at the IdP (best-effort), then delete the row. */
|
|
605
|
+
async function forgetIdpGrant(cfg: OeOauthXaaProviderConfig, subject: OeOauthSubject): Promise<boolean> {
|
|
606
|
+
const gRef = idpRef(cfg, subject);
|
|
607
|
+
return vault.withLock(gRef, async () => {
|
|
608
|
+
const grant = await readJson<IdpGrantRecord>(gRef);
|
|
609
|
+
if (grant) {
|
|
610
|
+
try {
|
|
611
|
+
const oidc = await oidcFor(cfg);
|
|
612
|
+
if (oidc.revocationEndpoint) {
|
|
613
|
+
await revokeTokenConfidential(oidc.revocationEndpoint, { token: grant.refreshToken, tokenTypeHint: "refresh_token" }, await xaaClient(cfg, "idp"), doFetch);
|
|
614
|
+
}
|
|
615
|
+
} catch (e) {
|
|
616
|
+
console.warn(`[oe-oauth] IdP revocation failed for "${cfg.ref}" (purging locally anyway): ${e instanceof Error ? e.message : String(e)}`);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
return vault.delete(gRef);
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
|
|
327
623
|
/**
|
|
328
624
|
* Exchange the callback's `code` against ONE pending row, in the dialect that row was minted in.
|
|
329
625
|
* 🔴 The row's stamp and the live config must AGREE: a disagreement is REFUSED rather than resolved.
|
|
@@ -333,6 +629,9 @@ export function makeOeOauthTokenVault(source: OeOauthProviderSource, deps: OeOau
|
|
|
333
629
|
// mode existed, and it must not start failing on a config-absent ref now. The drift check below
|
|
334
630
|
// therefore runs only when there IS a config to compare against.
|
|
335
631
|
const cfg = providersNow().get(pending.providerRef);
|
|
632
|
+
if (cfg !== undefined && isXaa(cfg)) {
|
|
633
|
+
throw new Error(`connection "${pending.providerRef}": this authorization started under another dialect, but the provider config now declares "${OE_OAUTH_MODE_XAA}" — start again.`);
|
|
634
|
+
}
|
|
336
635
|
const startedStatic = pending.mode === OE_OAUTH_MODE_STATIC;
|
|
337
636
|
if (cfg !== undefined && isStaticConfidential(cfg) !== startedStatic) {
|
|
338
637
|
throw new Error(
|
|
@@ -469,7 +768,39 @@ export function makeOeOauthTokenVault(source: OeOauthProviderSource, deps: OeOau
|
|
|
469
768
|
lastError = e;
|
|
470
769
|
}
|
|
471
770
|
}
|
|
472
|
-
|
|
771
|
+
// (OPE-2453) A person's IdP grant is shared by every `xaa` connection naming one IdP client, so it is purged
|
|
772
|
+
// once per IdP client, after the per-connection rows. It is the credential that mints all of them.
|
|
773
|
+
const seenIdp = new Set<string>();
|
|
774
|
+
for (const cfg of all.values()) {
|
|
775
|
+
if (!isXaa(cfg) || seenIdp.has(idpKey(cfg))) continue;
|
|
776
|
+
seenIdp.add(idpKey(cfg));
|
|
777
|
+
try {
|
|
778
|
+
if (await forgetIdpGrant(cfg, subject)) purged++;
|
|
779
|
+
} catch (e) {
|
|
780
|
+
failures++;
|
|
781
|
+
lastError = e;
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
// An IdP grant whose declaration is GONE is still a live IdP refresh token at rest. With no config there is no
|
|
785
|
+
// client to revoke it with, so it is deleted locally and the gap is said out loud.
|
|
786
|
+
// The ref is `oe-oauth:xaa-idp:<32 hex>:<subject key>`; the subject key is matched EXACTLY, never by suffix.
|
|
787
|
+
const covered = new Set([...all.values()].filter(isXaa).map((cfg) => idpRef(cfg, subject)));
|
|
788
|
+
const orphanRef = /^oe-oauth:xaa-idp:[0-9a-f]{32}:(.*)$/u;
|
|
789
|
+
let orphanFailed = false;
|
|
790
|
+
try {
|
|
791
|
+
for (const ref of await vault.listRefs("oe-oauth:xaa-idp:")) {
|
|
792
|
+
if (orphanRef.exec(ref)?.[1] !== subjectKey(subject) || covered.has(ref)) continue;
|
|
793
|
+
console.warn(`[oe-oauth] an IdP grant with no declared xaa connection was deleted locally — it could not be revoked at the IdP.`);
|
|
794
|
+
if (await vault.delete(ref)) purged++;
|
|
795
|
+
}
|
|
796
|
+
} catch (e) {
|
|
797
|
+
failures++;
|
|
798
|
+
lastError = e;
|
|
799
|
+
orphanFailed = true;
|
|
800
|
+
}
|
|
801
|
+
// "Every purge attempt failed" counts only the attempts that ran: the orphan sweep is one attempt only when
|
|
802
|
+
// it failed. Counting it always would let a purge where every provider failed return 0 with no error.
|
|
803
|
+
if (failures > 0 && purged === 0 && failures === all.size + seenIdp.size + (orphanFailed ? 1 : 0)) throw lastError;
|
|
473
804
|
return purged;
|
|
474
805
|
},
|
|
475
806
|
|
|
@@ -480,7 +811,11 @@ export function makeOeOauthTokenVault(source: OeOauthProviderSource, deps: OeOau
|
|
|
480
811
|
* counts as absent (mirrors the mint's readJson semantics — fail toward re-consent, never crash).
|
|
481
812
|
*/
|
|
482
813
|
async hasToken(providerRef: string, subject: OeOauthSubject): Promise<boolean> {
|
|
483
|
-
|
|
814
|
+
if ((await readJson<TokenRecord>(tokRef(providerRef, subject))) !== null) return true;
|
|
815
|
+
// (OPE-2453) For an `xaa` connection the person's GRANT is their IdP sign-in: with it, the next mint
|
|
816
|
+
// succeeds with no consent screen, even for an app this person has never used.
|
|
817
|
+
const cfg = providersNow().get(providerRef);
|
|
818
|
+
return cfg !== undefined && isXaa(cfg) && (await readJson<IdpGrantRecord>(idpRef(cfg, subject))) !== null;
|
|
484
819
|
},
|
|
485
820
|
|
|
486
821
|
/**
|
|
@@ -492,6 +827,16 @@ export function makeOeOauthTokenVault(source: OeOauthProviderSource, deps: OeOau
|
|
|
492
827
|
subject: OeOauthSubject,
|
|
493
828
|
options: { readonly expiresInMs?: number; readonly consent?: unknown },
|
|
494
829
|
): Promise<{ url: string }> {
|
|
830
|
+
const xaaCfg = providersNow().get(providerRef);
|
|
831
|
+
if (xaaCfg !== undefined && isXaa(xaaCfg)) {
|
|
832
|
+
const out = await xaaStartAuthorization(xaaCfg, subject, options);
|
|
833
|
+
try {
|
|
834
|
+
await sweepExpiredPending();
|
|
835
|
+
} catch (e) {
|
|
836
|
+
console.warn(`[oe-oauth] the expired-pending sweep failed (the authorization URL is unaffected): ${e instanceof Error ? e.message : String(e)}`);
|
|
837
|
+
}
|
|
838
|
+
return out;
|
|
839
|
+
}
|
|
495
840
|
const client = await ensureClient(providerRef);
|
|
496
841
|
const redirectUri = redirectUriOf();
|
|
497
842
|
if (!redirectUri) throw new Error(`connection "${providerRef}": OE_PUBLIC_BASE_URL is unset — cannot build a redirect_uri.`);
|
|
@@ -552,6 +897,10 @@ export function makeOeOauthTokenVault(source: OeOauthProviderSource, deps: OeOau
|
|
|
552
897
|
if (!pending) throw new Error("unknown or expired authorization state");
|
|
553
898
|
if (pending.consumed) throw new Error("authorization state already used");
|
|
554
899
|
if (now() > pending.expiresAt) throw new Error("authorization state expired");
|
|
900
|
+
if (pending.mode === OE_OAUTH_MODE_XAA) {
|
|
901
|
+
const { rebound } = await xaaCompleteAuthorization(state, pending, code);
|
|
902
|
+
return { providerRef: pending.providerRef, subject: pending.subject, rebound, ...(pending.consent ? { consent: pending.consent } : {}) };
|
|
903
|
+
}
|
|
555
904
|
const tok = await exchangeForPending(pending, code);
|
|
556
905
|
// 🔴 READ BEFORE THE WRITE. `storeToken` writes the row unconditionally (last-write-wins), so the
|
|
557
906
|
// only moment the REBIND is observable is here, before it. A second admin replacing the app row
|
|
@@ -586,6 +935,8 @@ export function makeOeOauthTokenVault(source: OeOauthProviderSource, deps: OeOau
|
|
|
586
935
|
* single-flight runs — an in-process mutex, then a cross-process row CLAIM that elects ONE refresher.
|
|
587
936
|
*/
|
|
588
937
|
async accessTokenFor(providerRef: string, subject: OeOauthSubject): Promise<string> {
|
|
938
|
+
const xaaCfg = providersNow().get(providerRef);
|
|
939
|
+
if (xaaCfg !== undefined && isXaa(xaaCfg)) return xaaAccessTokenFor(xaaCfg, providerRef, subject);
|
|
589
940
|
const ref = tokRef(providerRef, subject);
|
|
590
941
|
const fast = await readJson<TokenRecord>(ref);
|
|
591
942
|
if (!fast) throw new OeOauthNeedsConsentError(providerRef, subject);
|
|
@@ -8,7 +8,17 @@ export interface OktaDirectoryDeps {
|
|
|
8
8
|
readonly fetch?: FetchLike;
|
|
9
9
|
/** Resolve the API token. Default: core's broker seam — see config.ts's header. */
|
|
10
10
|
readonly resolveToken?: (broker: string, ref: string) => Promise<string>;
|
|
11
|
+
/** Does a connection sign people in through THIS Okta org for Cross App Access? Default: a declared `xaa`
|
|
12
|
+
* sign-in whose `idpIssuer` has this org's origin. See {@link xaaNamesOrg}. */
|
|
13
|
+
readonly proposeOwnAccount?: () => boolean;
|
|
11
14
|
}
|
|
15
|
+
/**
|
|
16
|
+
* (OPE-2453) True when a declared `xaa` sign-in names this Okta org as its IdP. Only then does the arm propose each
|
|
17
|
+
* person's OWN Okta account (`okta`, the Okta user id) beside the app accounts: the Cross App Access completion
|
|
18
|
+
* admits a sign-in only for a confirmed `okta` account, and nothing else writes one. With no `xaa` declaration
|
|
19
|
+
* the arm proposes exactly what it did before, so a deployment that does not use Cross App Access sees no change.
|
|
20
|
+
*/
|
|
21
|
+
export declare function xaaNamesOrg(orgUrl: string, declared?: ReadonlyMap<string, unknown>): boolean;
|
|
12
22
|
/**
|
|
13
23
|
* The `rel="next"` URL from the RFC-5988 `Link` header. Okta paginates with an opaque cursor and no total
|
|
14
24
|
* count, so following the header is the ONLY correct way to page — an offset would truncate or duplicate.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"okta.d.ts","sourceRoot":"","sources":["../../../agent/directory/okta.ts"],"names":[],"mappings":"AAGA,OAAO,
|
|
1
|
+
{"version":3,"file":"okta.d.ts","sourceRoot":"","sources":["../../../agent/directory/okta.ts"],"names":[],"mappings":"AAGA,OAAO,EAML,KAAK,iBAAiB,EACvB,MAAM,4CAA4C,CAAC;AACpD,OAAO,KAAK,EAAkB,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAEvE,oGAAoG;AACpG,MAAM,MAAM,SAAS,GAAG,OAAO,KAAK,CAAC;AAoDrC,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,MAAM,EAAE,mBAAmB,CAAC;IACrC,8DAA8D;IAC9D,QAAQ,CAAC,KAAK,CAAC,EAAE,SAAS,CAAC;IAC3B,mFAAmF;IACnF,QAAQ,CAAC,YAAY,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IACzE;oFACgF;IAChF,QAAQ,CAAC,iBAAiB,CAAC,EAAE,MAAM,OAAO,CAAC;CAC5C;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,GAAE,WAAW,CAAC,MAAM,EAAE,OAAO,CAA+B,GAAG,OAAO,CAiBzH;AAUD;;;GAGG;AACH,wBAAgB,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAO9E;AAED,2GAA2G;AAC3G,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,iBAAiB,GAAG,iBAAiB,CAkIpF"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// THE OKTA DIRECTORY ADAPTER — everything it returns is a PROPOSAL, and nothing here confirms a link. The
|
|
2
2
|
// grade follows the field: `externalId` is attested (the SCIM write-back), `credentials.userName` asserted.
|
|
3
|
-
import { getSecretVia } from "@open-everywhere/core/agent/adapter-api.js";
|
|
3
|
+
import { declaredConnectionSignIns, getSecretVia, OKTA_LINK_PLATFORM, } from "@open-everywhere/core/agent/adapter-api.js";
|
|
4
4
|
/** Okta user statuses that mean "this person is deactivated". Everything else (ACTIVE, PROVISIONED,
|
|
5
5
|
* STAGED, RECOVERY, LOCKED_OUT, PASSWORD_EXPIRED) is a live human with an access problem, not a
|
|
6
6
|
* departed one — and treating a locked-out person as deprovisioned would suspend their identity
|
|
@@ -13,6 +13,34 @@ const APP_PAGE_LIMIT = 500;
|
|
|
13
13
|
/** A hard cap on pages followed per resource — a runaway `Link: rel="next"` loop (a proxy rewriting
|
|
14
14
|
* the header, a vendor bug) must not spin forever inside a scheduled tick. 200 pages ≈ 40k users. */
|
|
15
15
|
const MAX_PAGES = 200;
|
|
16
|
+
/**
|
|
17
|
+
* (OPE-2453) True when a declared `xaa` sign-in names this Okta org as its IdP. Only then does the arm propose each
|
|
18
|
+
* person's OWN Okta account (`okta`, the Okta user id) beside the app accounts: the Cross App Access completion
|
|
19
|
+
* admits a sign-in only for a confirmed `okta` account, and nothing else writes one. With no `xaa` declaration
|
|
20
|
+
* the arm proposes exactly what it did before, so a deployment that does not use Cross App Access sees no change.
|
|
21
|
+
*/
|
|
22
|
+
export function xaaNamesOrg(orgUrl, declared = declaredConnectionSignIns()) {
|
|
23
|
+
let origin;
|
|
24
|
+
try {
|
|
25
|
+
origin = new URL(orgUrl).origin;
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
for (const v of declared.values()) {
|
|
31
|
+
const rec = v;
|
|
32
|
+
if (rec.mode !== "xaa" || typeof rec.idpIssuer !== "string")
|
|
33
|
+
continue;
|
|
34
|
+
try {
|
|
35
|
+
if (new URL(rec.idpIssuer).origin === origin)
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
16
44
|
/** The display name to show a human, from whichever profile fields Okta actually filled in. DISPLAY
|
|
17
45
|
* ONLY — link-store.ts invariant 4 means it can never become a lookup key, here or downstream. */
|
|
18
46
|
function displayNameOf(user) {
|
|
@@ -39,6 +67,7 @@ export function makeOktaDirectoryProvider(deps) {
|
|
|
39
67
|
const { config } = deps;
|
|
40
68
|
const fetchImpl = deps.fetch ?? fetch;
|
|
41
69
|
const resolveToken = deps.resolveToken ?? ((broker, ref) => getSecretVia(broker, ref));
|
|
70
|
+
const proposeOwnAccount = deps.proposeOwnAccount ?? (() => xaaNamesOrg(config.orgUrl));
|
|
42
71
|
// The operator's OWN Okta origin — the ONLY origin the SSWS token may ever be sent to. Computed
|
|
43
72
|
// once; every request in getAll (the first AND every `Link: rel="next"` hop) is checked against it.
|
|
44
73
|
const orgOrigin = new URL(config.orgUrl).origin;
|
|
@@ -148,6 +177,8 @@ export function makeOktaDirectoryProvider(deps) {
|
|
|
148
177
|
const users = await getAll(`/api/v1/users?limit=${PAGE_LIMIT}`, token);
|
|
149
178
|
const groups = await groupsByUser(token);
|
|
150
179
|
const accounts = await accountsByUser(token);
|
|
180
|
+
// Read once per sync, like the token: a declaration change takes effect at the next tick.
|
|
181
|
+
const ownAccount = proposeOwnAccount();
|
|
151
182
|
const people = [];
|
|
152
183
|
for (const user of users) {
|
|
153
184
|
if (!user.id)
|
|
@@ -157,7 +188,8 @@ export function makeOktaDirectoryProvider(deps) {
|
|
|
157
188
|
externalId: user.id,
|
|
158
189
|
displayName: displayNameOf(user),
|
|
159
190
|
...(email ? { email } : {}),
|
|
160
|
-
|
|
191
|
+
// The Okta user id IS Okta's own answer for its own account, so the grade is "attested".
|
|
192
|
+
accounts: ownAccount ? [...(accounts.get(user.id) ?? []), { platform: OKTA_LINK_PLATFORM, nativeId: user.id, grade: "attested" }] : (accounts.get(user.id) ?? []),
|
|
161
193
|
groups: (groups.get(user.id) ?? []).slice().sort(),
|
|
162
194
|
deprovisioned: DEPROVISIONED_STATUSES.has((user.status ?? "").toUpperCase()),
|
|
163
195
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"okta.js","sourceRoot":"","sources":["../../../agent/directory/okta.ts"],"names":[],"mappings":"AAAA,0GAA0G;AAC1G,4GAA4G;AAE5G,OAAO,
|
|
1
|
+
{"version":3,"file":"okta.js","sourceRoot":"","sources":["../../../agent/directory/okta.ts"],"names":[],"mappings":"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"}
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
export declare const OE_OAUTH_MODE_DISCOVERY = "discovery-dcr-pkce";
|
|
3
3
|
/** The static confidential-client dialect (OPE-1086) — no discovery, no registration, HTTP Basic. */
|
|
4
4
|
export declare const OE_OAUTH_MODE_STATIC = "static-confidential";
|
|
5
|
+
/** The Cross App Access dialect (OPE-2453) — an IdP sign-in once, then an ID-JAG mint per app token. */
|
|
6
|
+
export declare const OE_OAUTH_MODE_XAA = "xaa";
|
|
5
7
|
/** How a static provider encodes its token-request body. Declared, never inferred: Notion's REST
|
|
6
8
|
* token endpoint takes JSON, the RFC 6749 default is form-encoded, and sending the wrong one reads
|
|
7
9
|
* as an authentication failure rather than as a shape error. */
|
|
@@ -50,8 +52,32 @@ export interface OeOauthStaticProviderConfig extends OeOauthProviderConfigBase {
|
|
|
50
52
|
/** How the token-request body is encoded. */
|
|
51
53
|
readonly bodyEncoding: OeOauthBodyEncoding;
|
|
52
54
|
}
|
|
53
|
-
/**
|
|
54
|
-
|
|
55
|
+
/**
|
|
56
|
+
* A Cross App Access provider (OPE-2453). The person signs in to the IdP (`idpIssuer`) once; each mint then trades
|
|
57
|
+
* the person's IdP refresh token for an ID-JAG (RFC 8693, at the IdP) and the ID-JAG for the app's access token
|
|
58
|
+
* (RFC 7523, at `resourceTokenEndpoint`). Two confidential clients, two secret REFS, no literal secret.
|
|
59
|
+
*/
|
|
60
|
+
export interface OeOauthXaaProviderConfig extends OeOauthProviderConfigBase {
|
|
61
|
+
readonly mode: typeof OE_OAUTH_MODE_XAA;
|
|
62
|
+
/** The IdP's OIDC issuer. Its `/.well-known/openid-configuration` names the authorize, token, JWKS and
|
|
63
|
+
* revocation endpoints. HTTPS only. */
|
|
64
|
+
readonly idpIssuer: string;
|
|
65
|
+
readonly idpClientId: string;
|
|
66
|
+
readonly idpClientSecretRef: string;
|
|
67
|
+
readonly idpClientSecretBroker: string;
|
|
68
|
+
/** The app's authorization server issuer — the ID-JAG `audience`. */
|
|
69
|
+
readonly audience: string;
|
|
70
|
+
/** The RFC 8707 resource both legs name, when the app asks for one. */
|
|
71
|
+
readonly resource?: string;
|
|
72
|
+
readonly resourceTokenEndpoint: string;
|
|
73
|
+
readonly resourceClientId: string;
|
|
74
|
+
readonly resourceClientSecretRef: string;
|
|
75
|
+
readonly resourceClientSecretBroker: string;
|
|
76
|
+
}
|
|
77
|
+
/** One provider's OAuth target, in one of the three declared modes. */
|
|
78
|
+
export type OeOauthProviderConfig = OeOauthDiscoveryProviderConfig | OeOauthStaticProviderConfig | OeOauthXaaProviderConfig;
|
|
79
|
+
/** Narrow a provider config to the Cross App Access mode. Reads the DECLARED `mode` only. */
|
|
80
|
+
export declare function isXaa(cfg: OeOauthProviderConfig): cfg is OeOauthXaaProviderConfig;
|
|
55
81
|
/** Narrow a provider config to the static confidential-client mode. Reads the DECLARED `mode` and
|
|
56
82
|
* nothing else — never the presence of a field. */
|
|
57
83
|
export declare function isStaticConfidential(cfg: OeOauthProviderConfig): cfg is OeOauthStaticProviderConfig;
|