- oidc/oidcState.ts (pure, +3 tests): dynamic-registration cache (by issuer + redirectUri, corrupt-tolerant) and parseOidcCallbackParams (success/error/invalid). - oidc/oidcLoginUtil.ts: getOrRegisterClientId (cache + registerOidcClient) and startOidcLogin (discoverAndValidateOIDCIssuerWellKnown -> generateOidcAuthorization Url -> redirect; invalidates the cache on failure). redirectUri is the deterministic getOidcCallbackUrl(), and the SDK returns clientId/issuer on callback, so no hand-rolled transient state is needed. - login/OidcLogin.tsx: native-OIDC button mirroring SSOLogin + TokenLogin async/error. - login/Login.tsx: issuer-gated — when discovery advertises an issuer, render OidcLogin and suppress password/legacy-SSO; non-OIDC servers unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
61 lines
2.2 KiB
TypeScript
61 lines
2.2 KiB
TypeScript
// Pure OIDC-login state helpers (no imports) so they're unit-testable under the
|
|
// tsx + node:test harness. The SDK-driven flow lives in oidcLoginUtil.ts.
|
|
|
|
/**
|
|
* Dynamic-registration cache. MAS issues a `client_id` per dynamic registration;
|
|
* caching by issuer (scoped to the current redirectUri) avoids re-registering on
|
|
* every login. Invalidated when the redirectUri changes or the provider later
|
|
* rejects the cached id (`invalid_client`).
|
|
*/
|
|
const REG_CACHE_KEY = 'cinny_oidc_dynamic_clients';
|
|
type RegEntry = { clientId: string; redirectUri: string };
|
|
type RegCache = Record<string, RegEntry>;
|
|
|
|
const readRegCache = (): RegCache => {
|
|
try {
|
|
const raw = localStorage.getItem(REG_CACHE_KEY);
|
|
return raw ? (JSON.parse(raw) as RegCache) : {};
|
|
} catch {
|
|
return {};
|
|
}
|
|
};
|
|
const writeRegCache = (cache: RegCache): void => {
|
|
localStorage.setItem(REG_CACHE_KEY, JSON.stringify(cache));
|
|
};
|
|
|
|
export const getCachedClientId = (issuer: string, redirectUri: string): string | undefined => {
|
|
const entry = readRegCache()[issuer];
|
|
return entry && entry.redirectUri === redirectUri ? entry.clientId : undefined;
|
|
};
|
|
export const cacheClientId = (issuer: string, clientId: string, redirectUri: string): void => {
|
|
const cache = readRegCache();
|
|
cache[issuer] = { clientId, redirectUri };
|
|
writeRegCache(cache);
|
|
};
|
|
export const invalidateCachedClient = (issuer: string): void => {
|
|
const cache = readRegCache();
|
|
if (cache[issuer]) {
|
|
delete cache[issuer];
|
|
writeRegCache(cache);
|
|
}
|
|
};
|
|
|
|
/** Parsed shape of the provider's redirect back to our callback URL. */
|
|
export type OidcCallbackParams =
|
|
| { kind: 'success'; code: string; state: string }
|
|
| { kind: 'error'; error: string; errorDescription?: string }
|
|
| { kind: 'invalid' };
|
|
|
|
/** Pure: classify the callback query string into success / error / invalid. */
|
|
export const parseOidcCallbackParams = (search: string): OidcCallbackParams => {
|
|
const params = new URLSearchParams(search);
|
|
const error = params.get('error');
|
|
if (error) {
|
|
return { kind: 'error', error, errorDescription: params.get('error_description') ?? undefined };
|
|
}
|
|
const code = params.get('code');
|
|
const state = params.get('state');
|
|
if (code && state) return { kind: 'success', code, state };
|
|
return { kind: 'invalid' };
|
|
};
|