- 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>
63 lines
2.1 KiB
TypeScript
63 lines
2.1 KiB
TypeScript
import { test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import {
|
|
getCachedClientId,
|
|
cacheClientId,
|
|
invalidateCachedClient,
|
|
parseOidcCallbackParams,
|
|
} from './oidcState';
|
|
|
|
const installStorage = (): Map<string, string> => {
|
|
const store = new Map<string, string>();
|
|
(globalThis as { localStorage?: unknown }).localStorage = {
|
|
getItem: (k: string) => (store.has(k) ? store.get(k) : null),
|
|
setItem: (k: string, v: string) => {
|
|
store.set(k, String(v));
|
|
},
|
|
removeItem: (k: string) => {
|
|
store.delete(k);
|
|
},
|
|
};
|
|
return store;
|
|
};
|
|
|
|
test('registration cache: get / put / invalidate, scoped by issuer + redirectUri', () => {
|
|
installStorage();
|
|
assert.equal(getCachedClientId('iss', 'rd'), undefined);
|
|
cacheClientId('iss', 'client-1', 'rd');
|
|
assert.equal(getCachedClientId('iss', 'rd'), 'client-1');
|
|
assert.equal(getCachedClientId('iss', 'other-redirect'), undefined); // redirect mismatch = miss
|
|
assert.equal(getCachedClientId('iss2', 'rd'), undefined); // different issuer = miss
|
|
invalidateCachedClient('iss');
|
|
assert.equal(getCachedClientId('iss', 'rd'), undefined);
|
|
invalidateCachedClient('absent'); // no-op, no throw
|
|
});
|
|
|
|
test('registration cache tolerates corrupt storage', () => {
|
|
const store = installStorage();
|
|
store.set('cinny_oidc_dynamic_clients', '{ not json');
|
|
assert.equal(getCachedClientId('iss', 'rd'), undefined);
|
|
cacheClientId('iss', 'c', 'rd');
|
|
assert.equal(getCachedClientId('iss', 'rd'), 'c');
|
|
});
|
|
|
|
test('parseOidcCallbackParams classifies success / error / invalid', () => {
|
|
assert.deepEqual(parseOidcCallbackParams('?code=abc&state=xyz'), {
|
|
kind: 'success',
|
|
code: 'abc',
|
|
state: 'xyz',
|
|
});
|
|
assert.deepEqual(parseOidcCallbackParams('?error=access_denied&error_description=nope'), {
|
|
kind: 'error',
|
|
error: 'access_denied',
|
|
errorDescription: 'nope',
|
|
});
|
|
assert.deepEqual(parseOidcCallbackParams('?error=bad'), {
|
|
kind: 'error',
|
|
error: 'bad',
|
|
errorDescription: undefined,
|
|
});
|
|
assert.deepEqual(parseOidcCallbackParams('?code=only'), { kind: 'invalid' });
|
|
assert.deepEqual(parseOidcCallbackParams(''), { kind: 'invalid' });
|
|
});
|