@confighub/react-auth 0.5.4 → 0.6.1
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/README.md +17 -9
- package/dist/index.cjs +41 -24
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +20 -9
- package/dist/index.d.ts +20 -9
- package/dist/index.js +41 -25
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -50,7 +50,7 @@ organization's own IdP for Enterprise).
|
|
|
50
50
|
## `useAuth()`
|
|
51
51
|
|
|
52
52
|
```ts
|
|
53
|
-
const { status, user, error, login, logout,
|
|
53
|
+
const { status, user, error, login, logout, reauthenticate, signInWithTicket, getToken } = useAuth();
|
|
54
54
|
```
|
|
55
55
|
|
|
56
56
|
- `login(options?)` — redirects to the IdP. `returnTo` picks the landing path
|
|
@@ -59,20 +59,28 @@ const { status, user, error, login, logout, switchOrganization, reauthenticate,
|
|
|
59
59
|
out, the alias of the last successful login in this browser is used (remembered
|
|
60
60
|
per client in `localStorage`; a short public name, not a credential), so a new
|
|
61
61
|
tab or a login after logout lands in the same organization silently. `null`
|
|
62
|
-
sends no hint on purpose, so Keycloak prompts
|
|
63
|
-
|
|
62
|
+
sends no hint on purpose, so Keycloak prompts. Either is how an app switches
|
|
63
|
+
organization: a fresh login through the IdP, which is where membership is
|
|
64
|
+
decided. `prompt: 'none' | 'login' | 'create'` is passed through; `'create'` opens
|
|
65
|
+
the IdP's registration form, so a sign-up link is `login({ prompt: 'create' })`.
|
|
64
66
|
- `logout(options?)` — forgets the session in this tab. `endSession: true` also ends
|
|
65
67
|
the IdP session (RP-initiated logout with `id_token_hint`), landing on
|
|
66
68
|
`postLogoutRedirectUri` (default: the callback URI), which must be registered
|
|
67
|
-
for the client. Without it the next login rides the SSO cookie silently.
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
that offers the bearer form; a fresh `login()` with no organization hint is the
|
|
71
|
-
portable alternative, since the IdP then prompts for the organization.
|
|
69
|
+
for the client. Without it the next login rides the SSO cookie silently. While the
|
|
70
|
+
page navigates to the IdP, status stays `loading`, so an app that logs in
|
|
71
|
+
automatically on `unauthenticated` does not start a login that races the logout.
|
|
72
72
|
- `reauthenticate()` — the token stopped working: a `prompt=none` round trip for the
|
|
73
73
|
organization the session already had. Status is `loading` meanwhile, not
|
|
74
74
|
`unauthenticated`, so an app that auto-logs-in on `unauthenticated` does not race
|
|
75
|
-
it. If the IdP session is gone too, the page comes back `unauthenticated`.
|
|
75
|
+
it. If the IdP session is gone too, the page comes back `unauthenticated`. On an
|
|
76
|
+
instance with no identity provider there is nothing to ask, and status goes
|
|
77
|
+
straight to `unauthenticated`.
|
|
78
|
+
- `signInWithTicket(ticket)` — redeems a single-use ticket from
|
|
79
|
+
`cub auth browser-session` (`POST /auth/browser-session`) for a session. This is how
|
|
80
|
+
a browser signs in to an instance with no identity provider, which `/api/info`
|
|
81
|
+
shows by advertising no `AuthIssuer`; `login()` rejects with `NoIdentityProvider`
|
|
82
|
+
there. The session has no IdP behind it, so when it expires the user runs the
|
|
83
|
+
command again.
|
|
76
84
|
|
|
77
85
|
## Outside React
|
|
78
86
|
|
package/dist/index.cjs
CHANGED
|
@@ -14,6 +14,12 @@ var OrganizationMissing = class extends Error {
|
|
|
14
14
|
this.name = "OrganizationMissing";
|
|
15
15
|
}
|
|
16
16
|
};
|
|
17
|
+
var NoIdentityProvider = class extends Error {
|
|
18
|
+
constructor() {
|
|
19
|
+
super("this instance has no identity provider; sign in with `cub auth browser-session`");
|
|
20
|
+
this.name = "NoIdentityProvider";
|
|
21
|
+
}
|
|
22
|
+
};
|
|
17
23
|
var PKCE_KEY = "confighub_pkce";
|
|
18
24
|
var LAST_ORG_KEY = "confighub_last_org";
|
|
19
25
|
var lastOrgKey = (clientId) => `${LAST_ORG_KEY}:${clientId}`;
|
|
@@ -71,6 +77,7 @@ async function oidcMetadata(issuer) {
|
|
|
71
77
|
}
|
|
72
78
|
async function startLogin(base, clientId, login = {}, flow = {}, retry = {}) {
|
|
73
79
|
const info = await discover(base);
|
|
80
|
+
if (!info.AuthIssuer && !info.TokenExchangeEndpoint) throw new NoIdentityProvider();
|
|
74
81
|
if (!info.AuthIssuer || !info.TokenExchangeEndpoint) {
|
|
75
82
|
throw new Error(
|
|
76
83
|
"this instance is not configured for token-exchange auth (server needs CONFIGHUB_IDP_ISSUER)"
|
|
@@ -173,18 +180,15 @@ async function exchange(exchangeEndpoint, subjectToken) {
|
|
|
173
180
|
const minted = await exResp.json();
|
|
174
181
|
return { accessToken: minted.access_token, organizationId: minted.organization_id };
|
|
175
182
|
}
|
|
176
|
-
async function
|
|
177
|
-
const r = await fetch(trimSlash(base) + "/auth/
|
|
183
|
+
async function redeemBrowserTicket(base, ticket) {
|
|
184
|
+
const r = await fetch(trimSlash(base) + "/auth/browser-session", {
|
|
178
185
|
method: "POST",
|
|
179
|
-
headers: {
|
|
180
|
-
|
|
181
|
-
"Content-Type": "application/x-www-form-urlencoded"
|
|
182
|
-
},
|
|
183
|
-
body: new URLSearchParams({ organization_id: organizationId })
|
|
186
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
187
|
+
body: new URLSearchParams({ ticket })
|
|
184
188
|
});
|
|
185
|
-
if (!r.ok) throw new Error(`/auth/
|
|
189
|
+
if (!r.ok) throw new Error(`/auth/browser-session ${r.status}: ${await r.text()}`);
|
|
186
190
|
const minted = await r.json();
|
|
187
|
-
return { accessToken: minted.access_token, organizationId: minted.organization_id };
|
|
191
|
+
return { accessToken: minted.access_token, organizationId: minted.organization_id, idpClaims: {} };
|
|
188
192
|
}
|
|
189
193
|
async function endSession(base, clientId, idToken, postLogoutRedirectUri) {
|
|
190
194
|
const info = await discover(base);
|
|
@@ -331,27 +335,29 @@ function ConfigHubAuthProvider({
|
|
|
331
335
|
const logout = react.useCallback(
|
|
332
336
|
async (options) => {
|
|
333
337
|
const idToken = sessionRef.current?.idToken;
|
|
334
|
-
|
|
335
|
-
|
|
338
|
+
if (!options?.endSession) {
|
|
339
|
+
clearSession();
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
sessionRef.current = void 0;
|
|
343
|
+
setAccessToken(void 0);
|
|
344
|
+
persistSession(void 0);
|
|
345
|
+
resetPending();
|
|
346
|
+
setUser(null);
|
|
347
|
+
setStatus("loading");
|
|
348
|
+
try {
|
|
336
349
|
await endSession(
|
|
337
350
|
baseUrl,
|
|
338
351
|
clientId,
|
|
339
352
|
idToken,
|
|
340
353
|
options.postLogoutRedirectUri ?? callbackUri(flow)
|
|
341
354
|
);
|
|
355
|
+
} catch (e) {
|
|
356
|
+
setError(e instanceof Error ? e : new Error(String(e)));
|
|
357
|
+
setStatus("error");
|
|
342
358
|
}
|
|
343
359
|
},
|
|
344
|
-
[baseUrl, clientId, clearSession, flow]
|
|
345
|
-
);
|
|
346
|
-
const switchOrganization2 = react.useCallback(
|
|
347
|
-
async (organizationId) => {
|
|
348
|
-
const current = sessionRef.current;
|
|
349
|
-
if (!current) throw new Error("not authenticated");
|
|
350
|
-
const minted = await switchOrganization(baseUrl, current.accessToken, organizationId);
|
|
351
|
-
rememberOrganization(clientId, void 0);
|
|
352
|
-
applySession({ ...current, ...minted });
|
|
353
|
-
},
|
|
354
|
-
[applySession, baseUrl, clientId]
|
|
360
|
+
[baseUrl, clientId, clearSession, flow, persistSession]
|
|
355
361
|
);
|
|
356
362
|
const getToken = react.useCallback(() => sessionRef.current?.accessToken, []);
|
|
357
363
|
const reauthenticate = react.useCallback(async () => {
|
|
@@ -364,10 +370,20 @@ function ConfigHubAuthProvider({
|
|
|
364
370
|
try {
|
|
365
371
|
await startLogin(baseUrl, clientId, { prompt: "none", organization }, flow);
|
|
366
372
|
} catch (e) {
|
|
373
|
+
if (e instanceof NoIdentityProvider) {
|
|
374
|
+
setStatus("unauthenticated");
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
367
377
|
setError(e instanceof Error ? e : new Error(String(e)));
|
|
368
378
|
setStatus("error");
|
|
369
379
|
}
|
|
370
380
|
}, [baseUrl, clientId, flow, persistSession]);
|
|
381
|
+
const signInWithTicket = react.useCallback(
|
|
382
|
+
async (ticket) => {
|
|
383
|
+
applySession(await redeemBrowserTicket(baseUrl, ticket));
|
|
384
|
+
},
|
|
385
|
+
[applySession, baseUrl]
|
|
386
|
+
);
|
|
371
387
|
const handleUnauthorized2 = react.useCallback(() => {
|
|
372
388
|
if (!sessionRef.current) return;
|
|
373
389
|
if (onUnauthorized === "login") void reauthenticate();
|
|
@@ -388,12 +404,12 @@ function ConfigHubAuthProvider({
|
|
|
388
404
|
error,
|
|
389
405
|
login,
|
|
390
406
|
logout,
|
|
391
|
-
switchOrganization: switchOrganization2,
|
|
392
407
|
reauthenticate,
|
|
408
|
+
signInWithTicket,
|
|
393
409
|
getToken,
|
|
394
410
|
client
|
|
395
411
|
}),
|
|
396
|
-
[status, user, error, login, logout,
|
|
412
|
+
[status, user, error, login, logout, reauthenticate, signInWithTicket, getToken, client]
|
|
397
413
|
);
|
|
398
414
|
return /* @__PURE__ */ jsxRuntime.jsx(ConfigHubAuthContext.Provider, { value, children });
|
|
399
415
|
}
|
|
@@ -410,6 +426,7 @@ function useConfigHub() {
|
|
|
410
426
|
|
|
411
427
|
exports.ConfigHubAuthContext = ConfigHubAuthContext;
|
|
412
428
|
exports.ConfigHubAuthProvider = ConfigHubAuthProvider;
|
|
429
|
+
exports.NoIdentityProvider = NoIdentityProvider;
|
|
413
430
|
exports.OrganizationMissing = OrganizationMissing;
|
|
414
431
|
exports.callbackUri = callbackUri;
|
|
415
432
|
exports.decodeJwtClaims = decodeJwtClaims;
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/core.ts","../src/tokenStore.ts","../src/provider.tsx","../src/hooks.ts"],"names":["createContext","useState","useRef","useMemo","useCallback","useEffect","switchOrganization","handleUnauthorized","createConfigHubClient","jsx","useContext"],"mappings":"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","file":"index.cjs","sourcesContent":["// Copyright (C) ConfigHub, Inc.\n// SPDX-License-Identifier: MIT\n//\n// Framework-neutral browser-auth engine for ConfigHub. Productionized from the\n// reference harness `test/browser-auth/src/confighubAuth.ts` in the ConfigHub\n// monorepo, which is validated end to end against staging and prod.\n//\n// Flow (design: third-party-browser-app-auth.md §6):\n// GET {base}/api/info -> discovery { AuthIssuer, TokenExchangeEndpoint }\n// OIDC discovery on AuthIssuer -> authorize/token/end_session endpoints\n// PKCE authorize + code->token -> IdP token\n// POST {TokenExchangeEndpoint} (8693) -> minted ConfigHub token\n//\n// The minted token then rides `Authorization: Bearer` against `/api`. The flow is\n// edition-agnostic: `AuthIssuer` is whatever discovery names (ConfigHub's bundled\n// Keycloak for Cloud, the org's own IdP for Enterprise), so the same code runs\n// against both. Tokens are held by the caller; only the transient PKCE state is\n// parked in sessionStorage across the authorize redirect.\n\nexport interface Discovery {\n AuthIssuer?: string;\n TokenExchangeEndpoint?: string;\n TokenExchangeAudience?: string;\n}\n\nexport interface MintedSession {\n accessToken: string;\n organizationId: string;\n /** Claims of the validated IdP token (owning-org, audience, organization shape). */\n idpClaims: Record<string, unknown>;\n /**\n * The IdP's ID token, kept only so logout can pass it as `id_token_hint` to the\n * end-session endpoint. Absent for sessions that did not come from an OIDC login.\n */\n idToken?: string;\n}\n\nexport interface LoginOptions {\n /**\n * Where to land after login, as a same-origin path (`/space/x?tab=units`). Carried\n * through the authorize round trip in the PKCE state, never in the redirect URI:\n * OAuth clients register exact redirect URIs, so the URI itself must not vary with\n * the page the user started from. Defaults to the current path and query.\n */\n returnTo?: string;\n /**\n * Which organization to sign in to, as the Keycloak organization alias sent in\n * the `organization:<alias>` scope.\n *\n * - a string: that organization, no prompt;\n * - `undefined` (default): the organization of the last successful login in this\n * browser, remembered per client in `localStorage`, so a new tab or a login after\n * logout lands in the same organization without a prompt; with nothing\n * remembered, Keycloak decides (prompt for a multi-org user, or the org matching\n * the email domain on a fresh authentication);\n * - `null`: no hint on purpose, so Keycloak prompts. This is \"switch organization\".\n */\n organization?: string | null;\n /**\n * `'none'` asks the IdP to re-authenticate without any UI, failing with\n * `login_required` if the SSO session is gone -- the way to refresh an expired\n * ConfigHub token when the user is still signed in at the IdP. `'login'` forces the\n * login form even with a live SSO session.\n */\n prompt?: 'none' | 'login';\n}\n\nexport interface FlowOptions {\n /**\n * Same-origin path the IdP redirects back to, and therefore the redirect URI to\n * register for the client: `{origin}{callbackPath}`. Defaults to `/`.\n */\n callbackPath?: string;\n}\n\ninterface PkceState {\n verifier: string;\n state: string;\n clientId: string;\n tokenEndpoint: string;\n exchangeEndpoint: string;\n redirectUri: string;\n returnTo: string;\n silent: boolean;\n /** The organization alias the authorize request hinted, if any. */\n hintedOrganization?: string;\n /** This login is already the retry after a token without an organization. */\n retriedForOrganization?: boolean;\n}\n\n/**\n * Thrown by `completeLoginFromRedirect` when the IdP token names no organization,\n * which the exchange would refuse. Seen on a fresh brokered (Google) login, where\n * Keycloak's organization step does not run; on the next login the SSO session is\n * alive and it does, so the caller logs in again, once, with no hint. Any\n * remembered alias has already been forgotten.\n */\nexport class OrganizationMissing extends Error {\n constructor(public readonly returnTo: string) {\n super('the identity provider issued a token with no organization');\n this.name = 'OrganizationMissing';\n }\n}\n\nexport interface RetryOptions {\n /** @internal set by the provider on the one retry after OrganizationMissing. */\n retriedForOrganization?: boolean;\n}\n\nconst PKCE_KEY = 'confighub_pkce';\nconst LAST_ORG_KEY = 'confighub_last_org';\n\n// The alias is a short public identifier, not a credential, so localStorage is the\n// right place: it must outlive the tab, which is exactly what the token must not.\nconst lastOrgKey = (clientId: string): string => `${LAST_ORG_KEY}:${clientId}`;\n\n/** The organization alias of the last successful login for this client, if any. */\nexport function rememberedOrganization(clientId: string): string | undefined {\n try {\n return localStorage.getItem(lastOrgKey(clientId)) ?? undefined;\n } catch {\n return undefined;\n }\n}\n\n/** @internal */\nexport function rememberOrganization(clientId: string, alias: string | undefined): void {\n try {\n if (alias) localStorage.setItem(lastOrgKey(clientId), alias);\n else localStorage.removeItem(lastOrgKey(clientId));\n } catch {\n // Storage unavailable: the next login gets no hint.\n }\n}\n\n/**\n * The alias in an IdP token's `organization` claim (`{ \"<alias>\": { id } }`), or\n * undefined when the claim is absent or names more than one organization.\n */\nexport function organizationAliasOf(idpClaims: Record<string, unknown>): string | undefined {\n const org = idpClaims.organization;\n if (!org || typeof org !== 'object') return undefined;\n const aliases = Object.keys(org as Record<string, unknown>);\n return aliases.length === 1 ? aliases[0] : undefined;\n}\n\nconst trimSlash = (s: string): string => s.replace(/\\/+$/, '');\n\n/** The fixed callback URI: the page origin plus the configured callback path. */\nexport const callbackUri = (opts?: FlowOptions): string =>\n window.location.origin + (opts?.callbackPath ?? '/');\n\nconst currentPath = (): string =>\n window.location.pathname + window.location.search + window.location.hash;\n\nconst b64url = (buf: ArrayBuffer): string =>\n btoa(String.fromCharCode(...new Uint8Array(buf)))\n .replace(/\\+/g, '-')\n .replace(/\\//g, '_')\n .replace(/=+$/, '');\n\nconst randomString = (n = 64): string =>\n b64url(crypto.getRandomValues(new Uint8Array(n)).buffer);\n\nasync function sha256(s: string): Promise<string> {\n return b64url(await crypto.subtle.digest('SHA-256', new TextEncoder().encode(s)));\n}\n\n/** Decode a JWT's claims without verifying it. Returns {} for anything malformed. */\nexport function decodeJwtClaims(token: string): Record<string, unknown> {\n const part = token.split('.')[1];\n if (!part) return {};\n try {\n return JSON.parse(atob(part.replace(/-/g, '+').replace(/_/g, '/')));\n } catch {\n return {};\n }\n}\n\n/** Whether a JWT's `exp` is in the past (with a small skew allowance). */\nexport function isExpired(token: string, skewSeconds = 30): boolean {\n const exp = decodeJwtClaims(token).exp;\n if (typeof exp !== 'number') return false;\n return exp * 1000 <= Date.now() + skewSeconds * 1000;\n}\n\nexport async function discover(base: string): Promise<Discovery> {\n const r = await fetch(trimSlash(base) + '/api/info');\n if (!r.ok) throw new Error('/api/info failed: ' + r.status);\n return r.json();\n}\n\ninterface OidcMetadata {\n authorization_endpoint: string;\n token_endpoint: string;\n end_session_endpoint?: string;\n}\n\nasync function oidcMetadata(issuer: string): Promise<OidcMetadata> {\n const r = await fetch(trimSlash(issuer) + '/.well-known/openid-configuration');\n if (!r.ok) throw new Error('OIDC discovery failed: ' + r.status);\n return r.json();\n}\n\n/**\n * Discover, build a PKCE request, and navigate to the IdP authorize endpoint.\n * Returns only by redirecting the page; `completeLoginFromRedirect()` finishes on\n * the way back.\n */\nexport async function startLogin(\n base: string,\n clientId: string,\n login: LoginOptions = {},\n flow: FlowOptions = {},\n retry: RetryOptions = {},\n): Promise<void> {\n const info = await discover(base);\n if (!info.AuthIssuer || !info.TokenExchangeEndpoint) {\n throw new Error(\n 'this instance is not configured for token-exchange auth (server needs CONFIGHUB_IDP_ISSUER)',\n );\n }\n const meta = await oidcMetadata(info.AuthIssuer);\n const verifier = randomString();\n const challenge = await sha256(verifier);\n const state = randomString(16);\n const redirectUri = callbackUri(flow);\n // The \"organization\" scope makes Keycloak emit the org claim the exchange resolves;\n // \"organization:<alias>\" selects one without prompting.\n const alias =\n login.organization === null\n ? undefined\n : (login.organization ?? rememberedOrganization(clientId));\n const orgScope = alias ? `organization:${alias}` : 'organization';\n const pkce: PkceState = {\n verifier,\n state,\n clientId,\n tokenEndpoint: meta.token_endpoint,\n exchangeEndpoint: info.TokenExchangeEndpoint,\n redirectUri,\n returnTo: login.returnTo ?? currentPath(),\n silent: login.prompt === 'none',\n hintedOrganization: alias,\n retriedForOrganization: retry.retriedForOrganization,\n };\n sessionStorage.setItem(PKCE_KEY, JSON.stringify(pkce));\n const params: Record<string, string> = {\n response_type: 'code',\n client_id: clientId,\n redirect_uri: redirectUri,\n scope: `openid email profile ${orgScope}`,\n code_challenge: challenge,\n code_challenge_method: 'S256',\n state,\n };\n if (login.prompt) params.prompt = login.prompt;\n\n const authURL = new URL(meta.authorization_endpoint);\n authURL.search = new URLSearchParams(params).toString();\n window.location.assign(authURL.toString());\n}\n\n// Memoize so React StrictMode's double-mount can't redeem the one-time code twice.\nlet pending: Promise<MintedSession | null> | null = null;\n\n/**\n * If the page is the IdP redirect (`?code=...`), exchange the code for an IdP token\n * and then exchange that for a minted ConfigHub token, and restore the URL the\n * login started from. Returns null on a normal load, when a `prompt=none` attempt\n * came back with `login_required` (the SSO session is gone; the caller should offer\n * an interactive login), and when the redirect carries a code this tab has no PKCE\n * state for (an IdP email flow landed here; the caller should start a login).\n */\nexport function completeLoginFromRedirect(): Promise<MintedSession | null> {\n if (!pending) pending = doCompleteLogin();\n return pending;\n}\n\nconst SILENT_FAILURES = new Set(['login_required', 'interaction_required', 'consent_required']);\n\nasync function doCompleteLogin(): Promise<MintedSession | null> {\n const params = new URLSearchParams(window.location.search);\n const code = params.get('code');\n const error = params.get('error');\n if (!code && !error) return null;\n\n const savedRaw = sessionStorage.getItem(PKCE_KEY);\n sessionStorage.removeItem(PKCE_KEY);\n const saved: PkceState | null = savedRaw ? JSON.parse(savedRaw) : null;\n\n // Put the URL back to where the user started before anything else can fail, so\n // neither the code nor an error string lingers in the address bar or history.\n history.replaceState({}, '', saved?.returnTo ?? callbackUri());\n\n if (error) {\n if (saved?.silent && SILENT_FAILURES.has(error)) return null;\n throw new Error(`IdP returned error: ${error} ${params.get('error_description') ?? ''}`);\n }\n // A code with no PKCE state means this tab did not start the login: the IdP's\n // email flows (password reset, verify email) finish the authentication in the tab\n // the mail link opened and redirect it here, while the verifier sits in the tab\n // that began. The code is unusable without it, so drop it and report a normal\n // load; the caller starts a login, which the now-live IdP session completes\n // without a prompt.\n if (!saved) return null;\n if (params.get('state') !== saved.state) throw new Error('state mismatch; aborting');\n\n // Exchange the authorization code for an IdP token (PKCE, public client).\n const tokenResp = await fetch(saved.tokenEndpoint, {\n method: 'POST',\n headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\n body: new URLSearchParams({\n grant_type: 'authorization_code',\n code: code!,\n redirect_uri: saved.redirectUri,\n client_id: saved.clientId,\n code_verifier: saved.verifier,\n }),\n });\n if (!tokenResp.ok) {\n throw new Error(`IdP token endpoint ${tokenResp.status}: ${await tokenResp.text()}`);\n }\n const idpToken = await tokenResp.json();\n\n // RFC 8693 token exchange against ConfigHub -> minted ConfigHub token.\n const idpClaims = decodeJwtClaims(idpToken.access_token);\n if (!organizationAliasOf(idpClaims) && !saved.retriedForOrganization) {\n // The exchange would refuse this token. Rather than surface that, log in\n // once more: with the SSO session now alive the IdP runs its organization\n // step. A hint that was sent evidently did not help, so forget it.\n if (saved.hintedOrganization) rememberOrganization(saved.clientId, undefined);\n throw new OrganizationMissing(saved.returnTo);\n }\n const minted = await exchange(saved.exchangeEndpoint, idpToken.access_token);\n rememberOrganization(saved.clientId, organizationAliasOf(idpClaims));\n return {\n ...minted,\n idpClaims,\n idToken: typeof idpToken.id_token === 'string' ? idpToken.id_token : undefined,\n };\n}\n\nasync function exchange(\n exchangeEndpoint: string,\n subjectToken: string,\n): Promise<Pick<MintedSession, 'accessToken' | 'organizationId'>> {\n const exResp = await fetch(exchangeEndpoint, {\n method: 'POST',\n headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\n body: new URLSearchParams({\n grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange',\n subject_token: subjectToken,\n subject_token_type: 'urn:ietf:params:oauth:token-type:access_token',\n }),\n });\n if (!exResp.ok) throw new Error(`/auth/exchange ${exResp.status}: ${await exResp.text()}`);\n const minted = await exResp.json();\n return { accessToken: minted.access_token, organizationId: minted.organization_id };\n}\n\n/**\n * Trade the current minted token for one scoped to another organization the user\n * belongs to (`POST {base}/auth/switch-organization`, bearer-authenticated). The\n * IdP session is untouched; only the ConfigHub token changes.\n */\nexport async function switchOrganization(\n base: string,\n accessToken: string,\n organizationId: string,\n): Promise<Pick<MintedSession, 'accessToken' | 'organizationId'>> {\n const r = await fetch(trimSlash(base) + '/auth/switch-organization', {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${accessToken}`,\n 'Content-Type': 'application/x-www-form-urlencoded',\n },\n body: new URLSearchParams({ organization_id: organizationId }),\n });\n if (!r.ok) throw new Error(`/auth/switch-organization ${r.status}: ${await r.text()}`);\n const minted = await r.json();\n return { accessToken: minted.access_token, organizationId: minted.organization_id };\n}\n\n/**\n * End the IdP session (RP-initiated logout) and land on `postLogoutRedirectUri`,\n * which must be registered for the client. Returns only by redirecting. If the\n * issuer publishes no end-session endpoint, navigates to the redirect URI directly.\n */\nexport async function endSession(\n base: string,\n clientId: string,\n idToken: string | undefined,\n postLogoutRedirectUri: string,\n): Promise<void> {\n const info = await discover(base);\n const meta = info.AuthIssuer ? await oidcMetadata(info.AuthIssuer) : undefined;\n if (!meta?.end_session_endpoint) {\n window.location.assign(postLogoutRedirectUri);\n return;\n }\n const params: Record<string, string> = {\n client_id: clientId,\n post_logout_redirect_uri: postLogoutRedirectUri,\n };\n if (idToken) params.id_token_hint = idToken;\n const url = new URL(meta.end_session_endpoint);\n url.search = new URLSearchParams(params).toString();\n window.location.assign(url.toString());\n}\n\n/** Discard the in-progress login memo (used on logout so a later login re-runs). */\nexport function resetPending(): void {\n pending = null;\n}\n","// Copyright (C) ConfigHub, Inc.\n// SPDX-License-Identifier: MIT\n\n// A module-level holder for the current minted token, so non-React consumers can read\n// it. RTK Query's `prepareHeaders` (in @confighub/rtk-query) is not a hook and cannot\n// read React context, so it calls getAccessToken() instead. The provider keeps this in\n// sync with its React state, and registers its 401 handling here for the same reason.\nimport { isExpired } from './core';\n\nlet currentToken: string | undefined;\nlet unauthorizedHandler: (() => void) | undefined;\n// Set while a recovery is under way, so a burst of failing requests starts one.\nlet recovering = false;\n\n/** @internal — called by the provider; not part of the public surface. */\nexport function setAccessToken(token: string | undefined): void {\n currentToken = token;\n if (token !== undefined) recovering = false;\n}\n\n/** @internal — the mounted provider's reaction to a rejected token. */\nexport function setUnauthorizedHandler(handler: (() => void) | undefined): void {\n unauthorizedHandler = handler;\n recovering = false;\n}\n\n/**\n * The current minted ConfigHub token, or undefined when unauthenticated. Pass this as\n * the `getToken` for `@confighub/rtk-query`'s `configureConfigHub`, or read it anywhere\n * you need the token outside React.\n *\n * A token past its `exp` is never handed out: the server would reject it. Instead the\n * provider's recovery starts (a silent re-authentication, or a logout, per its\n * `onUnauthorized`) and the request goes without a token; the page is about to\n * navigate to the IdP and back to where it was.\n */\nexport function getAccessToken(): string | undefined {\n if (currentToken !== undefined && isExpired(currentToken)) {\n handleUnauthorized();\n return undefined;\n }\n return currentToken;\n}\n\n/**\n * Tell the mounted provider the API rejected the token (401). Pass this as the\n * `onUnauthorized` for `@confighub/rtk-query`'s `configureConfigHub` to recover from a\n * token rejected before its expiry, e.g. after a server key rotation; expiry itself is\n * handled by `getAccessToken`. A no-op with no provider mounted, and while a recovery\n * is already under way.\n */\nexport function handleUnauthorized(): void {\n if (recovering || !unauthorizedHandler) return;\n recovering = true;\n unauthorizedHandler();\n}\n","// Copyright (C) ConfigHub, Inc.\n// SPDX-License-Identifier: MIT\n\nimport { createConfigHubClient, type ConfigHubClient } from '@confighub/api';\nimport {\n createContext,\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n type ReactNode,\n} from 'react';\nimport {\n OrganizationMissing,\n callbackUri,\n completeLoginFromRedirect,\n endSession,\n isExpired,\n organizationAliasOf,\n rememberOrganization,\n resetPending,\n startLogin,\n switchOrganization as switchOrganizationCore,\n type LoginOptions,\n type MintedSession,\n} from './core';\nimport { setAccessToken, setUnauthorizedHandler } from './tokenStore';\n\nexport type AuthStatus = 'loading' | 'authenticated' | 'unauthenticated' | 'error';\n\nexport interface ConfigHubUser {\n organizationId: string;\n idpClaims: Record<string, unknown>;\n}\n\nexport interface LogoutOptions {\n /**\n * Also end the IdP session (RP-initiated logout), so the next login asks for\n * credentials instead of riding the SSO cookie. Redirects the page; the landing\n * URI must be registered for the client. Default: false, which only forgets the\n * token in this tab.\n */\n endSession?: boolean;\n /** Where to land after IdP logout. Defaults to the callback URI. */\n postLogoutRedirectUri?: string;\n}\n\nexport interface ConfigHubAuthContextValue {\n status: AuthStatus;\n user: ConfigHubUser | null;\n error: Error | null;\n /** Begin login: redirects the page to the IdP. */\n login: (options?: LoginOptions) => Promise<void>;\n /** Forget the session in this tab and, optionally, end the IdP session too. */\n logout: (options?: LogoutOptions) => Promise<void>;\n /**\n * Re-mint the ConfigHub token for another organization the user belongs to. The\n * IdP session is untouched. Rejects with the server's error if the user is not a\n * member; the current session stays as it was.\n */\n switchOrganization: (organizationId: string) => Promise<void>;\n /**\n * The ConfigHub token stopped working (a 401). Try to get a new one without any\n * UI: a `prompt=none` round trip through the IdP, for the organization the session\n * already had. Status goes to `loading` meanwhile, not `unauthenticated`, so an\n * app that auto-logs-in on `unauthenticated` does not race this with an\n * interactive login. If the IdP session is gone too, the page comes back\n * `unauthenticated`. Redirects the page.\n */\n reauthenticate: () => Promise<void>;\n /** Current bearer token, or undefined when unauthenticated. */\n getToken: () => string | undefined;\n /** A typed API client pre-wired with the current token. Stable across renders. */\n client: ConfigHubClient;\n}\n\nexport const ConfigHubAuthContext = createContext<ConfigHubAuthContextValue | null>(null);\n\nexport interface ConfigHubAuthProviderProps {\n /** Absolute base URL of the ConfigHub instance, e.g. `https://hub.confighub.com`. */\n baseUrl: string;\n /** This app's registered OAuth `client_id` (from `cub oauthclient create`). */\n clientId: string;\n /**\n * Same-origin path the IdP redirects back to; `{origin}{callbackPath}` is the\n * redirect URI to register. Defaults to `/`. Fixed on purpose: the page a user\n * starts login from travels in the PKCE state, not in the redirect URI.\n */\n callbackPath?: string;\n /**\n * `'session'` (default) keeps the minted token in `sessionStorage` so a reload or an\n * in-tab navigation does not round-trip through the IdP. Tab-scoped and gone when the\n * tab closes. `'none'`: memory only, every page load starts unauthenticated.\n */\n persist?: 'none' | 'session';\n /**\n * What a 401 from the API means. `'login'` (default): the token is stale, try a\n * silent re-authentication (`prompt=none`) and fall back to unauthenticated if the\n * IdP session is gone too. `'logout'`: just drop the session.\n */\n onUnauthorized?: 'login' | 'logout';\n children: ReactNode;\n}\n\nconst SESSION_KEY = 'confighub_session';\n\nfunction readPersisted(): MintedSession | null {\n try {\n const raw = sessionStorage.getItem(SESSION_KEY);\n if (!raw) return null;\n const session: MintedSession = JSON.parse(raw);\n if (!session.accessToken || isExpired(session.accessToken)) {\n sessionStorage.removeItem(SESSION_KEY);\n return null;\n }\n return session;\n } catch {\n return null;\n }\n}\n\n/**\n * Runs the browser-direct auth flow and manages the token lifecycle. On mount it\n * completes a redirect if the page is the IdP callback, restores a persisted\n * session if there is one, and otherwise starts unauthenticated until `login()`\n * is called.\n */\nexport function ConfigHubAuthProvider({\n baseUrl,\n clientId,\n callbackPath,\n persist = 'session',\n onUnauthorized = 'login',\n children,\n}: ConfigHubAuthProviderProps): JSX.Element {\n const [status, setStatus] = useState<AuthStatus>('loading');\n const [user, setUser] = useState<ConfigHubUser | null>(null);\n const [error, setError] = useState<Error | null>(null);\n\n // The session lives in a ref so getToken() reads the latest value synchronously\n // without re-creating the API client on every render.\n const sessionRef = useRef<MintedSession | undefined>(undefined);\n const flow = useMemo(() => ({ callbackPath }), [callbackPath]);\n\n const persistSession = useCallback(\n (session: MintedSession | undefined) => {\n if (persist !== 'session') return;\n try {\n if (session) sessionStorage.setItem(SESSION_KEY, JSON.stringify(session));\n else sessionStorage.removeItem(SESSION_KEY);\n } catch {\n // Storage unavailable (private mode, quota): memory-only for this tab.\n }\n },\n [persist],\n );\n\n const applySession = useCallback(\n (session: MintedSession) => {\n sessionRef.current = session;\n setAccessToken(session.accessToken); // keep the non-React accessor in sync (rtk-query)\n persistSession(session);\n setUser({ organizationId: session.organizationId, idpClaims: session.idpClaims });\n setError(null);\n setStatus('authenticated');\n },\n [persistSession],\n );\n\n const clearSession = useCallback(() => {\n sessionRef.current = undefined;\n setAccessToken(undefined);\n persistSession(undefined);\n resetPending();\n setUser(null);\n setStatus('unauthenticated');\n }, [persistSession]);\n\n useEffect(() => {\n let cancelled = false;\n completeLoginFromRedirect()\n .then((session) => {\n if (cancelled) return;\n if (session) return applySession(session);\n const persisted = persist === 'session' ? readPersisted() : null;\n if (persisted) return applySession(persisted);\n setStatus('unauthenticated');\n })\n .catch((e: unknown) => {\n if (cancelled) return;\n if (e instanceof OrganizationMissing) {\n // Once, with no hint, flagged so a second miss surfaces as an error.\n void startLogin(\n baseUrl,\n clientId,\n { returnTo: e.returnTo, organization: null },\n flow,\n { retriedForOrganization: true },\n );\n return;\n }\n setError(e instanceof Error ? e : new Error(String(e)));\n setStatus('error');\n });\n return () => {\n cancelled = true;\n };\n }, [applySession, persist, baseUrl, clientId, flow]);\n\n const login = useCallback(\n async (options?: LoginOptions) => {\n setError(null);\n try {\n await startLogin(baseUrl, clientId, options, flow);\n } catch (e: unknown) {\n setError(e instanceof Error ? e : new Error(String(e)));\n setStatus('error');\n }\n },\n [baseUrl, clientId, flow],\n );\n\n const logout = useCallback(\n async (options?: LogoutOptions) => {\n const idToken = sessionRef.current?.idToken;\n clearSession();\n if (options?.endSession) {\n await endSession(\n baseUrl,\n clientId,\n idToken,\n options.postLogoutRedirectUri ?? callbackUri(flow),\n );\n }\n },\n [baseUrl, clientId, clearSession, flow],\n );\n\n const switchOrganization = useCallback(\n async (organizationId: string) => {\n const current = sessionRef.current;\n if (!current) throw new Error('not authenticated');\n const minted = await switchOrganizationCore(baseUrl, current.accessToken, organizationId);\n // The session's IdP claims still name the previous org, so its alias must not\n // be remembered as the default for the next login.\n rememberOrganization(clientId, undefined);\n applySession({ ...current, ...minted });\n },\n [applySession, baseUrl, clientId],\n );\n\n const getToken = useCallback(() => sessionRef.current?.accessToken, []);\n\n const reauthenticate = useCallback(async () => {\n const organization = organizationAliasOf(sessionRef.current?.idpClaims ?? {});\n // Forget the token but stay 'loading': the page is about to navigate away,\n // and 'unauthenticated' would invite an interactive login in the meantime.\n sessionRef.current = undefined;\n setAccessToken(undefined);\n persistSession(undefined);\n resetPending();\n setStatus('loading');\n try {\n await startLogin(baseUrl, clientId, { prompt: 'none', organization }, flow);\n } catch (e: unknown) {\n setError(e instanceof Error ? e : new Error(String(e)));\n setStatus('error');\n }\n }, [baseUrl, clientId, flow, persistSession]);\n\n // A 401 means the minted token no longer works. Silent re-auth keeps the user's\n // place if the IdP session is still alive; otherwise the page comes back\n // unauthenticated and the app offers a real login.\n const handleUnauthorized = useCallback(() => {\n if (!sessionRef.current) return;\n if (onUnauthorized === 'login') void reauthenticate();\n else clearSession();\n }, [clearSession, reauthenticate, onUnauthorized]);\n\n // Non-React callers (getAccessToken on an expired token, handleUnauthorized from a\n // data client's 401) reach this provider's handling through the token store.\n useEffect(() => {\n setUnauthorizedHandler(handleUnauthorized);\n return () => setUnauthorizedHandler(undefined);\n }, [handleUnauthorized]);\n\n // One client for the provider's lifetime. getToken reads the session ref.\n const client = useMemo(\n () => createConfigHubClient({ baseUrl, getToken, onUnauthorized: handleUnauthorized }),\n [baseUrl, getToken, handleUnauthorized],\n );\n\n const value = useMemo<ConfigHubAuthContextValue>(\n () => ({\n status,\n user,\n error,\n login,\n logout,\n switchOrganization,\n reauthenticate,\n getToken,\n client,\n }),\n [status, user, error, login, logout, switchOrganization, reauthenticate, getToken, client],\n );\n\n return (\n <ConfigHubAuthContext.Provider value={value}>{children}</ConfigHubAuthContext.Provider>\n );\n}\n","// Copyright (C) ConfigHub, Inc.\n// SPDX-License-Identifier: MIT\n\nimport type { ConfigHubClient } from '@confighub/api';\nimport { useContext } from 'react';\nimport { ConfigHubAuthContext, type ConfigHubAuthContextValue } from './provider';\n\n/**\n * Access the ConfigHub auth state and actions. Must be called under a\n * `<ConfigHubAuthProvider>`.\n *\n * ```ts\n * const { status, user, login, logout } = useAuth();\n * ```\n */\nexport function useAuth(): ConfigHubAuthContextValue {\n const ctx = useContext(ConfigHubAuthContext);\n if (!ctx) {\n throw new Error('useAuth must be used within a <ConfigHubAuthProvider>');\n }\n return ctx;\n}\n\n/**\n * The typed ConfigHub API client, pre-wired with the current token. This is the\n * seam between `@confighub/react-auth` and `@confighub/api`: you never pass a\n * token by hand.\n *\n * ```ts\n * const api = useConfigHub();\n * const { data } = await api.GET('/space/{space_id}/unit', { params: { path: { space_id } } });\n * ```\n */\nexport function useConfigHub(): ConfigHubClient {\n return useAuth().client;\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/core.ts","../src/tokenStore.ts","../src/provider.tsx","../src/hooks.ts"],"names":["createContext","useState","useRef","useMemo","useCallback","useEffect","handleUnauthorized","createConfigHubClient","jsx","useContext"],"mappings":"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","file":"index.cjs","sourcesContent":["// Copyright (C) ConfigHub, Inc.\n// SPDX-License-Identifier: MIT\n//\n// Framework-neutral browser-auth engine for ConfigHub. Productionized from the\n// reference harness `test/browser-auth/src/confighubAuth.ts` in the ConfigHub\n// monorepo, which is validated end to end against staging and prod.\n//\n// Flow (design: third-party-browser-app-auth.md §6):\n// GET {base}/api/info -> discovery { AuthIssuer, TokenExchangeEndpoint }\n// OIDC discovery on AuthIssuer -> authorize/token/end_session endpoints\n// PKCE authorize + code->token -> IdP token\n// POST {TokenExchangeEndpoint} (8693) -> minted ConfigHub token\n//\n// The minted token then rides `Authorization: Bearer` against `/api`. The flow is\n// edition-agnostic: `AuthIssuer` is whatever discovery names (ConfigHub's bundled\n// Keycloak for Cloud, the org's own IdP for Enterprise), so the same code runs\n// against both. Tokens are held by the caller; only the transient PKCE state is\n// parked in sessionStorage across the authorize redirect.\n\nexport interface Discovery {\n AuthIssuer?: string;\n TokenExchangeEndpoint?: string;\n TokenExchangeAudience?: string;\n}\n\nexport interface MintedSession {\n accessToken: string;\n organizationId: string;\n /** Claims of the validated IdP token (owning-org, audience, organization shape). */\n idpClaims: Record<string, unknown>;\n /**\n * The IdP's ID token, kept only so logout can pass it as `id_token_hint` to the\n * end-session endpoint. Absent for sessions that did not come from an OIDC login.\n */\n idToken?: string;\n}\n\nexport interface LoginOptions {\n /**\n * Where to land after login, as a same-origin path (`/space/x?tab=units`). Carried\n * through the authorize round trip in the PKCE state, never in the redirect URI:\n * OAuth clients register exact redirect URIs, so the URI itself must not vary with\n * the page the user started from. Defaults to the current path and query.\n */\n returnTo?: string;\n /**\n * Which organization to sign in to, as the Keycloak organization alias sent in\n * the `organization:<alias>` scope.\n *\n * - a string: that organization, no prompt;\n * - `undefined` (default): the organization of the last successful login in this\n * browser, remembered per client in `localStorage`, so a new tab or a login after\n * logout lands in the same organization without a prompt; with nothing\n * remembered, Keycloak decides (prompt for a multi-org user, or the org matching\n * the email domain on a fresh authentication);\n * - `null`: no hint on purpose, so Keycloak prompts. This is \"switch organization\".\n */\n organization?: string | null;\n /**\n * `'none'` asks the IdP to re-authenticate without any UI, failing with\n * `login_required` if the SSO session is gone -- the way to refresh an expired\n * ConfigHub token when the user is still signed in at the IdP. `'login'` forces the\n * login form even with a live SSO session. `'create'` opens the IdP's registration\n * form instead of its login form (OpenID Connect Prompt Create; Keycloak honours it),\n * so a \"sign up\" link can be an ordinary PKCE login that lands the new user in the\n * app signed in.\n */\n prompt?: 'none' | 'login' | 'create';\n}\n\nexport interface FlowOptions {\n /**\n * Same-origin path the IdP redirects back to, and therefore the redirect URI to\n * register for the client: `{origin}{callbackPath}`. Defaults to `/`.\n */\n callbackPath?: string;\n}\n\ninterface PkceState {\n verifier: string;\n state: string;\n clientId: string;\n tokenEndpoint: string;\n exchangeEndpoint: string;\n redirectUri: string;\n returnTo: string;\n silent: boolean;\n /** The organization alias the authorize request hinted, if any. */\n hintedOrganization?: string;\n /** This login is already the retry after a token without an organization. */\n retriedForOrganization?: boolean;\n}\n\n/**\n * Thrown by `completeLoginFromRedirect` when the IdP token names no organization,\n * which the exchange would refuse. Seen on a fresh brokered (Google) login, where\n * Keycloak's organization step does not run; on the next login the SSO session is\n * alive and it does, so the caller logs in again, once, with no hint. Any\n * remembered alias has already been forgotten.\n */\nexport class OrganizationMissing extends Error {\n constructor(public readonly returnTo: string) {\n super('the identity provider issued a token with no organization');\n this.name = 'OrganizationMissing';\n }\n}\n\n/**\n * Thrown by `startLogin` when the instance advertises no identity provider. Such an\n * instance signs a browser in with a ticket from `cub auth browser-session` instead\n * (see `redeemBrowserTicket`), so there is no login to start.\n */\nexport class NoIdentityProvider extends Error {\n constructor() {\n super('this instance has no identity provider; sign in with `cub auth browser-session`');\n this.name = 'NoIdentityProvider';\n }\n}\n\nexport interface RetryOptions {\n /** @internal set by the provider on the one retry after OrganizationMissing. */\n retriedForOrganization?: boolean;\n}\n\nconst PKCE_KEY = 'confighub_pkce';\nconst LAST_ORG_KEY = 'confighub_last_org';\n\n// The alias is a short public identifier, not a credential, so localStorage is the\n// right place: it must outlive the tab, which is exactly what the token must not.\nconst lastOrgKey = (clientId: string): string => `${LAST_ORG_KEY}:${clientId}`;\n\n/** The organization alias of the last successful login for this client, if any. */\nexport function rememberedOrganization(clientId: string): string | undefined {\n try {\n return localStorage.getItem(lastOrgKey(clientId)) ?? undefined;\n } catch {\n return undefined;\n }\n}\n\n/** @internal */\nexport function rememberOrganization(clientId: string, alias: string | undefined): void {\n try {\n if (alias) localStorage.setItem(lastOrgKey(clientId), alias);\n else localStorage.removeItem(lastOrgKey(clientId));\n } catch {\n // Storage unavailable: the next login gets no hint.\n }\n}\n\n/**\n * The alias in an IdP token's `organization` claim (`{ \"<alias>\": { id } }`), or\n * undefined when the claim is absent or names more than one organization.\n */\nexport function organizationAliasOf(idpClaims: Record<string, unknown>): string | undefined {\n const org = idpClaims.organization;\n if (!org || typeof org !== 'object') return undefined;\n const aliases = Object.keys(org as Record<string, unknown>);\n return aliases.length === 1 ? aliases[0] : undefined;\n}\n\nconst trimSlash = (s: string): string => s.replace(/\\/+$/, '');\n\n/** The fixed callback URI: the page origin plus the configured callback path. */\nexport const callbackUri = (opts?: FlowOptions): string =>\n window.location.origin + (opts?.callbackPath ?? '/');\n\nconst currentPath = (): string =>\n window.location.pathname + window.location.search + window.location.hash;\n\nconst b64url = (buf: ArrayBuffer): string =>\n btoa(String.fromCharCode(...new Uint8Array(buf)))\n .replace(/\\+/g, '-')\n .replace(/\\//g, '_')\n .replace(/=+$/, '');\n\nconst randomString = (n = 64): string =>\n b64url(crypto.getRandomValues(new Uint8Array(n)).buffer);\n\nasync function sha256(s: string): Promise<string> {\n return b64url(await crypto.subtle.digest('SHA-256', new TextEncoder().encode(s)));\n}\n\n/** Decode a JWT's claims without verifying it. Returns {} for anything malformed. */\nexport function decodeJwtClaims(token: string): Record<string, unknown> {\n const part = token.split('.')[1];\n if (!part) return {};\n try {\n return JSON.parse(atob(part.replace(/-/g, '+').replace(/_/g, '/')));\n } catch {\n return {};\n }\n}\n\n/** Whether a JWT's `exp` is in the past (with a small skew allowance). */\nexport function isExpired(token: string, skewSeconds = 30): boolean {\n const exp = decodeJwtClaims(token).exp;\n if (typeof exp !== 'number') return false;\n return exp * 1000 <= Date.now() + skewSeconds * 1000;\n}\n\nexport async function discover(base: string): Promise<Discovery> {\n const r = await fetch(trimSlash(base) + '/api/info');\n if (!r.ok) throw new Error('/api/info failed: ' + r.status);\n return r.json();\n}\n\ninterface OidcMetadata {\n authorization_endpoint: string;\n token_endpoint: string;\n end_session_endpoint?: string;\n}\n\nasync function oidcMetadata(issuer: string): Promise<OidcMetadata> {\n const r = await fetch(trimSlash(issuer) + '/.well-known/openid-configuration');\n if (!r.ok) throw new Error('OIDC discovery failed: ' + r.status);\n return r.json();\n}\n\n/**\n * Discover, build a PKCE request, and navigate to the IdP authorize endpoint.\n * Returns only by redirecting the page; `completeLoginFromRedirect()` finishes on\n * the way back.\n */\nexport async function startLogin(\n base: string,\n clientId: string,\n login: LoginOptions = {},\n flow: FlowOptions = {},\n retry: RetryOptions = {},\n): Promise<void> {\n const info = await discover(base);\n if (!info.AuthIssuer && !info.TokenExchangeEndpoint) throw new NoIdentityProvider();\n if (!info.AuthIssuer || !info.TokenExchangeEndpoint) {\n throw new Error(\n 'this instance is not configured for token-exchange auth (server needs CONFIGHUB_IDP_ISSUER)',\n );\n }\n const meta = await oidcMetadata(info.AuthIssuer);\n const verifier = randomString();\n const challenge = await sha256(verifier);\n const state = randomString(16);\n const redirectUri = callbackUri(flow);\n // The \"organization\" scope makes Keycloak emit the org claim the exchange resolves;\n // \"organization:<alias>\" selects one without prompting.\n const alias =\n login.organization === null\n ? undefined\n : (login.organization ?? rememberedOrganization(clientId));\n const orgScope = alias ? `organization:${alias}` : 'organization';\n const pkce: PkceState = {\n verifier,\n state,\n clientId,\n tokenEndpoint: meta.token_endpoint,\n exchangeEndpoint: info.TokenExchangeEndpoint,\n redirectUri,\n returnTo: login.returnTo ?? currentPath(),\n silent: login.prompt === 'none',\n hintedOrganization: alias,\n retriedForOrganization: retry.retriedForOrganization,\n };\n sessionStorage.setItem(PKCE_KEY, JSON.stringify(pkce));\n const params: Record<string, string> = {\n response_type: 'code',\n client_id: clientId,\n redirect_uri: redirectUri,\n scope: `openid email profile ${orgScope}`,\n code_challenge: challenge,\n code_challenge_method: 'S256',\n state,\n };\n if (login.prompt) params.prompt = login.prompt;\n\n const authURL = new URL(meta.authorization_endpoint);\n authURL.search = new URLSearchParams(params).toString();\n window.location.assign(authURL.toString());\n}\n\n// Memoize so React StrictMode's double-mount can't redeem the one-time code twice.\nlet pending: Promise<MintedSession | null> | null = null;\n\n/**\n * If the page is the IdP redirect (`?code=...`), exchange the code for an IdP token\n * and then exchange that for a minted ConfigHub token, and restore the URL the\n * login started from. Returns null on a normal load, when a `prompt=none` attempt\n * came back with `login_required` (the SSO session is gone; the caller should offer\n * an interactive login), and when the redirect carries a code this tab has no PKCE\n * state for (an IdP email flow landed here; the caller should start a login).\n */\nexport function completeLoginFromRedirect(): Promise<MintedSession | null> {\n if (!pending) pending = doCompleteLogin();\n return pending;\n}\n\nconst SILENT_FAILURES = new Set(['login_required', 'interaction_required', 'consent_required']);\n\nasync function doCompleteLogin(): Promise<MintedSession | null> {\n const params = new URLSearchParams(window.location.search);\n const code = params.get('code');\n const error = params.get('error');\n if (!code && !error) return null;\n\n const savedRaw = sessionStorage.getItem(PKCE_KEY);\n sessionStorage.removeItem(PKCE_KEY);\n const saved: PkceState | null = savedRaw ? JSON.parse(savedRaw) : null;\n\n // Put the URL back to where the user started before anything else can fail, so\n // neither the code nor an error string lingers in the address bar or history.\n history.replaceState({}, '', saved?.returnTo ?? callbackUri());\n\n if (error) {\n if (saved?.silent && SILENT_FAILURES.has(error)) return null;\n throw new Error(`IdP returned error: ${error} ${params.get('error_description') ?? ''}`);\n }\n // A code with no PKCE state means this tab did not start the login: the IdP's\n // email flows (password reset, verify email) finish the authentication in the tab\n // the mail link opened and redirect it here, while the verifier sits in the tab\n // that began. The code is unusable without it, so drop it and report a normal\n // load; the caller starts a login, which the now-live IdP session completes\n // without a prompt.\n if (!saved) return null;\n if (params.get('state') !== saved.state) throw new Error('state mismatch; aborting');\n\n // Exchange the authorization code for an IdP token (PKCE, public client).\n const tokenResp = await fetch(saved.tokenEndpoint, {\n method: 'POST',\n headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\n body: new URLSearchParams({\n grant_type: 'authorization_code',\n code: code!,\n redirect_uri: saved.redirectUri,\n client_id: saved.clientId,\n code_verifier: saved.verifier,\n }),\n });\n if (!tokenResp.ok) {\n throw new Error(`IdP token endpoint ${tokenResp.status}: ${await tokenResp.text()}`);\n }\n const idpToken = await tokenResp.json();\n\n // RFC 8693 token exchange against ConfigHub -> minted ConfigHub token.\n const idpClaims = decodeJwtClaims(idpToken.access_token);\n if (!organizationAliasOf(idpClaims) && !saved.retriedForOrganization) {\n // The exchange would refuse this token. Rather than surface that, log in\n // once more: with the SSO session now alive the IdP runs its organization\n // step. A hint that was sent evidently did not help, so forget it.\n if (saved.hintedOrganization) rememberOrganization(saved.clientId, undefined);\n throw new OrganizationMissing(saved.returnTo);\n }\n const minted = await exchange(saved.exchangeEndpoint, idpToken.access_token);\n rememberOrganization(saved.clientId, organizationAliasOf(idpClaims));\n return {\n ...minted,\n idpClaims,\n idToken: typeof idpToken.id_token === 'string' ? idpToken.id_token : undefined,\n };\n}\n\nasync function exchange(\n exchangeEndpoint: string,\n subjectToken: string,\n): Promise<Pick<MintedSession, 'accessToken' | 'organizationId'>> {\n const exResp = await fetch(exchangeEndpoint, {\n method: 'POST',\n headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\n body: new URLSearchParams({\n grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange',\n subject_token: subjectToken,\n subject_token_type: 'urn:ietf:params:oauth:token-type:access_token',\n }),\n });\n if (!exResp.ok) throw new Error(`/auth/exchange ${exResp.status}: ${await exResp.text()}`);\n const minted = await exResp.json();\n return { accessToken: minted.access_token, organizationId: minted.organization_id };\n}\n\n/**\n * Redeem a single-use ticket from `cub auth browser-session` for a minted ConfigHub\n * token (`POST {base}/auth/browser-session`). This is how a browser signs in to an\n * instance with no identity provider, and how a local identity (a break-glass\n * administrator) signs in to one that has a provider. The session has no IdP behind\n * it: when the token expires, the user runs the command again.\n */\nexport async function redeemBrowserTicket(base: string, ticket: string): Promise<MintedSession> {\n const r = await fetch(trimSlash(base) + '/auth/browser-session', {\n method: 'POST',\n headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\n body: new URLSearchParams({ ticket }),\n });\n if (!r.ok) throw new Error(`/auth/browser-session ${r.status}: ${await r.text()}`);\n const minted = await r.json();\n return { accessToken: minted.access_token, organizationId: minted.organization_id, idpClaims: {} };\n}\n\n/**\n * End the IdP session (RP-initiated logout) and land on `postLogoutRedirectUri`,\n * which must be registered for the client. Returns only by redirecting. If the\n * issuer publishes no end-session endpoint, navigates to the redirect URI directly.\n */\nexport async function endSession(\n base: string,\n clientId: string,\n idToken: string | undefined,\n postLogoutRedirectUri: string,\n): Promise<void> {\n const info = await discover(base);\n const meta = info.AuthIssuer ? await oidcMetadata(info.AuthIssuer) : undefined;\n if (!meta?.end_session_endpoint) {\n window.location.assign(postLogoutRedirectUri);\n return;\n }\n const params: Record<string, string> = {\n client_id: clientId,\n post_logout_redirect_uri: postLogoutRedirectUri,\n };\n if (idToken) params.id_token_hint = idToken;\n const url = new URL(meta.end_session_endpoint);\n url.search = new URLSearchParams(params).toString();\n window.location.assign(url.toString());\n}\n\n/** Discard the in-progress login memo (used on logout so a later login re-runs). */\nexport function resetPending(): void {\n pending = null;\n}\n","// Copyright (C) ConfigHub, Inc.\n// SPDX-License-Identifier: MIT\n\n// A module-level holder for the current minted token, so non-React consumers can read\n// it. RTK Query's `prepareHeaders` (in @confighub/rtk-query) is not a hook and cannot\n// read React context, so it calls getAccessToken() instead. The provider keeps this in\n// sync with its React state, and registers its 401 handling here for the same reason.\nimport { isExpired } from './core';\n\nlet currentToken: string | undefined;\nlet unauthorizedHandler: (() => void) | undefined;\n// Set while a recovery is under way, so a burst of failing requests starts one.\nlet recovering = false;\n\n/** @internal — called by the provider; not part of the public surface. */\nexport function setAccessToken(token: string | undefined): void {\n currentToken = token;\n if (token !== undefined) recovering = false;\n}\n\n/** @internal — the mounted provider's reaction to a rejected token. */\nexport function setUnauthorizedHandler(handler: (() => void) | undefined): void {\n unauthorizedHandler = handler;\n recovering = false;\n}\n\n/**\n * The current minted ConfigHub token, or undefined when unauthenticated. Pass this as\n * the `getToken` for `@confighub/rtk-query`'s `configureConfigHub`, or read it anywhere\n * you need the token outside React.\n *\n * A token past its `exp` is never handed out: the server would reject it. Instead the\n * provider's recovery starts (a silent re-authentication, or a logout, per its\n * `onUnauthorized`) and the request goes without a token; the page is about to\n * navigate to the IdP and back to where it was.\n */\nexport function getAccessToken(): string | undefined {\n if (currentToken !== undefined && isExpired(currentToken)) {\n handleUnauthorized();\n return undefined;\n }\n return currentToken;\n}\n\n/**\n * Tell the mounted provider the API rejected the token (401). Pass this as the\n * `onUnauthorized` for `@confighub/rtk-query`'s `configureConfigHub` to recover from a\n * token rejected before its expiry, e.g. after a server key rotation; expiry itself is\n * handled by `getAccessToken`. A no-op with no provider mounted, and while a recovery\n * is already under way.\n */\nexport function handleUnauthorized(): void {\n if (recovering || !unauthorizedHandler) return;\n recovering = true;\n unauthorizedHandler();\n}\n","// Copyright (C) ConfigHub, Inc.\n// SPDX-License-Identifier: MIT\n\nimport { createConfigHubClient, type ConfigHubClient } from '@confighub/api';\nimport {\n createContext,\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n type ReactNode,\n} from 'react';\nimport {\n NoIdentityProvider,\n OrganizationMissing,\n callbackUri,\n completeLoginFromRedirect,\n endSession,\n isExpired,\n organizationAliasOf,\n redeemBrowserTicket,\n resetPending,\n startLogin,\n type LoginOptions,\n type MintedSession,\n} from './core';\nimport { setAccessToken, setUnauthorizedHandler } from './tokenStore';\n\nexport type AuthStatus = 'loading' | 'authenticated' | 'unauthenticated' | 'error';\n\nexport interface ConfigHubUser {\n organizationId: string;\n idpClaims: Record<string, unknown>;\n}\n\nexport interface LogoutOptions {\n /**\n * Also end the IdP session (RP-initiated logout), so the next login asks for\n * credentials instead of riding the SSO cookie. Redirects the page; the landing\n * URI must be registered for the client. Default: false, which only forgets the\n * token in this tab.\n */\n endSession?: boolean;\n /** Where to land after IdP logout. Defaults to the callback URI. */\n postLogoutRedirectUri?: string;\n}\n\nexport interface ConfigHubAuthContextValue {\n status: AuthStatus;\n user: ConfigHubUser | null;\n error: Error | null;\n /** Begin login: redirects the page to the IdP. */\n login: (options?: LoginOptions) => Promise<void>;\n /** Forget the session in this tab and, optionally, end the IdP session too. */\n logout: (options?: LogoutOptions) => Promise<void>;\n /**\n * The ConfigHub token stopped working (a 401). Try to get a new one without any\n * UI: a `prompt=none` round trip through the IdP, for the organization the session\n * already had. Status goes to `loading` meanwhile, not `unauthenticated`, so an\n * app that auto-logs-in on `unauthenticated` does not race this with an\n * interactive login. If the IdP session is gone too, the page comes back\n * `unauthenticated`. Redirects the page.\n */\n reauthenticate: () => Promise<void>;\n /**\n * Sign in with a ticket from `cub auth browser-session`, for an instance with no\n * identity provider or a local identity. Rejects with the server's error if the\n * ticket is unknown, expired or already used; the session stays as it was.\n */\n signInWithTicket: (ticket: string) => Promise<void>;\n /** Current bearer token, or undefined when unauthenticated. */\n getToken: () => string | undefined;\n /** A typed API client pre-wired with the current token. Stable across renders. */\n client: ConfigHubClient;\n}\n\nexport const ConfigHubAuthContext = createContext<ConfigHubAuthContextValue | null>(null);\n\nexport interface ConfigHubAuthProviderProps {\n /** Absolute base URL of the ConfigHub instance, e.g. `https://hub.confighub.com`. */\n baseUrl: string;\n /** This app's registered OAuth `client_id` (from `cub oauthclient create`). */\n clientId: string;\n /**\n * Same-origin path the IdP redirects back to; `{origin}{callbackPath}` is the\n * redirect URI to register. Defaults to `/`. Fixed on purpose: the page a user\n * starts login from travels in the PKCE state, not in the redirect URI.\n */\n callbackPath?: string;\n /**\n * `'session'` (default) keeps the minted token in `sessionStorage` so a reload or an\n * in-tab navigation does not round-trip through the IdP. Tab-scoped and gone when the\n * tab closes. `'none'`: memory only, every page load starts unauthenticated.\n */\n persist?: 'none' | 'session';\n /**\n * What a 401 from the API means. `'login'` (default): the token is stale, try a\n * silent re-authentication (`prompt=none`) and fall back to unauthenticated if the\n * IdP session is gone too. `'logout'`: just drop the session.\n */\n onUnauthorized?: 'login' | 'logout';\n children: ReactNode;\n}\n\nconst SESSION_KEY = 'confighub_session';\n\nfunction readPersisted(): MintedSession | null {\n try {\n const raw = sessionStorage.getItem(SESSION_KEY);\n if (!raw) return null;\n const session: MintedSession = JSON.parse(raw);\n if (!session.accessToken || isExpired(session.accessToken)) {\n sessionStorage.removeItem(SESSION_KEY);\n return null;\n }\n return session;\n } catch {\n return null;\n }\n}\n\n/**\n * Runs the browser-direct auth flow and manages the token lifecycle. On mount it\n * completes a redirect if the page is the IdP callback, restores a persisted\n * session if there is one, and otherwise starts unauthenticated until `login()`\n * is called.\n */\nexport function ConfigHubAuthProvider({\n baseUrl,\n clientId,\n callbackPath,\n persist = 'session',\n onUnauthorized = 'login',\n children,\n}: ConfigHubAuthProviderProps): JSX.Element {\n const [status, setStatus] = useState<AuthStatus>('loading');\n const [user, setUser] = useState<ConfigHubUser | null>(null);\n const [error, setError] = useState<Error | null>(null);\n\n // The session lives in a ref so getToken() reads the latest value synchronously\n // without re-creating the API client on every render.\n const sessionRef = useRef<MintedSession | undefined>(undefined);\n const flow = useMemo(() => ({ callbackPath }), [callbackPath]);\n\n const persistSession = useCallback(\n (session: MintedSession | undefined) => {\n if (persist !== 'session') return;\n try {\n if (session) sessionStorage.setItem(SESSION_KEY, JSON.stringify(session));\n else sessionStorage.removeItem(SESSION_KEY);\n } catch {\n // Storage unavailable (private mode, quota): memory-only for this tab.\n }\n },\n [persist],\n );\n\n const applySession = useCallback(\n (session: MintedSession) => {\n sessionRef.current = session;\n setAccessToken(session.accessToken); // keep the non-React accessor in sync (rtk-query)\n persistSession(session);\n setUser({ organizationId: session.organizationId, idpClaims: session.idpClaims });\n setError(null);\n setStatus('authenticated');\n },\n [persistSession],\n );\n\n const clearSession = useCallback(() => {\n sessionRef.current = undefined;\n setAccessToken(undefined);\n persistSession(undefined);\n resetPending();\n setUser(null);\n setStatus('unauthenticated');\n }, [persistSession]);\n\n useEffect(() => {\n let cancelled = false;\n completeLoginFromRedirect()\n .then((session) => {\n if (cancelled) return;\n if (session) return applySession(session);\n const persisted = persist === 'session' ? readPersisted() : null;\n if (persisted) return applySession(persisted);\n setStatus('unauthenticated');\n })\n .catch((e: unknown) => {\n if (cancelled) return;\n if (e instanceof OrganizationMissing) {\n // Once, with no hint, flagged so a second miss surfaces as an error.\n void startLogin(\n baseUrl,\n clientId,\n { returnTo: e.returnTo, organization: null },\n flow,\n { retriedForOrganization: true },\n );\n return;\n }\n setError(e instanceof Error ? e : new Error(String(e)));\n setStatus('error');\n });\n return () => {\n cancelled = true;\n };\n }, [applySession, persist, baseUrl, clientId, flow]);\n\n const login = useCallback(\n async (options?: LoginOptions) => {\n setError(null);\n try {\n await startLogin(baseUrl, clientId, options, flow);\n } catch (e: unknown) {\n setError(e instanceof Error ? e : new Error(String(e)));\n setStatus('error');\n }\n },\n [baseUrl, clientId, flow],\n );\n\n const logout = useCallback(\n async (options?: LogoutOptions) => {\n const idToken = sessionRef.current?.idToken;\n if (!options?.endSession) {\n clearSession();\n return;\n }\n // Forget the session but stay 'loading': the page is about to navigate to the\n // IdP's end-session endpoint, and 'unauthenticated' would let an app that\n // auto-logs-in start a login that races the logout and can win it, landing\n // the user back in the session they just ended.\n sessionRef.current = undefined;\n setAccessToken(undefined);\n persistSession(undefined);\n resetPending();\n setUser(null);\n setStatus('loading');\n try {\n await endSession(\n baseUrl,\n clientId,\n idToken,\n options.postLogoutRedirectUri ?? callbackUri(flow),\n );\n } catch (e: unknown) {\n // The session in this tab is already gone; say why the IdP's is not.\n setError(e instanceof Error ? e : new Error(String(e)));\n setStatus('error');\n }\n },\n [baseUrl, clientId, clearSession, flow, persistSession],\n );\n\n const getToken = useCallback(() => sessionRef.current?.accessToken, []);\n\n const reauthenticate = useCallback(async () => {\n const organization = organizationAliasOf(sessionRef.current?.idpClaims ?? {});\n // Forget the token but stay 'loading': the page is about to navigate away,\n // and 'unauthenticated' would invite an interactive login in the meantime.\n sessionRef.current = undefined;\n setAccessToken(undefined);\n persistSession(undefined);\n resetPending();\n setStatus('loading');\n try {\n await startLogin(baseUrl, clientId, { prompt: 'none', organization }, flow);\n } catch (e: unknown) {\n // No IdP to ask: the session came from a ticket, and the user needs a new one.\n if (e instanceof NoIdentityProvider) {\n setStatus('unauthenticated');\n return;\n }\n setError(e instanceof Error ? e : new Error(String(e)));\n setStatus('error');\n }\n }, [baseUrl, clientId, flow, persistSession]);\n\n const signInWithTicket = useCallback(\n async (ticket: string) => {\n applySession(await redeemBrowserTicket(baseUrl, ticket));\n },\n [applySession, baseUrl],\n );\n\n // A 401 means the minted token no longer works. Silent re-auth keeps the user's\n // place if the IdP session is still alive; otherwise the page comes back\n // unauthenticated and the app offers a real login.\n const handleUnauthorized = useCallback(() => {\n if (!sessionRef.current) return;\n if (onUnauthorized === 'login') void reauthenticate();\n else clearSession();\n }, [clearSession, reauthenticate, onUnauthorized]);\n\n // Non-React callers (getAccessToken on an expired token, handleUnauthorized from a\n // data client's 401) reach this provider's handling through the token store.\n useEffect(() => {\n setUnauthorizedHandler(handleUnauthorized);\n return () => setUnauthorizedHandler(undefined);\n }, [handleUnauthorized]);\n\n // One client for the provider's lifetime. getToken reads the session ref.\n const client = useMemo(\n () => createConfigHubClient({ baseUrl, getToken, onUnauthorized: handleUnauthorized }),\n [baseUrl, getToken, handleUnauthorized],\n );\n\n const value = useMemo<ConfigHubAuthContextValue>(\n () => ({\n status,\n user,\n error,\n login,\n logout,\n reauthenticate,\n signInWithTicket,\n getToken,\n client,\n }),\n [status, user, error, login, logout, reauthenticate, signInWithTicket, getToken, client],\n );\n\n return (\n <ConfigHubAuthContext.Provider value={value}>{children}</ConfigHubAuthContext.Provider>\n );\n}\n","// Copyright (C) ConfigHub, Inc.\n// SPDX-License-Identifier: MIT\n\nimport type { ConfigHubClient } from '@confighub/api';\nimport { useContext } from 'react';\nimport { ConfigHubAuthContext, type ConfigHubAuthContextValue } from './provider';\n\n/**\n * Access the ConfigHub auth state and actions. Must be called under a\n * `<ConfigHubAuthProvider>`.\n *\n * ```ts\n * const { status, user, login, logout } = useAuth();\n * ```\n */\nexport function useAuth(): ConfigHubAuthContextValue {\n const ctx = useContext(ConfigHubAuthContext);\n if (!ctx) {\n throw new Error('useAuth must be used within a <ConfigHubAuthProvider>');\n }\n return ctx;\n}\n\n/**\n * The typed ConfigHub API client, pre-wired with the current token. This is the\n * seam between `@confighub/react-auth` and `@confighub/api`: you never pass a\n * token by hand.\n *\n * ```ts\n * const api = useConfigHub();\n * const { data } = await api.GET('/space/{space_id}/unit', { params: { path: { space_id } } });\n * ```\n */\nexport function useConfigHub(): ConfigHubClient {\n return useAuth().client;\n}\n"]}
|
package/dist/index.d.cts
CHANGED
|
@@ -43,9 +43,12 @@ interface LoginOptions {
|
|
|
43
43
|
* `'none'` asks the IdP to re-authenticate without any UI, failing with
|
|
44
44
|
* `login_required` if the SSO session is gone -- the way to refresh an expired
|
|
45
45
|
* ConfigHub token when the user is still signed in at the IdP. `'login'` forces the
|
|
46
|
-
* login form even with a live SSO session.
|
|
46
|
+
* login form even with a live SSO session. `'create'` opens the IdP's registration
|
|
47
|
+
* form instead of its login form (OpenID Connect Prompt Create; Keycloak honours it),
|
|
48
|
+
* so a "sign up" link can be an ordinary PKCE login that lands the new user in the
|
|
49
|
+
* app signed in.
|
|
47
50
|
*/
|
|
48
|
-
prompt?: 'none' | 'login';
|
|
51
|
+
prompt?: 'none' | 'login' | 'create';
|
|
49
52
|
}
|
|
50
53
|
interface FlowOptions {
|
|
51
54
|
/**
|
|
@@ -65,6 +68,14 @@ declare class OrganizationMissing extends Error {
|
|
|
65
68
|
readonly returnTo: string;
|
|
66
69
|
constructor(returnTo: string);
|
|
67
70
|
}
|
|
71
|
+
/**
|
|
72
|
+
* Thrown by `startLogin` when the instance advertises no identity provider. Such an
|
|
73
|
+
* instance signs a browser in with a ticket from `cub auth browser-session` instead
|
|
74
|
+
* (see `redeemBrowserTicket`), so there is no login to start.
|
|
75
|
+
*/
|
|
76
|
+
declare class NoIdentityProvider extends Error {
|
|
77
|
+
constructor();
|
|
78
|
+
}
|
|
68
79
|
/** The organization alias of the last successful login for this client, if any. */
|
|
69
80
|
declare function rememberedOrganization(clientId: string): string | undefined;
|
|
70
81
|
/**
|
|
@@ -103,12 +114,6 @@ interface ConfigHubAuthContextValue {
|
|
|
103
114
|
login: (options?: LoginOptions) => Promise<void>;
|
|
104
115
|
/** Forget the session in this tab and, optionally, end the IdP session too. */
|
|
105
116
|
logout: (options?: LogoutOptions) => Promise<void>;
|
|
106
|
-
/**
|
|
107
|
-
* Re-mint the ConfigHub token for another organization the user belongs to. The
|
|
108
|
-
* IdP session is untouched. Rejects with the server's error if the user is not a
|
|
109
|
-
* member; the current session stays as it was.
|
|
110
|
-
*/
|
|
111
|
-
switchOrganization: (organizationId: string) => Promise<void>;
|
|
112
117
|
/**
|
|
113
118
|
* The ConfigHub token stopped working (a 401). Try to get a new one without any
|
|
114
119
|
* UI: a `prompt=none` round trip through the IdP, for the organization the session
|
|
@@ -118,6 +123,12 @@ interface ConfigHubAuthContextValue {
|
|
|
118
123
|
* `unauthenticated`. Redirects the page.
|
|
119
124
|
*/
|
|
120
125
|
reauthenticate: () => Promise<void>;
|
|
126
|
+
/**
|
|
127
|
+
* Sign in with a ticket from `cub auth browser-session`, for an instance with no
|
|
128
|
+
* identity provider or a local identity. Rejects with the server's error if the
|
|
129
|
+
* ticket is unknown, expired or already used; the session stays as it was.
|
|
130
|
+
*/
|
|
131
|
+
signInWithTicket: (ticket: string) => Promise<void>;
|
|
121
132
|
/** Current bearer token, or undefined when unauthenticated. */
|
|
122
133
|
getToken: () => string | undefined;
|
|
123
134
|
/** A typed API client pre-wired with the current token. Stable across renders. */
|
|
@@ -198,4 +209,4 @@ declare function getAccessToken(): string | undefined;
|
|
|
198
209
|
*/
|
|
199
210
|
declare function handleUnauthorized(): void;
|
|
200
211
|
|
|
201
|
-
export { type AuthStatus, ConfigHubAuthContext, type ConfigHubAuthContextValue, ConfigHubAuthProvider, type ConfigHubAuthProviderProps, type ConfigHubUser, type Discovery, type FlowOptions, type LoginOptions, type LogoutOptions, type MintedSession, OrganizationMissing, callbackUri, decodeJwtClaims, getAccessToken, handleUnauthorized, isExpired, organizationAliasOf, rememberedOrganization, useAuth, useConfigHub };
|
|
212
|
+
export { type AuthStatus, ConfigHubAuthContext, type ConfigHubAuthContextValue, ConfigHubAuthProvider, type ConfigHubAuthProviderProps, type ConfigHubUser, type Discovery, type FlowOptions, type LoginOptions, type LogoutOptions, type MintedSession, NoIdentityProvider, OrganizationMissing, callbackUri, decodeJwtClaims, getAccessToken, handleUnauthorized, isExpired, organizationAliasOf, rememberedOrganization, useAuth, useConfigHub };
|
package/dist/index.d.ts
CHANGED
|
@@ -43,9 +43,12 @@ interface LoginOptions {
|
|
|
43
43
|
* `'none'` asks the IdP to re-authenticate without any UI, failing with
|
|
44
44
|
* `login_required` if the SSO session is gone -- the way to refresh an expired
|
|
45
45
|
* ConfigHub token when the user is still signed in at the IdP. `'login'` forces the
|
|
46
|
-
* login form even with a live SSO session.
|
|
46
|
+
* login form even with a live SSO session. `'create'` opens the IdP's registration
|
|
47
|
+
* form instead of its login form (OpenID Connect Prompt Create; Keycloak honours it),
|
|
48
|
+
* so a "sign up" link can be an ordinary PKCE login that lands the new user in the
|
|
49
|
+
* app signed in.
|
|
47
50
|
*/
|
|
48
|
-
prompt?: 'none' | 'login';
|
|
51
|
+
prompt?: 'none' | 'login' | 'create';
|
|
49
52
|
}
|
|
50
53
|
interface FlowOptions {
|
|
51
54
|
/**
|
|
@@ -65,6 +68,14 @@ declare class OrganizationMissing extends Error {
|
|
|
65
68
|
readonly returnTo: string;
|
|
66
69
|
constructor(returnTo: string);
|
|
67
70
|
}
|
|
71
|
+
/**
|
|
72
|
+
* Thrown by `startLogin` when the instance advertises no identity provider. Such an
|
|
73
|
+
* instance signs a browser in with a ticket from `cub auth browser-session` instead
|
|
74
|
+
* (see `redeemBrowserTicket`), so there is no login to start.
|
|
75
|
+
*/
|
|
76
|
+
declare class NoIdentityProvider extends Error {
|
|
77
|
+
constructor();
|
|
78
|
+
}
|
|
68
79
|
/** The organization alias of the last successful login for this client, if any. */
|
|
69
80
|
declare function rememberedOrganization(clientId: string): string | undefined;
|
|
70
81
|
/**
|
|
@@ -103,12 +114,6 @@ interface ConfigHubAuthContextValue {
|
|
|
103
114
|
login: (options?: LoginOptions) => Promise<void>;
|
|
104
115
|
/** Forget the session in this tab and, optionally, end the IdP session too. */
|
|
105
116
|
logout: (options?: LogoutOptions) => Promise<void>;
|
|
106
|
-
/**
|
|
107
|
-
* Re-mint the ConfigHub token for another organization the user belongs to. The
|
|
108
|
-
* IdP session is untouched. Rejects with the server's error if the user is not a
|
|
109
|
-
* member; the current session stays as it was.
|
|
110
|
-
*/
|
|
111
|
-
switchOrganization: (organizationId: string) => Promise<void>;
|
|
112
117
|
/**
|
|
113
118
|
* The ConfigHub token stopped working (a 401). Try to get a new one without any
|
|
114
119
|
* UI: a `prompt=none` round trip through the IdP, for the organization the session
|
|
@@ -118,6 +123,12 @@ interface ConfigHubAuthContextValue {
|
|
|
118
123
|
* `unauthenticated`. Redirects the page.
|
|
119
124
|
*/
|
|
120
125
|
reauthenticate: () => Promise<void>;
|
|
126
|
+
/**
|
|
127
|
+
* Sign in with a ticket from `cub auth browser-session`, for an instance with no
|
|
128
|
+
* identity provider or a local identity. Rejects with the server's error if the
|
|
129
|
+
* ticket is unknown, expired or already used; the session stays as it was.
|
|
130
|
+
*/
|
|
131
|
+
signInWithTicket: (ticket: string) => Promise<void>;
|
|
121
132
|
/** Current bearer token, or undefined when unauthenticated. */
|
|
122
133
|
getToken: () => string | undefined;
|
|
123
134
|
/** A typed API client pre-wired with the current token. Stable across renders. */
|
|
@@ -198,4 +209,4 @@ declare function getAccessToken(): string | undefined;
|
|
|
198
209
|
*/
|
|
199
210
|
declare function handleUnauthorized(): void;
|
|
200
211
|
|
|
201
|
-
export { type AuthStatus, ConfigHubAuthContext, type ConfigHubAuthContextValue, ConfigHubAuthProvider, type ConfigHubAuthProviderProps, type ConfigHubUser, type Discovery, type FlowOptions, type LoginOptions, type LogoutOptions, type MintedSession, OrganizationMissing, callbackUri, decodeJwtClaims, getAccessToken, handleUnauthorized, isExpired, organizationAliasOf, rememberedOrganization, useAuth, useConfigHub };
|
|
212
|
+
export { type AuthStatus, ConfigHubAuthContext, type ConfigHubAuthContextValue, ConfigHubAuthProvider, type ConfigHubAuthProviderProps, type ConfigHubUser, type Discovery, type FlowOptions, type LoginOptions, type LogoutOptions, type MintedSession, NoIdentityProvider, OrganizationMissing, callbackUri, decodeJwtClaims, getAccessToken, handleUnauthorized, isExpired, organizationAliasOf, rememberedOrganization, useAuth, useConfigHub };
|
package/dist/index.js
CHANGED
|
@@ -12,6 +12,12 @@ var OrganizationMissing = class extends Error {
|
|
|
12
12
|
this.name = "OrganizationMissing";
|
|
13
13
|
}
|
|
14
14
|
};
|
|
15
|
+
var NoIdentityProvider = class extends Error {
|
|
16
|
+
constructor() {
|
|
17
|
+
super("this instance has no identity provider; sign in with `cub auth browser-session`");
|
|
18
|
+
this.name = "NoIdentityProvider";
|
|
19
|
+
}
|
|
20
|
+
};
|
|
15
21
|
var PKCE_KEY = "confighub_pkce";
|
|
16
22
|
var LAST_ORG_KEY = "confighub_last_org";
|
|
17
23
|
var lastOrgKey = (clientId) => `${LAST_ORG_KEY}:${clientId}`;
|
|
@@ -69,6 +75,7 @@ async function oidcMetadata(issuer) {
|
|
|
69
75
|
}
|
|
70
76
|
async function startLogin(base, clientId, login = {}, flow = {}, retry = {}) {
|
|
71
77
|
const info = await discover(base);
|
|
78
|
+
if (!info.AuthIssuer && !info.TokenExchangeEndpoint) throw new NoIdentityProvider();
|
|
72
79
|
if (!info.AuthIssuer || !info.TokenExchangeEndpoint) {
|
|
73
80
|
throw new Error(
|
|
74
81
|
"this instance is not configured for token-exchange auth (server needs CONFIGHUB_IDP_ISSUER)"
|
|
@@ -171,18 +178,15 @@ async function exchange(exchangeEndpoint, subjectToken) {
|
|
|
171
178
|
const minted = await exResp.json();
|
|
172
179
|
return { accessToken: minted.access_token, organizationId: minted.organization_id };
|
|
173
180
|
}
|
|
174
|
-
async function
|
|
175
|
-
const r = await fetch(trimSlash(base) + "/auth/
|
|
181
|
+
async function redeemBrowserTicket(base, ticket) {
|
|
182
|
+
const r = await fetch(trimSlash(base) + "/auth/browser-session", {
|
|
176
183
|
method: "POST",
|
|
177
|
-
headers: {
|
|
178
|
-
|
|
179
|
-
"Content-Type": "application/x-www-form-urlencoded"
|
|
180
|
-
},
|
|
181
|
-
body: new URLSearchParams({ organization_id: organizationId })
|
|
184
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
185
|
+
body: new URLSearchParams({ ticket })
|
|
182
186
|
});
|
|
183
|
-
if (!r.ok) throw new Error(`/auth/
|
|
187
|
+
if (!r.ok) throw new Error(`/auth/browser-session ${r.status}: ${await r.text()}`);
|
|
184
188
|
const minted = await r.json();
|
|
185
|
-
return { accessToken: minted.access_token, organizationId: minted.organization_id };
|
|
189
|
+
return { accessToken: minted.access_token, organizationId: minted.organization_id, idpClaims: {} };
|
|
186
190
|
}
|
|
187
191
|
async function endSession(base, clientId, idToken, postLogoutRedirectUri) {
|
|
188
192
|
const info = await discover(base);
|
|
@@ -329,27 +333,29 @@ function ConfigHubAuthProvider({
|
|
|
329
333
|
const logout = useCallback(
|
|
330
334
|
async (options) => {
|
|
331
335
|
const idToken = sessionRef.current?.idToken;
|
|
332
|
-
|
|
333
|
-
|
|
336
|
+
if (!options?.endSession) {
|
|
337
|
+
clearSession();
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
sessionRef.current = void 0;
|
|
341
|
+
setAccessToken(void 0);
|
|
342
|
+
persistSession(void 0);
|
|
343
|
+
resetPending();
|
|
344
|
+
setUser(null);
|
|
345
|
+
setStatus("loading");
|
|
346
|
+
try {
|
|
334
347
|
await endSession(
|
|
335
348
|
baseUrl,
|
|
336
349
|
clientId,
|
|
337
350
|
idToken,
|
|
338
351
|
options.postLogoutRedirectUri ?? callbackUri(flow)
|
|
339
352
|
);
|
|
353
|
+
} catch (e) {
|
|
354
|
+
setError(e instanceof Error ? e : new Error(String(e)));
|
|
355
|
+
setStatus("error");
|
|
340
356
|
}
|
|
341
357
|
},
|
|
342
|
-
[baseUrl, clientId, clearSession, flow]
|
|
343
|
-
);
|
|
344
|
-
const switchOrganization2 = useCallback(
|
|
345
|
-
async (organizationId) => {
|
|
346
|
-
const current = sessionRef.current;
|
|
347
|
-
if (!current) throw new Error("not authenticated");
|
|
348
|
-
const minted = await switchOrganization(baseUrl, current.accessToken, organizationId);
|
|
349
|
-
rememberOrganization(clientId, void 0);
|
|
350
|
-
applySession({ ...current, ...minted });
|
|
351
|
-
},
|
|
352
|
-
[applySession, baseUrl, clientId]
|
|
358
|
+
[baseUrl, clientId, clearSession, flow, persistSession]
|
|
353
359
|
);
|
|
354
360
|
const getToken = useCallback(() => sessionRef.current?.accessToken, []);
|
|
355
361
|
const reauthenticate = useCallback(async () => {
|
|
@@ -362,10 +368,20 @@ function ConfigHubAuthProvider({
|
|
|
362
368
|
try {
|
|
363
369
|
await startLogin(baseUrl, clientId, { prompt: "none", organization }, flow);
|
|
364
370
|
} catch (e) {
|
|
371
|
+
if (e instanceof NoIdentityProvider) {
|
|
372
|
+
setStatus("unauthenticated");
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
365
375
|
setError(e instanceof Error ? e : new Error(String(e)));
|
|
366
376
|
setStatus("error");
|
|
367
377
|
}
|
|
368
378
|
}, [baseUrl, clientId, flow, persistSession]);
|
|
379
|
+
const signInWithTicket = useCallback(
|
|
380
|
+
async (ticket) => {
|
|
381
|
+
applySession(await redeemBrowserTicket(baseUrl, ticket));
|
|
382
|
+
},
|
|
383
|
+
[applySession, baseUrl]
|
|
384
|
+
);
|
|
369
385
|
const handleUnauthorized2 = useCallback(() => {
|
|
370
386
|
if (!sessionRef.current) return;
|
|
371
387
|
if (onUnauthorized === "login") void reauthenticate();
|
|
@@ -386,12 +402,12 @@ function ConfigHubAuthProvider({
|
|
|
386
402
|
error,
|
|
387
403
|
login,
|
|
388
404
|
logout,
|
|
389
|
-
switchOrganization: switchOrganization2,
|
|
390
405
|
reauthenticate,
|
|
406
|
+
signInWithTicket,
|
|
391
407
|
getToken,
|
|
392
408
|
client
|
|
393
409
|
}),
|
|
394
|
-
[status, user, error, login, logout,
|
|
410
|
+
[status, user, error, login, logout, reauthenticate, signInWithTicket, getToken, client]
|
|
395
411
|
);
|
|
396
412
|
return /* @__PURE__ */ jsx(ConfigHubAuthContext.Provider, { value, children });
|
|
397
413
|
}
|
|
@@ -406,6 +422,6 @@ function useConfigHub() {
|
|
|
406
422
|
return useAuth().client;
|
|
407
423
|
}
|
|
408
424
|
|
|
409
|
-
export { ConfigHubAuthContext, ConfigHubAuthProvider, OrganizationMissing, callbackUri, decodeJwtClaims, getAccessToken, handleUnauthorized, isExpired, organizationAliasOf, rememberedOrganization, useAuth, useConfigHub };
|
|
425
|
+
export { ConfigHubAuthContext, ConfigHubAuthProvider, NoIdentityProvider, OrganizationMissing, callbackUri, decodeJwtClaims, getAccessToken, handleUnauthorized, isExpired, organizationAliasOf, rememberedOrganization, useAuth, useConfigHub };
|
|
410
426
|
//# sourceMappingURL=index.js.map
|
|
411
427
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/core.ts","../src/tokenStore.ts","../src/provider.tsx","../src/hooks.ts"],"names":["switchOrganization","handleUnauthorized"],"mappings":"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","file":"index.js","sourcesContent":["// Copyright (C) ConfigHub, Inc.\n// SPDX-License-Identifier: MIT\n//\n// Framework-neutral browser-auth engine for ConfigHub. Productionized from the\n// reference harness `test/browser-auth/src/confighubAuth.ts` in the ConfigHub\n// monorepo, which is validated end to end against staging and prod.\n//\n// Flow (design: third-party-browser-app-auth.md §6):\n// GET {base}/api/info -> discovery { AuthIssuer, TokenExchangeEndpoint }\n// OIDC discovery on AuthIssuer -> authorize/token/end_session endpoints\n// PKCE authorize + code->token -> IdP token\n// POST {TokenExchangeEndpoint} (8693) -> minted ConfigHub token\n//\n// The minted token then rides `Authorization: Bearer` against `/api`. The flow is\n// edition-agnostic: `AuthIssuer` is whatever discovery names (ConfigHub's bundled\n// Keycloak for Cloud, the org's own IdP for Enterprise), so the same code runs\n// against both. Tokens are held by the caller; only the transient PKCE state is\n// parked in sessionStorage across the authorize redirect.\n\nexport interface Discovery {\n AuthIssuer?: string;\n TokenExchangeEndpoint?: string;\n TokenExchangeAudience?: string;\n}\n\nexport interface MintedSession {\n accessToken: string;\n organizationId: string;\n /** Claims of the validated IdP token (owning-org, audience, organization shape). */\n idpClaims: Record<string, unknown>;\n /**\n * The IdP's ID token, kept only so logout can pass it as `id_token_hint` to the\n * end-session endpoint. Absent for sessions that did not come from an OIDC login.\n */\n idToken?: string;\n}\n\nexport interface LoginOptions {\n /**\n * Where to land after login, as a same-origin path (`/space/x?tab=units`). Carried\n * through the authorize round trip in the PKCE state, never in the redirect URI:\n * OAuth clients register exact redirect URIs, so the URI itself must not vary with\n * the page the user started from. Defaults to the current path and query.\n */\n returnTo?: string;\n /**\n * Which organization to sign in to, as the Keycloak organization alias sent in\n * the `organization:<alias>` scope.\n *\n * - a string: that organization, no prompt;\n * - `undefined` (default): the organization of the last successful login in this\n * browser, remembered per client in `localStorage`, so a new tab or a login after\n * logout lands in the same organization without a prompt; with nothing\n * remembered, Keycloak decides (prompt for a multi-org user, or the org matching\n * the email domain on a fresh authentication);\n * - `null`: no hint on purpose, so Keycloak prompts. This is \"switch organization\".\n */\n organization?: string | null;\n /**\n * `'none'` asks the IdP to re-authenticate without any UI, failing with\n * `login_required` if the SSO session is gone -- the way to refresh an expired\n * ConfigHub token when the user is still signed in at the IdP. `'login'` forces the\n * login form even with a live SSO session.\n */\n prompt?: 'none' | 'login';\n}\n\nexport interface FlowOptions {\n /**\n * Same-origin path the IdP redirects back to, and therefore the redirect URI to\n * register for the client: `{origin}{callbackPath}`. Defaults to `/`.\n */\n callbackPath?: string;\n}\n\ninterface PkceState {\n verifier: string;\n state: string;\n clientId: string;\n tokenEndpoint: string;\n exchangeEndpoint: string;\n redirectUri: string;\n returnTo: string;\n silent: boolean;\n /** The organization alias the authorize request hinted, if any. */\n hintedOrganization?: string;\n /** This login is already the retry after a token without an organization. */\n retriedForOrganization?: boolean;\n}\n\n/**\n * Thrown by `completeLoginFromRedirect` when the IdP token names no organization,\n * which the exchange would refuse. Seen on a fresh brokered (Google) login, where\n * Keycloak's organization step does not run; on the next login the SSO session is\n * alive and it does, so the caller logs in again, once, with no hint. Any\n * remembered alias has already been forgotten.\n */\nexport class OrganizationMissing extends Error {\n constructor(public readonly returnTo: string) {\n super('the identity provider issued a token with no organization');\n this.name = 'OrganizationMissing';\n }\n}\n\nexport interface RetryOptions {\n /** @internal set by the provider on the one retry after OrganizationMissing. */\n retriedForOrganization?: boolean;\n}\n\nconst PKCE_KEY = 'confighub_pkce';\nconst LAST_ORG_KEY = 'confighub_last_org';\n\n// The alias is a short public identifier, not a credential, so localStorage is the\n// right place: it must outlive the tab, which is exactly what the token must not.\nconst lastOrgKey = (clientId: string): string => `${LAST_ORG_KEY}:${clientId}`;\n\n/** The organization alias of the last successful login for this client, if any. */\nexport function rememberedOrganization(clientId: string): string | undefined {\n try {\n return localStorage.getItem(lastOrgKey(clientId)) ?? undefined;\n } catch {\n return undefined;\n }\n}\n\n/** @internal */\nexport function rememberOrganization(clientId: string, alias: string | undefined): void {\n try {\n if (alias) localStorage.setItem(lastOrgKey(clientId), alias);\n else localStorage.removeItem(lastOrgKey(clientId));\n } catch {\n // Storage unavailable: the next login gets no hint.\n }\n}\n\n/**\n * The alias in an IdP token's `organization` claim (`{ \"<alias>\": { id } }`), or\n * undefined when the claim is absent or names more than one organization.\n */\nexport function organizationAliasOf(idpClaims: Record<string, unknown>): string | undefined {\n const org = idpClaims.organization;\n if (!org || typeof org !== 'object') return undefined;\n const aliases = Object.keys(org as Record<string, unknown>);\n return aliases.length === 1 ? aliases[0] : undefined;\n}\n\nconst trimSlash = (s: string): string => s.replace(/\\/+$/, '');\n\n/** The fixed callback URI: the page origin plus the configured callback path. */\nexport const callbackUri = (opts?: FlowOptions): string =>\n window.location.origin + (opts?.callbackPath ?? '/');\n\nconst currentPath = (): string =>\n window.location.pathname + window.location.search + window.location.hash;\n\nconst b64url = (buf: ArrayBuffer): string =>\n btoa(String.fromCharCode(...new Uint8Array(buf)))\n .replace(/\\+/g, '-')\n .replace(/\\//g, '_')\n .replace(/=+$/, '');\n\nconst randomString = (n = 64): string =>\n b64url(crypto.getRandomValues(new Uint8Array(n)).buffer);\n\nasync function sha256(s: string): Promise<string> {\n return b64url(await crypto.subtle.digest('SHA-256', new TextEncoder().encode(s)));\n}\n\n/** Decode a JWT's claims without verifying it. Returns {} for anything malformed. */\nexport function decodeJwtClaims(token: string): Record<string, unknown> {\n const part = token.split('.')[1];\n if (!part) return {};\n try {\n return JSON.parse(atob(part.replace(/-/g, '+').replace(/_/g, '/')));\n } catch {\n return {};\n }\n}\n\n/** Whether a JWT's `exp` is in the past (with a small skew allowance). */\nexport function isExpired(token: string, skewSeconds = 30): boolean {\n const exp = decodeJwtClaims(token).exp;\n if (typeof exp !== 'number') return false;\n return exp * 1000 <= Date.now() + skewSeconds * 1000;\n}\n\nexport async function discover(base: string): Promise<Discovery> {\n const r = await fetch(trimSlash(base) + '/api/info');\n if (!r.ok) throw new Error('/api/info failed: ' + r.status);\n return r.json();\n}\n\ninterface OidcMetadata {\n authorization_endpoint: string;\n token_endpoint: string;\n end_session_endpoint?: string;\n}\n\nasync function oidcMetadata(issuer: string): Promise<OidcMetadata> {\n const r = await fetch(trimSlash(issuer) + '/.well-known/openid-configuration');\n if (!r.ok) throw new Error('OIDC discovery failed: ' + r.status);\n return r.json();\n}\n\n/**\n * Discover, build a PKCE request, and navigate to the IdP authorize endpoint.\n * Returns only by redirecting the page; `completeLoginFromRedirect()` finishes on\n * the way back.\n */\nexport async function startLogin(\n base: string,\n clientId: string,\n login: LoginOptions = {},\n flow: FlowOptions = {},\n retry: RetryOptions = {},\n): Promise<void> {\n const info = await discover(base);\n if (!info.AuthIssuer || !info.TokenExchangeEndpoint) {\n throw new Error(\n 'this instance is not configured for token-exchange auth (server needs CONFIGHUB_IDP_ISSUER)',\n );\n }\n const meta = await oidcMetadata(info.AuthIssuer);\n const verifier = randomString();\n const challenge = await sha256(verifier);\n const state = randomString(16);\n const redirectUri = callbackUri(flow);\n // The \"organization\" scope makes Keycloak emit the org claim the exchange resolves;\n // \"organization:<alias>\" selects one without prompting.\n const alias =\n login.organization === null\n ? undefined\n : (login.organization ?? rememberedOrganization(clientId));\n const orgScope = alias ? `organization:${alias}` : 'organization';\n const pkce: PkceState = {\n verifier,\n state,\n clientId,\n tokenEndpoint: meta.token_endpoint,\n exchangeEndpoint: info.TokenExchangeEndpoint,\n redirectUri,\n returnTo: login.returnTo ?? currentPath(),\n silent: login.prompt === 'none',\n hintedOrganization: alias,\n retriedForOrganization: retry.retriedForOrganization,\n };\n sessionStorage.setItem(PKCE_KEY, JSON.stringify(pkce));\n const params: Record<string, string> = {\n response_type: 'code',\n client_id: clientId,\n redirect_uri: redirectUri,\n scope: `openid email profile ${orgScope}`,\n code_challenge: challenge,\n code_challenge_method: 'S256',\n state,\n };\n if (login.prompt) params.prompt = login.prompt;\n\n const authURL = new URL(meta.authorization_endpoint);\n authURL.search = new URLSearchParams(params).toString();\n window.location.assign(authURL.toString());\n}\n\n// Memoize so React StrictMode's double-mount can't redeem the one-time code twice.\nlet pending: Promise<MintedSession | null> | null = null;\n\n/**\n * If the page is the IdP redirect (`?code=...`), exchange the code for an IdP token\n * and then exchange that for a minted ConfigHub token, and restore the URL the\n * login started from. Returns null on a normal load, when a `prompt=none` attempt\n * came back with `login_required` (the SSO session is gone; the caller should offer\n * an interactive login), and when the redirect carries a code this tab has no PKCE\n * state for (an IdP email flow landed here; the caller should start a login).\n */\nexport function completeLoginFromRedirect(): Promise<MintedSession | null> {\n if (!pending) pending = doCompleteLogin();\n return pending;\n}\n\nconst SILENT_FAILURES = new Set(['login_required', 'interaction_required', 'consent_required']);\n\nasync function doCompleteLogin(): Promise<MintedSession | null> {\n const params = new URLSearchParams(window.location.search);\n const code = params.get('code');\n const error = params.get('error');\n if (!code && !error) return null;\n\n const savedRaw = sessionStorage.getItem(PKCE_KEY);\n sessionStorage.removeItem(PKCE_KEY);\n const saved: PkceState | null = savedRaw ? JSON.parse(savedRaw) : null;\n\n // Put the URL back to where the user started before anything else can fail, so\n // neither the code nor an error string lingers in the address bar or history.\n history.replaceState({}, '', saved?.returnTo ?? callbackUri());\n\n if (error) {\n if (saved?.silent && SILENT_FAILURES.has(error)) return null;\n throw new Error(`IdP returned error: ${error} ${params.get('error_description') ?? ''}`);\n }\n // A code with no PKCE state means this tab did not start the login: the IdP's\n // email flows (password reset, verify email) finish the authentication in the tab\n // the mail link opened and redirect it here, while the verifier sits in the tab\n // that began. The code is unusable without it, so drop it and report a normal\n // load; the caller starts a login, which the now-live IdP session completes\n // without a prompt.\n if (!saved) return null;\n if (params.get('state') !== saved.state) throw new Error('state mismatch; aborting');\n\n // Exchange the authorization code for an IdP token (PKCE, public client).\n const tokenResp = await fetch(saved.tokenEndpoint, {\n method: 'POST',\n headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\n body: new URLSearchParams({\n grant_type: 'authorization_code',\n code: code!,\n redirect_uri: saved.redirectUri,\n client_id: saved.clientId,\n code_verifier: saved.verifier,\n }),\n });\n if (!tokenResp.ok) {\n throw new Error(`IdP token endpoint ${tokenResp.status}: ${await tokenResp.text()}`);\n }\n const idpToken = await tokenResp.json();\n\n // RFC 8693 token exchange against ConfigHub -> minted ConfigHub token.\n const idpClaims = decodeJwtClaims(idpToken.access_token);\n if (!organizationAliasOf(idpClaims) && !saved.retriedForOrganization) {\n // The exchange would refuse this token. Rather than surface that, log in\n // once more: with the SSO session now alive the IdP runs its organization\n // step. A hint that was sent evidently did not help, so forget it.\n if (saved.hintedOrganization) rememberOrganization(saved.clientId, undefined);\n throw new OrganizationMissing(saved.returnTo);\n }\n const minted = await exchange(saved.exchangeEndpoint, idpToken.access_token);\n rememberOrganization(saved.clientId, organizationAliasOf(idpClaims));\n return {\n ...minted,\n idpClaims,\n idToken: typeof idpToken.id_token === 'string' ? idpToken.id_token : undefined,\n };\n}\n\nasync function exchange(\n exchangeEndpoint: string,\n subjectToken: string,\n): Promise<Pick<MintedSession, 'accessToken' | 'organizationId'>> {\n const exResp = await fetch(exchangeEndpoint, {\n method: 'POST',\n headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\n body: new URLSearchParams({\n grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange',\n subject_token: subjectToken,\n subject_token_type: 'urn:ietf:params:oauth:token-type:access_token',\n }),\n });\n if (!exResp.ok) throw new Error(`/auth/exchange ${exResp.status}: ${await exResp.text()}`);\n const minted = await exResp.json();\n return { accessToken: minted.access_token, organizationId: minted.organization_id };\n}\n\n/**\n * Trade the current minted token for one scoped to another organization the user\n * belongs to (`POST {base}/auth/switch-organization`, bearer-authenticated). The\n * IdP session is untouched; only the ConfigHub token changes.\n */\nexport async function switchOrganization(\n base: string,\n accessToken: string,\n organizationId: string,\n): Promise<Pick<MintedSession, 'accessToken' | 'organizationId'>> {\n const r = await fetch(trimSlash(base) + '/auth/switch-organization', {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${accessToken}`,\n 'Content-Type': 'application/x-www-form-urlencoded',\n },\n body: new URLSearchParams({ organization_id: organizationId }),\n });\n if (!r.ok) throw new Error(`/auth/switch-organization ${r.status}: ${await r.text()}`);\n const minted = await r.json();\n return { accessToken: minted.access_token, organizationId: minted.organization_id };\n}\n\n/**\n * End the IdP session (RP-initiated logout) and land on `postLogoutRedirectUri`,\n * which must be registered for the client. Returns only by redirecting. If the\n * issuer publishes no end-session endpoint, navigates to the redirect URI directly.\n */\nexport async function endSession(\n base: string,\n clientId: string,\n idToken: string | undefined,\n postLogoutRedirectUri: string,\n): Promise<void> {\n const info = await discover(base);\n const meta = info.AuthIssuer ? await oidcMetadata(info.AuthIssuer) : undefined;\n if (!meta?.end_session_endpoint) {\n window.location.assign(postLogoutRedirectUri);\n return;\n }\n const params: Record<string, string> = {\n client_id: clientId,\n post_logout_redirect_uri: postLogoutRedirectUri,\n };\n if (idToken) params.id_token_hint = idToken;\n const url = new URL(meta.end_session_endpoint);\n url.search = new URLSearchParams(params).toString();\n window.location.assign(url.toString());\n}\n\n/** Discard the in-progress login memo (used on logout so a later login re-runs). */\nexport function resetPending(): void {\n pending = null;\n}\n","// Copyright (C) ConfigHub, Inc.\n// SPDX-License-Identifier: MIT\n\n// A module-level holder for the current minted token, so non-React consumers can read\n// it. RTK Query's `prepareHeaders` (in @confighub/rtk-query) is not a hook and cannot\n// read React context, so it calls getAccessToken() instead. The provider keeps this in\n// sync with its React state, and registers its 401 handling here for the same reason.\nimport { isExpired } from './core';\n\nlet currentToken: string | undefined;\nlet unauthorizedHandler: (() => void) | undefined;\n// Set while a recovery is under way, so a burst of failing requests starts one.\nlet recovering = false;\n\n/** @internal — called by the provider; not part of the public surface. */\nexport function setAccessToken(token: string | undefined): void {\n currentToken = token;\n if (token !== undefined) recovering = false;\n}\n\n/** @internal — the mounted provider's reaction to a rejected token. */\nexport function setUnauthorizedHandler(handler: (() => void) | undefined): void {\n unauthorizedHandler = handler;\n recovering = false;\n}\n\n/**\n * The current minted ConfigHub token, or undefined when unauthenticated. Pass this as\n * the `getToken` for `@confighub/rtk-query`'s `configureConfigHub`, or read it anywhere\n * you need the token outside React.\n *\n * A token past its `exp` is never handed out: the server would reject it. Instead the\n * provider's recovery starts (a silent re-authentication, or a logout, per its\n * `onUnauthorized`) and the request goes without a token; the page is about to\n * navigate to the IdP and back to where it was.\n */\nexport function getAccessToken(): string | undefined {\n if (currentToken !== undefined && isExpired(currentToken)) {\n handleUnauthorized();\n return undefined;\n }\n return currentToken;\n}\n\n/**\n * Tell the mounted provider the API rejected the token (401). Pass this as the\n * `onUnauthorized` for `@confighub/rtk-query`'s `configureConfigHub` to recover from a\n * token rejected before its expiry, e.g. after a server key rotation; expiry itself is\n * handled by `getAccessToken`. A no-op with no provider mounted, and while a recovery\n * is already under way.\n */\nexport function handleUnauthorized(): void {\n if (recovering || !unauthorizedHandler) return;\n recovering = true;\n unauthorizedHandler();\n}\n","// Copyright (C) ConfigHub, Inc.\n// SPDX-License-Identifier: MIT\n\nimport { createConfigHubClient, type ConfigHubClient } from '@confighub/api';\nimport {\n createContext,\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n type ReactNode,\n} from 'react';\nimport {\n OrganizationMissing,\n callbackUri,\n completeLoginFromRedirect,\n endSession,\n isExpired,\n organizationAliasOf,\n rememberOrganization,\n resetPending,\n startLogin,\n switchOrganization as switchOrganizationCore,\n type LoginOptions,\n type MintedSession,\n} from './core';\nimport { setAccessToken, setUnauthorizedHandler } from './tokenStore';\n\nexport type AuthStatus = 'loading' | 'authenticated' | 'unauthenticated' | 'error';\n\nexport interface ConfigHubUser {\n organizationId: string;\n idpClaims: Record<string, unknown>;\n}\n\nexport interface LogoutOptions {\n /**\n * Also end the IdP session (RP-initiated logout), so the next login asks for\n * credentials instead of riding the SSO cookie. Redirects the page; the landing\n * URI must be registered for the client. Default: false, which only forgets the\n * token in this tab.\n */\n endSession?: boolean;\n /** Where to land after IdP logout. Defaults to the callback URI. */\n postLogoutRedirectUri?: string;\n}\n\nexport interface ConfigHubAuthContextValue {\n status: AuthStatus;\n user: ConfigHubUser | null;\n error: Error | null;\n /** Begin login: redirects the page to the IdP. */\n login: (options?: LoginOptions) => Promise<void>;\n /** Forget the session in this tab and, optionally, end the IdP session too. */\n logout: (options?: LogoutOptions) => Promise<void>;\n /**\n * Re-mint the ConfigHub token for another organization the user belongs to. The\n * IdP session is untouched. Rejects with the server's error if the user is not a\n * member; the current session stays as it was.\n */\n switchOrganization: (organizationId: string) => Promise<void>;\n /**\n * The ConfigHub token stopped working (a 401). Try to get a new one without any\n * UI: a `prompt=none` round trip through the IdP, for the organization the session\n * already had. Status goes to `loading` meanwhile, not `unauthenticated`, so an\n * app that auto-logs-in on `unauthenticated` does not race this with an\n * interactive login. If the IdP session is gone too, the page comes back\n * `unauthenticated`. Redirects the page.\n */\n reauthenticate: () => Promise<void>;\n /** Current bearer token, or undefined when unauthenticated. */\n getToken: () => string | undefined;\n /** A typed API client pre-wired with the current token. Stable across renders. */\n client: ConfigHubClient;\n}\n\nexport const ConfigHubAuthContext = createContext<ConfigHubAuthContextValue | null>(null);\n\nexport interface ConfigHubAuthProviderProps {\n /** Absolute base URL of the ConfigHub instance, e.g. `https://hub.confighub.com`. */\n baseUrl: string;\n /** This app's registered OAuth `client_id` (from `cub oauthclient create`). */\n clientId: string;\n /**\n * Same-origin path the IdP redirects back to; `{origin}{callbackPath}` is the\n * redirect URI to register. Defaults to `/`. Fixed on purpose: the page a user\n * starts login from travels in the PKCE state, not in the redirect URI.\n */\n callbackPath?: string;\n /**\n * `'session'` (default) keeps the minted token in `sessionStorage` so a reload or an\n * in-tab navigation does not round-trip through the IdP. Tab-scoped and gone when the\n * tab closes. `'none'`: memory only, every page load starts unauthenticated.\n */\n persist?: 'none' | 'session';\n /**\n * What a 401 from the API means. `'login'` (default): the token is stale, try a\n * silent re-authentication (`prompt=none`) and fall back to unauthenticated if the\n * IdP session is gone too. `'logout'`: just drop the session.\n */\n onUnauthorized?: 'login' | 'logout';\n children: ReactNode;\n}\n\nconst SESSION_KEY = 'confighub_session';\n\nfunction readPersisted(): MintedSession | null {\n try {\n const raw = sessionStorage.getItem(SESSION_KEY);\n if (!raw) return null;\n const session: MintedSession = JSON.parse(raw);\n if (!session.accessToken || isExpired(session.accessToken)) {\n sessionStorage.removeItem(SESSION_KEY);\n return null;\n }\n return session;\n } catch {\n return null;\n }\n}\n\n/**\n * Runs the browser-direct auth flow and manages the token lifecycle. On mount it\n * completes a redirect if the page is the IdP callback, restores a persisted\n * session if there is one, and otherwise starts unauthenticated until `login()`\n * is called.\n */\nexport function ConfigHubAuthProvider({\n baseUrl,\n clientId,\n callbackPath,\n persist = 'session',\n onUnauthorized = 'login',\n children,\n}: ConfigHubAuthProviderProps): JSX.Element {\n const [status, setStatus] = useState<AuthStatus>('loading');\n const [user, setUser] = useState<ConfigHubUser | null>(null);\n const [error, setError] = useState<Error | null>(null);\n\n // The session lives in a ref so getToken() reads the latest value synchronously\n // without re-creating the API client on every render.\n const sessionRef = useRef<MintedSession | undefined>(undefined);\n const flow = useMemo(() => ({ callbackPath }), [callbackPath]);\n\n const persistSession = useCallback(\n (session: MintedSession | undefined) => {\n if (persist !== 'session') return;\n try {\n if (session) sessionStorage.setItem(SESSION_KEY, JSON.stringify(session));\n else sessionStorage.removeItem(SESSION_KEY);\n } catch {\n // Storage unavailable (private mode, quota): memory-only for this tab.\n }\n },\n [persist],\n );\n\n const applySession = useCallback(\n (session: MintedSession) => {\n sessionRef.current = session;\n setAccessToken(session.accessToken); // keep the non-React accessor in sync (rtk-query)\n persistSession(session);\n setUser({ organizationId: session.organizationId, idpClaims: session.idpClaims });\n setError(null);\n setStatus('authenticated');\n },\n [persistSession],\n );\n\n const clearSession = useCallback(() => {\n sessionRef.current = undefined;\n setAccessToken(undefined);\n persistSession(undefined);\n resetPending();\n setUser(null);\n setStatus('unauthenticated');\n }, [persistSession]);\n\n useEffect(() => {\n let cancelled = false;\n completeLoginFromRedirect()\n .then((session) => {\n if (cancelled) return;\n if (session) return applySession(session);\n const persisted = persist === 'session' ? readPersisted() : null;\n if (persisted) return applySession(persisted);\n setStatus('unauthenticated');\n })\n .catch((e: unknown) => {\n if (cancelled) return;\n if (e instanceof OrganizationMissing) {\n // Once, with no hint, flagged so a second miss surfaces as an error.\n void startLogin(\n baseUrl,\n clientId,\n { returnTo: e.returnTo, organization: null },\n flow,\n { retriedForOrganization: true },\n );\n return;\n }\n setError(e instanceof Error ? e : new Error(String(e)));\n setStatus('error');\n });\n return () => {\n cancelled = true;\n };\n }, [applySession, persist, baseUrl, clientId, flow]);\n\n const login = useCallback(\n async (options?: LoginOptions) => {\n setError(null);\n try {\n await startLogin(baseUrl, clientId, options, flow);\n } catch (e: unknown) {\n setError(e instanceof Error ? e : new Error(String(e)));\n setStatus('error');\n }\n },\n [baseUrl, clientId, flow],\n );\n\n const logout = useCallback(\n async (options?: LogoutOptions) => {\n const idToken = sessionRef.current?.idToken;\n clearSession();\n if (options?.endSession) {\n await endSession(\n baseUrl,\n clientId,\n idToken,\n options.postLogoutRedirectUri ?? callbackUri(flow),\n );\n }\n },\n [baseUrl, clientId, clearSession, flow],\n );\n\n const switchOrganization = useCallback(\n async (organizationId: string) => {\n const current = sessionRef.current;\n if (!current) throw new Error('not authenticated');\n const minted = await switchOrganizationCore(baseUrl, current.accessToken, organizationId);\n // The session's IdP claims still name the previous org, so its alias must not\n // be remembered as the default for the next login.\n rememberOrganization(clientId, undefined);\n applySession({ ...current, ...minted });\n },\n [applySession, baseUrl, clientId],\n );\n\n const getToken = useCallback(() => sessionRef.current?.accessToken, []);\n\n const reauthenticate = useCallback(async () => {\n const organization = organizationAliasOf(sessionRef.current?.idpClaims ?? {});\n // Forget the token but stay 'loading': the page is about to navigate away,\n // and 'unauthenticated' would invite an interactive login in the meantime.\n sessionRef.current = undefined;\n setAccessToken(undefined);\n persistSession(undefined);\n resetPending();\n setStatus('loading');\n try {\n await startLogin(baseUrl, clientId, { prompt: 'none', organization }, flow);\n } catch (e: unknown) {\n setError(e instanceof Error ? e : new Error(String(e)));\n setStatus('error');\n }\n }, [baseUrl, clientId, flow, persistSession]);\n\n // A 401 means the minted token no longer works. Silent re-auth keeps the user's\n // place if the IdP session is still alive; otherwise the page comes back\n // unauthenticated and the app offers a real login.\n const handleUnauthorized = useCallback(() => {\n if (!sessionRef.current) return;\n if (onUnauthorized === 'login') void reauthenticate();\n else clearSession();\n }, [clearSession, reauthenticate, onUnauthorized]);\n\n // Non-React callers (getAccessToken on an expired token, handleUnauthorized from a\n // data client's 401) reach this provider's handling through the token store.\n useEffect(() => {\n setUnauthorizedHandler(handleUnauthorized);\n return () => setUnauthorizedHandler(undefined);\n }, [handleUnauthorized]);\n\n // One client for the provider's lifetime. getToken reads the session ref.\n const client = useMemo(\n () => createConfigHubClient({ baseUrl, getToken, onUnauthorized: handleUnauthorized }),\n [baseUrl, getToken, handleUnauthorized],\n );\n\n const value = useMemo<ConfigHubAuthContextValue>(\n () => ({\n status,\n user,\n error,\n login,\n logout,\n switchOrganization,\n reauthenticate,\n getToken,\n client,\n }),\n [status, user, error, login, logout, switchOrganization, reauthenticate, getToken, client],\n );\n\n return (\n <ConfigHubAuthContext.Provider value={value}>{children}</ConfigHubAuthContext.Provider>\n );\n}\n","// Copyright (C) ConfigHub, Inc.\n// SPDX-License-Identifier: MIT\n\nimport type { ConfigHubClient } from '@confighub/api';\nimport { useContext } from 'react';\nimport { ConfigHubAuthContext, type ConfigHubAuthContextValue } from './provider';\n\n/**\n * Access the ConfigHub auth state and actions. Must be called under a\n * `<ConfigHubAuthProvider>`.\n *\n * ```ts\n * const { status, user, login, logout } = useAuth();\n * ```\n */\nexport function useAuth(): ConfigHubAuthContextValue {\n const ctx = useContext(ConfigHubAuthContext);\n if (!ctx) {\n throw new Error('useAuth must be used within a <ConfigHubAuthProvider>');\n }\n return ctx;\n}\n\n/**\n * The typed ConfigHub API client, pre-wired with the current token. This is the\n * seam between `@confighub/react-auth` and `@confighub/api`: you never pass a\n * token by hand.\n *\n * ```ts\n * const api = useConfigHub();\n * const { data } = await api.GET('/space/{space_id}/unit', { params: { path: { space_id } } });\n * ```\n */\nexport function useConfigHub(): ConfigHubClient {\n return useAuth().client;\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/core.ts","../src/tokenStore.ts","../src/provider.tsx","../src/hooks.ts"],"names":["handleUnauthorized"],"mappings":"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","file":"index.js","sourcesContent":["// Copyright (C) ConfigHub, Inc.\n// SPDX-License-Identifier: MIT\n//\n// Framework-neutral browser-auth engine for ConfigHub. Productionized from the\n// reference harness `test/browser-auth/src/confighubAuth.ts` in the ConfigHub\n// monorepo, which is validated end to end against staging and prod.\n//\n// Flow (design: third-party-browser-app-auth.md §6):\n// GET {base}/api/info -> discovery { AuthIssuer, TokenExchangeEndpoint }\n// OIDC discovery on AuthIssuer -> authorize/token/end_session endpoints\n// PKCE authorize + code->token -> IdP token\n// POST {TokenExchangeEndpoint} (8693) -> minted ConfigHub token\n//\n// The minted token then rides `Authorization: Bearer` against `/api`. The flow is\n// edition-agnostic: `AuthIssuer` is whatever discovery names (ConfigHub's bundled\n// Keycloak for Cloud, the org's own IdP for Enterprise), so the same code runs\n// against both. Tokens are held by the caller; only the transient PKCE state is\n// parked in sessionStorage across the authorize redirect.\n\nexport interface Discovery {\n AuthIssuer?: string;\n TokenExchangeEndpoint?: string;\n TokenExchangeAudience?: string;\n}\n\nexport interface MintedSession {\n accessToken: string;\n organizationId: string;\n /** Claims of the validated IdP token (owning-org, audience, organization shape). */\n idpClaims: Record<string, unknown>;\n /**\n * The IdP's ID token, kept only so logout can pass it as `id_token_hint` to the\n * end-session endpoint. Absent for sessions that did not come from an OIDC login.\n */\n idToken?: string;\n}\n\nexport interface LoginOptions {\n /**\n * Where to land after login, as a same-origin path (`/space/x?tab=units`). Carried\n * through the authorize round trip in the PKCE state, never in the redirect URI:\n * OAuth clients register exact redirect URIs, so the URI itself must not vary with\n * the page the user started from. Defaults to the current path and query.\n */\n returnTo?: string;\n /**\n * Which organization to sign in to, as the Keycloak organization alias sent in\n * the `organization:<alias>` scope.\n *\n * - a string: that organization, no prompt;\n * - `undefined` (default): the organization of the last successful login in this\n * browser, remembered per client in `localStorage`, so a new tab or a login after\n * logout lands in the same organization without a prompt; with nothing\n * remembered, Keycloak decides (prompt for a multi-org user, or the org matching\n * the email domain on a fresh authentication);\n * - `null`: no hint on purpose, so Keycloak prompts. This is \"switch organization\".\n */\n organization?: string | null;\n /**\n * `'none'` asks the IdP to re-authenticate without any UI, failing with\n * `login_required` if the SSO session is gone -- the way to refresh an expired\n * ConfigHub token when the user is still signed in at the IdP. `'login'` forces the\n * login form even with a live SSO session. `'create'` opens the IdP's registration\n * form instead of its login form (OpenID Connect Prompt Create; Keycloak honours it),\n * so a \"sign up\" link can be an ordinary PKCE login that lands the new user in the\n * app signed in.\n */\n prompt?: 'none' | 'login' | 'create';\n}\n\nexport interface FlowOptions {\n /**\n * Same-origin path the IdP redirects back to, and therefore the redirect URI to\n * register for the client: `{origin}{callbackPath}`. Defaults to `/`.\n */\n callbackPath?: string;\n}\n\ninterface PkceState {\n verifier: string;\n state: string;\n clientId: string;\n tokenEndpoint: string;\n exchangeEndpoint: string;\n redirectUri: string;\n returnTo: string;\n silent: boolean;\n /** The organization alias the authorize request hinted, if any. */\n hintedOrganization?: string;\n /** This login is already the retry after a token without an organization. */\n retriedForOrganization?: boolean;\n}\n\n/**\n * Thrown by `completeLoginFromRedirect` when the IdP token names no organization,\n * which the exchange would refuse. Seen on a fresh brokered (Google) login, where\n * Keycloak's organization step does not run; on the next login the SSO session is\n * alive and it does, so the caller logs in again, once, with no hint. Any\n * remembered alias has already been forgotten.\n */\nexport class OrganizationMissing extends Error {\n constructor(public readonly returnTo: string) {\n super('the identity provider issued a token with no organization');\n this.name = 'OrganizationMissing';\n }\n}\n\n/**\n * Thrown by `startLogin` when the instance advertises no identity provider. Such an\n * instance signs a browser in with a ticket from `cub auth browser-session` instead\n * (see `redeemBrowserTicket`), so there is no login to start.\n */\nexport class NoIdentityProvider extends Error {\n constructor() {\n super('this instance has no identity provider; sign in with `cub auth browser-session`');\n this.name = 'NoIdentityProvider';\n }\n}\n\nexport interface RetryOptions {\n /** @internal set by the provider on the one retry after OrganizationMissing. */\n retriedForOrganization?: boolean;\n}\n\nconst PKCE_KEY = 'confighub_pkce';\nconst LAST_ORG_KEY = 'confighub_last_org';\n\n// The alias is a short public identifier, not a credential, so localStorage is the\n// right place: it must outlive the tab, which is exactly what the token must not.\nconst lastOrgKey = (clientId: string): string => `${LAST_ORG_KEY}:${clientId}`;\n\n/** The organization alias of the last successful login for this client, if any. */\nexport function rememberedOrganization(clientId: string): string | undefined {\n try {\n return localStorage.getItem(lastOrgKey(clientId)) ?? undefined;\n } catch {\n return undefined;\n }\n}\n\n/** @internal */\nexport function rememberOrganization(clientId: string, alias: string | undefined): void {\n try {\n if (alias) localStorage.setItem(lastOrgKey(clientId), alias);\n else localStorage.removeItem(lastOrgKey(clientId));\n } catch {\n // Storage unavailable: the next login gets no hint.\n }\n}\n\n/**\n * The alias in an IdP token's `organization` claim (`{ \"<alias>\": { id } }`), or\n * undefined when the claim is absent or names more than one organization.\n */\nexport function organizationAliasOf(idpClaims: Record<string, unknown>): string | undefined {\n const org = idpClaims.organization;\n if (!org || typeof org !== 'object') return undefined;\n const aliases = Object.keys(org as Record<string, unknown>);\n return aliases.length === 1 ? aliases[0] : undefined;\n}\n\nconst trimSlash = (s: string): string => s.replace(/\\/+$/, '');\n\n/** The fixed callback URI: the page origin plus the configured callback path. */\nexport const callbackUri = (opts?: FlowOptions): string =>\n window.location.origin + (opts?.callbackPath ?? '/');\n\nconst currentPath = (): string =>\n window.location.pathname + window.location.search + window.location.hash;\n\nconst b64url = (buf: ArrayBuffer): string =>\n btoa(String.fromCharCode(...new Uint8Array(buf)))\n .replace(/\\+/g, '-')\n .replace(/\\//g, '_')\n .replace(/=+$/, '');\n\nconst randomString = (n = 64): string =>\n b64url(crypto.getRandomValues(new Uint8Array(n)).buffer);\n\nasync function sha256(s: string): Promise<string> {\n return b64url(await crypto.subtle.digest('SHA-256', new TextEncoder().encode(s)));\n}\n\n/** Decode a JWT's claims without verifying it. Returns {} for anything malformed. */\nexport function decodeJwtClaims(token: string): Record<string, unknown> {\n const part = token.split('.')[1];\n if (!part) return {};\n try {\n return JSON.parse(atob(part.replace(/-/g, '+').replace(/_/g, '/')));\n } catch {\n return {};\n }\n}\n\n/** Whether a JWT's `exp` is in the past (with a small skew allowance). */\nexport function isExpired(token: string, skewSeconds = 30): boolean {\n const exp = decodeJwtClaims(token).exp;\n if (typeof exp !== 'number') return false;\n return exp * 1000 <= Date.now() + skewSeconds * 1000;\n}\n\nexport async function discover(base: string): Promise<Discovery> {\n const r = await fetch(trimSlash(base) + '/api/info');\n if (!r.ok) throw new Error('/api/info failed: ' + r.status);\n return r.json();\n}\n\ninterface OidcMetadata {\n authorization_endpoint: string;\n token_endpoint: string;\n end_session_endpoint?: string;\n}\n\nasync function oidcMetadata(issuer: string): Promise<OidcMetadata> {\n const r = await fetch(trimSlash(issuer) + '/.well-known/openid-configuration');\n if (!r.ok) throw new Error('OIDC discovery failed: ' + r.status);\n return r.json();\n}\n\n/**\n * Discover, build a PKCE request, and navigate to the IdP authorize endpoint.\n * Returns only by redirecting the page; `completeLoginFromRedirect()` finishes on\n * the way back.\n */\nexport async function startLogin(\n base: string,\n clientId: string,\n login: LoginOptions = {},\n flow: FlowOptions = {},\n retry: RetryOptions = {},\n): Promise<void> {\n const info = await discover(base);\n if (!info.AuthIssuer && !info.TokenExchangeEndpoint) throw new NoIdentityProvider();\n if (!info.AuthIssuer || !info.TokenExchangeEndpoint) {\n throw new Error(\n 'this instance is not configured for token-exchange auth (server needs CONFIGHUB_IDP_ISSUER)',\n );\n }\n const meta = await oidcMetadata(info.AuthIssuer);\n const verifier = randomString();\n const challenge = await sha256(verifier);\n const state = randomString(16);\n const redirectUri = callbackUri(flow);\n // The \"organization\" scope makes Keycloak emit the org claim the exchange resolves;\n // \"organization:<alias>\" selects one without prompting.\n const alias =\n login.organization === null\n ? undefined\n : (login.organization ?? rememberedOrganization(clientId));\n const orgScope = alias ? `organization:${alias}` : 'organization';\n const pkce: PkceState = {\n verifier,\n state,\n clientId,\n tokenEndpoint: meta.token_endpoint,\n exchangeEndpoint: info.TokenExchangeEndpoint,\n redirectUri,\n returnTo: login.returnTo ?? currentPath(),\n silent: login.prompt === 'none',\n hintedOrganization: alias,\n retriedForOrganization: retry.retriedForOrganization,\n };\n sessionStorage.setItem(PKCE_KEY, JSON.stringify(pkce));\n const params: Record<string, string> = {\n response_type: 'code',\n client_id: clientId,\n redirect_uri: redirectUri,\n scope: `openid email profile ${orgScope}`,\n code_challenge: challenge,\n code_challenge_method: 'S256',\n state,\n };\n if (login.prompt) params.prompt = login.prompt;\n\n const authURL = new URL(meta.authorization_endpoint);\n authURL.search = new URLSearchParams(params).toString();\n window.location.assign(authURL.toString());\n}\n\n// Memoize so React StrictMode's double-mount can't redeem the one-time code twice.\nlet pending: Promise<MintedSession | null> | null = null;\n\n/**\n * If the page is the IdP redirect (`?code=...`), exchange the code for an IdP token\n * and then exchange that for a minted ConfigHub token, and restore the URL the\n * login started from. Returns null on a normal load, when a `prompt=none` attempt\n * came back with `login_required` (the SSO session is gone; the caller should offer\n * an interactive login), and when the redirect carries a code this tab has no PKCE\n * state for (an IdP email flow landed here; the caller should start a login).\n */\nexport function completeLoginFromRedirect(): Promise<MintedSession | null> {\n if (!pending) pending = doCompleteLogin();\n return pending;\n}\n\nconst SILENT_FAILURES = new Set(['login_required', 'interaction_required', 'consent_required']);\n\nasync function doCompleteLogin(): Promise<MintedSession | null> {\n const params = new URLSearchParams(window.location.search);\n const code = params.get('code');\n const error = params.get('error');\n if (!code && !error) return null;\n\n const savedRaw = sessionStorage.getItem(PKCE_KEY);\n sessionStorage.removeItem(PKCE_KEY);\n const saved: PkceState | null = savedRaw ? JSON.parse(savedRaw) : null;\n\n // Put the URL back to where the user started before anything else can fail, so\n // neither the code nor an error string lingers in the address bar or history.\n history.replaceState({}, '', saved?.returnTo ?? callbackUri());\n\n if (error) {\n if (saved?.silent && SILENT_FAILURES.has(error)) return null;\n throw new Error(`IdP returned error: ${error} ${params.get('error_description') ?? ''}`);\n }\n // A code with no PKCE state means this tab did not start the login: the IdP's\n // email flows (password reset, verify email) finish the authentication in the tab\n // the mail link opened and redirect it here, while the verifier sits in the tab\n // that began. The code is unusable without it, so drop it and report a normal\n // load; the caller starts a login, which the now-live IdP session completes\n // without a prompt.\n if (!saved) return null;\n if (params.get('state') !== saved.state) throw new Error('state mismatch; aborting');\n\n // Exchange the authorization code for an IdP token (PKCE, public client).\n const tokenResp = await fetch(saved.tokenEndpoint, {\n method: 'POST',\n headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\n body: new URLSearchParams({\n grant_type: 'authorization_code',\n code: code!,\n redirect_uri: saved.redirectUri,\n client_id: saved.clientId,\n code_verifier: saved.verifier,\n }),\n });\n if (!tokenResp.ok) {\n throw new Error(`IdP token endpoint ${tokenResp.status}: ${await tokenResp.text()}`);\n }\n const idpToken = await tokenResp.json();\n\n // RFC 8693 token exchange against ConfigHub -> minted ConfigHub token.\n const idpClaims = decodeJwtClaims(idpToken.access_token);\n if (!organizationAliasOf(idpClaims) && !saved.retriedForOrganization) {\n // The exchange would refuse this token. Rather than surface that, log in\n // once more: with the SSO session now alive the IdP runs its organization\n // step. A hint that was sent evidently did not help, so forget it.\n if (saved.hintedOrganization) rememberOrganization(saved.clientId, undefined);\n throw new OrganizationMissing(saved.returnTo);\n }\n const minted = await exchange(saved.exchangeEndpoint, idpToken.access_token);\n rememberOrganization(saved.clientId, organizationAliasOf(idpClaims));\n return {\n ...minted,\n idpClaims,\n idToken: typeof idpToken.id_token === 'string' ? idpToken.id_token : undefined,\n };\n}\n\nasync function exchange(\n exchangeEndpoint: string,\n subjectToken: string,\n): Promise<Pick<MintedSession, 'accessToken' | 'organizationId'>> {\n const exResp = await fetch(exchangeEndpoint, {\n method: 'POST',\n headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\n body: new URLSearchParams({\n grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange',\n subject_token: subjectToken,\n subject_token_type: 'urn:ietf:params:oauth:token-type:access_token',\n }),\n });\n if (!exResp.ok) throw new Error(`/auth/exchange ${exResp.status}: ${await exResp.text()}`);\n const minted = await exResp.json();\n return { accessToken: minted.access_token, organizationId: minted.organization_id };\n}\n\n/**\n * Redeem a single-use ticket from `cub auth browser-session` for a minted ConfigHub\n * token (`POST {base}/auth/browser-session`). This is how a browser signs in to an\n * instance with no identity provider, and how a local identity (a break-glass\n * administrator) signs in to one that has a provider. The session has no IdP behind\n * it: when the token expires, the user runs the command again.\n */\nexport async function redeemBrowserTicket(base: string, ticket: string): Promise<MintedSession> {\n const r = await fetch(trimSlash(base) + '/auth/browser-session', {\n method: 'POST',\n headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\n body: new URLSearchParams({ ticket }),\n });\n if (!r.ok) throw new Error(`/auth/browser-session ${r.status}: ${await r.text()}`);\n const minted = await r.json();\n return { accessToken: minted.access_token, organizationId: minted.organization_id, idpClaims: {} };\n}\n\n/**\n * End the IdP session (RP-initiated logout) and land on `postLogoutRedirectUri`,\n * which must be registered for the client. Returns only by redirecting. If the\n * issuer publishes no end-session endpoint, navigates to the redirect URI directly.\n */\nexport async function endSession(\n base: string,\n clientId: string,\n idToken: string | undefined,\n postLogoutRedirectUri: string,\n): Promise<void> {\n const info = await discover(base);\n const meta = info.AuthIssuer ? await oidcMetadata(info.AuthIssuer) : undefined;\n if (!meta?.end_session_endpoint) {\n window.location.assign(postLogoutRedirectUri);\n return;\n }\n const params: Record<string, string> = {\n client_id: clientId,\n post_logout_redirect_uri: postLogoutRedirectUri,\n };\n if (idToken) params.id_token_hint = idToken;\n const url = new URL(meta.end_session_endpoint);\n url.search = new URLSearchParams(params).toString();\n window.location.assign(url.toString());\n}\n\n/** Discard the in-progress login memo (used on logout so a later login re-runs). */\nexport function resetPending(): void {\n pending = null;\n}\n","// Copyright (C) ConfigHub, Inc.\n// SPDX-License-Identifier: MIT\n\n// A module-level holder for the current minted token, so non-React consumers can read\n// it. RTK Query's `prepareHeaders` (in @confighub/rtk-query) is not a hook and cannot\n// read React context, so it calls getAccessToken() instead. The provider keeps this in\n// sync with its React state, and registers its 401 handling here for the same reason.\nimport { isExpired } from './core';\n\nlet currentToken: string | undefined;\nlet unauthorizedHandler: (() => void) | undefined;\n// Set while a recovery is under way, so a burst of failing requests starts one.\nlet recovering = false;\n\n/** @internal — called by the provider; not part of the public surface. */\nexport function setAccessToken(token: string | undefined): void {\n currentToken = token;\n if (token !== undefined) recovering = false;\n}\n\n/** @internal — the mounted provider's reaction to a rejected token. */\nexport function setUnauthorizedHandler(handler: (() => void) | undefined): void {\n unauthorizedHandler = handler;\n recovering = false;\n}\n\n/**\n * The current minted ConfigHub token, or undefined when unauthenticated. Pass this as\n * the `getToken` for `@confighub/rtk-query`'s `configureConfigHub`, or read it anywhere\n * you need the token outside React.\n *\n * A token past its `exp` is never handed out: the server would reject it. Instead the\n * provider's recovery starts (a silent re-authentication, or a logout, per its\n * `onUnauthorized`) and the request goes without a token; the page is about to\n * navigate to the IdP and back to where it was.\n */\nexport function getAccessToken(): string | undefined {\n if (currentToken !== undefined && isExpired(currentToken)) {\n handleUnauthorized();\n return undefined;\n }\n return currentToken;\n}\n\n/**\n * Tell the mounted provider the API rejected the token (401). Pass this as the\n * `onUnauthorized` for `@confighub/rtk-query`'s `configureConfigHub` to recover from a\n * token rejected before its expiry, e.g. after a server key rotation; expiry itself is\n * handled by `getAccessToken`. A no-op with no provider mounted, and while a recovery\n * is already under way.\n */\nexport function handleUnauthorized(): void {\n if (recovering || !unauthorizedHandler) return;\n recovering = true;\n unauthorizedHandler();\n}\n","// Copyright (C) ConfigHub, Inc.\n// SPDX-License-Identifier: MIT\n\nimport { createConfigHubClient, type ConfigHubClient } from '@confighub/api';\nimport {\n createContext,\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n type ReactNode,\n} from 'react';\nimport {\n NoIdentityProvider,\n OrganizationMissing,\n callbackUri,\n completeLoginFromRedirect,\n endSession,\n isExpired,\n organizationAliasOf,\n redeemBrowserTicket,\n resetPending,\n startLogin,\n type LoginOptions,\n type MintedSession,\n} from './core';\nimport { setAccessToken, setUnauthorizedHandler } from './tokenStore';\n\nexport type AuthStatus = 'loading' | 'authenticated' | 'unauthenticated' | 'error';\n\nexport interface ConfigHubUser {\n organizationId: string;\n idpClaims: Record<string, unknown>;\n}\n\nexport interface LogoutOptions {\n /**\n * Also end the IdP session (RP-initiated logout), so the next login asks for\n * credentials instead of riding the SSO cookie. Redirects the page; the landing\n * URI must be registered for the client. Default: false, which only forgets the\n * token in this tab.\n */\n endSession?: boolean;\n /** Where to land after IdP logout. Defaults to the callback URI. */\n postLogoutRedirectUri?: string;\n}\n\nexport interface ConfigHubAuthContextValue {\n status: AuthStatus;\n user: ConfigHubUser | null;\n error: Error | null;\n /** Begin login: redirects the page to the IdP. */\n login: (options?: LoginOptions) => Promise<void>;\n /** Forget the session in this tab and, optionally, end the IdP session too. */\n logout: (options?: LogoutOptions) => Promise<void>;\n /**\n * The ConfigHub token stopped working (a 401). Try to get a new one without any\n * UI: a `prompt=none` round trip through the IdP, for the organization the session\n * already had. Status goes to `loading` meanwhile, not `unauthenticated`, so an\n * app that auto-logs-in on `unauthenticated` does not race this with an\n * interactive login. If the IdP session is gone too, the page comes back\n * `unauthenticated`. Redirects the page.\n */\n reauthenticate: () => Promise<void>;\n /**\n * Sign in with a ticket from `cub auth browser-session`, for an instance with no\n * identity provider or a local identity. Rejects with the server's error if the\n * ticket is unknown, expired or already used; the session stays as it was.\n */\n signInWithTicket: (ticket: string) => Promise<void>;\n /** Current bearer token, or undefined when unauthenticated. */\n getToken: () => string | undefined;\n /** A typed API client pre-wired with the current token. Stable across renders. */\n client: ConfigHubClient;\n}\n\nexport const ConfigHubAuthContext = createContext<ConfigHubAuthContextValue | null>(null);\n\nexport interface ConfigHubAuthProviderProps {\n /** Absolute base URL of the ConfigHub instance, e.g. `https://hub.confighub.com`. */\n baseUrl: string;\n /** This app's registered OAuth `client_id` (from `cub oauthclient create`). */\n clientId: string;\n /**\n * Same-origin path the IdP redirects back to; `{origin}{callbackPath}` is the\n * redirect URI to register. Defaults to `/`. Fixed on purpose: the page a user\n * starts login from travels in the PKCE state, not in the redirect URI.\n */\n callbackPath?: string;\n /**\n * `'session'` (default) keeps the minted token in `sessionStorage` so a reload or an\n * in-tab navigation does not round-trip through the IdP. Tab-scoped and gone when the\n * tab closes. `'none'`: memory only, every page load starts unauthenticated.\n */\n persist?: 'none' | 'session';\n /**\n * What a 401 from the API means. `'login'` (default): the token is stale, try a\n * silent re-authentication (`prompt=none`) and fall back to unauthenticated if the\n * IdP session is gone too. `'logout'`: just drop the session.\n */\n onUnauthorized?: 'login' | 'logout';\n children: ReactNode;\n}\n\nconst SESSION_KEY = 'confighub_session';\n\nfunction readPersisted(): MintedSession | null {\n try {\n const raw = sessionStorage.getItem(SESSION_KEY);\n if (!raw) return null;\n const session: MintedSession = JSON.parse(raw);\n if (!session.accessToken || isExpired(session.accessToken)) {\n sessionStorage.removeItem(SESSION_KEY);\n return null;\n }\n return session;\n } catch {\n return null;\n }\n}\n\n/**\n * Runs the browser-direct auth flow and manages the token lifecycle. On mount it\n * completes a redirect if the page is the IdP callback, restores a persisted\n * session if there is one, and otherwise starts unauthenticated until `login()`\n * is called.\n */\nexport function ConfigHubAuthProvider({\n baseUrl,\n clientId,\n callbackPath,\n persist = 'session',\n onUnauthorized = 'login',\n children,\n}: ConfigHubAuthProviderProps): JSX.Element {\n const [status, setStatus] = useState<AuthStatus>('loading');\n const [user, setUser] = useState<ConfigHubUser | null>(null);\n const [error, setError] = useState<Error | null>(null);\n\n // The session lives in a ref so getToken() reads the latest value synchronously\n // without re-creating the API client on every render.\n const sessionRef = useRef<MintedSession | undefined>(undefined);\n const flow = useMemo(() => ({ callbackPath }), [callbackPath]);\n\n const persistSession = useCallback(\n (session: MintedSession | undefined) => {\n if (persist !== 'session') return;\n try {\n if (session) sessionStorage.setItem(SESSION_KEY, JSON.stringify(session));\n else sessionStorage.removeItem(SESSION_KEY);\n } catch {\n // Storage unavailable (private mode, quota): memory-only for this tab.\n }\n },\n [persist],\n );\n\n const applySession = useCallback(\n (session: MintedSession) => {\n sessionRef.current = session;\n setAccessToken(session.accessToken); // keep the non-React accessor in sync (rtk-query)\n persistSession(session);\n setUser({ organizationId: session.organizationId, idpClaims: session.idpClaims });\n setError(null);\n setStatus('authenticated');\n },\n [persistSession],\n );\n\n const clearSession = useCallback(() => {\n sessionRef.current = undefined;\n setAccessToken(undefined);\n persistSession(undefined);\n resetPending();\n setUser(null);\n setStatus('unauthenticated');\n }, [persistSession]);\n\n useEffect(() => {\n let cancelled = false;\n completeLoginFromRedirect()\n .then((session) => {\n if (cancelled) return;\n if (session) return applySession(session);\n const persisted = persist === 'session' ? readPersisted() : null;\n if (persisted) return applySession(persisted);\n setStatus('unauthenticated');\n })\n .catch((e: unknown) => {\n if (cancelled) return;\n if (e instanceof OrganizationMissing) {\n // Once, with no hint, flagged so a second miss surfaces as an error.\n void startLogin(\n baseUrl,\n clientId,\n { returnTo: e.returnTo, organization: null },\n flow,\n { retriedForOrganization: true },\n );\n return;\n }\n setError(e instanceof Error ? e : new Error(String(e)));\n setStatus('error');\n });\n return () => {\n cancelled = true;\n };\n }, [applySession, persist, baseUrl, clientId, flow]);\n\n const login = useCallback(\n async (options?: LoginOptions) => {\n setError(null);\n try {\n await startLogin(baseUrl, clientId, options, flow);\n } catch (e: unknown) {\n setError(e instanceof Error ? e : new Error(String(e)));\n setStatus('error');\n }\n },\n [baseUrl, clientId, flow],\n );\n\n const logout = useCallback(\n async (options?: LogoutOptions) => {\n const idToken = sessionRef.current?.idToken;\n if (!options?.endSession) {\n clearSession();\n return;\n }\n // Forget the session but stay 'loading': the page is about to navigate to the\n // IdP's end-session endpoint, and 'unauthenticated' would let an app that\n // auto-logs-in start a login that races the logout and can win it, landing\n // the user back in the session they just ended.\n sessionRef.current = undefined;\n setAccessToken(undefined);\n persistSession(undefined);\n resetPending();\n setUser(null);\n setStatus('loading');\n try {\n await endSession(\n baseUrl,\n clientId,\n idToken,\n options.postLogoutRedirectUri ?? callbackUri(flow),\n );\n } catch (e: unknown) {\n // The session in this tab is already gone; say why the IdP's is not.\n setError(e instanceof Error ? e : new Error(String(e)));\n setStatus('error');\n }\n },\n [baseUrl, clientId, clearSession, flow, persistSession],\n );\n\n const getToken = useCallback(() => sessionRef.current?.accessToken, []);\n\n const reauthenticate = useCallback(async () => {\n const organization = organizationAliasOf(sessionRef.current?.idpClaims ?? {});\n // Forget the token but stay 'loading': the page is about to navigate away,\n // and 'unauthenticated' would invite an interactive login in the meantime.\n sessionRef.current = undefined;\n setAccessToken(undefined);\n persistSession(undefined);\n resetPending();\n setStatus('loading');\n try {\n await startLogin(baseUrl, clientId, { prompt: 'none', organization }, flow);\n } catch (e: unknown) {\n // No IdP to ask: the session came from a ticket, and the user needs a new one.\n if (e instanceof NoIdentityProvider) {\n setStatus('unauthenticated');\n return;\n }\n setError(e instanceof Error ? e : new Error(String(e)));\n setStatus('error');\n }\n }, [baseUrl, clientId, flow, persistSession]);\n\n const signInWithTicket = useCallback(\n async (ticket: string) => {\n applySession(await redeemBrowserTicket(baseUrl, ticket));\n },\n [applySession, baseUrl],\n );\n\n // A 401 means the minted token no longer works. Silent re-auth keeps the user's\n // place if the IdP session is still alive; otherwise the page comes back\n // unauthenticated and the app offers a real login.\n const handleUnauthorized = useCallback(() => {\n if (!sessionRef.current) return;\n if (onUnauthorized === 'login') void reauthenticate();\n else clearSession();\n }, [clearSession, reauthenticate, onUnauthorized]);\n\n // Non-React callers (getAccessToken on an expired token, handleUnauthorized from a\n // data client's 401) reach this provider's handling through the token store.\n useEffect(() => {\n setUnauthorizedHandler(handleUnauthorized);\n return () => setUnauthorizedHandler(undefined);\n }, [handleUnauthorized]);\n\n // One client for the provider's lifetime. getToken reads the session ref.\n const client = useMemo(\n () => createConfigHubClient({ baseUrl, getToken, onUnauthorized: handleUnauthorized }),\n [baseUrl, getToken, handleUnauthorized],\n );\n\n const value = useMemo<ConfigHubAuthContextValue>(\n () => ({\n status,\n user,\n error,\n login,\n logout,\n reauthenticate,\n signInWithTicket,\n getToken,\n client,\n }),\n [status, user, error, login, logout, reauthenticate, signInWithTicket, getToken, client],\n );\n\n return (\n <ConfigHubAuthContext.Provider value={value}>{children}</ConfigHubAuthContext.Provider>\n );\n}\n","// Copyright (C) ConfigHub, Inc.\n// SPDX-License-Identifier: MIT\n\nimport type { ConfigHubClient } from '@confighub/api';\nimport { useContext } from 'react';\nimport { ConfigHubAuthContext, type ConfigHubAuthContextValue } from './provider';\n\n/**\n * Access the ConfigHub auth state and actions. Must be called under a\n * `<ConfigHubAuthProvider>`.\n *\n * ```ts\n * const { status, user, login, logout } = useAuth();\n * ```\n */\nexport function useAuth(): ConfigHubAuthContextValue {\n const ctx = useContext(ConfigHubAuthContext);\n if (!ctx) {\n throw new Error('useAuth must be used within a <ConfigHubAuthProvider>');\n }\n return ctx;\n}\n\n/**\n * The typed ConfigHub API client, pre-wired with the current token. This is the\n * seam between `@confighub/react-auth` and `@confighub/api`: you never pass a\n * token by hand.\n *\n * ```ts\n * const api = useConfigHub();\n * const { data } = await api.GET('/space/{space_id}/unit', { params: { path: { space_id } } });\n * ```\n */\nexport function useConfigHub(): ConfigHubClient {\n return useAuth().client;\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@confighub/react-auth",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"description": "React auth provider and hooks for ConfigHub browser apps (OIDC PKCE + RFC 8693 token exchange)",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"prepublishOnly": "npm run build"
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@confighub/api": "^0.
|
|
42
|
+
"@confighub/api": "^0.6.1"
|
|
43
43
|
},
|
|
44
44
|
"peerDependencies": {
|
|
45
45
|
"react": "^18.0.0 || ^19.0.0"
|