oidc-caep-client
An OpenID Connect client library for browser single-page apps. It runs entirely in the page as a public client, with no client secret and no server holding tokens. It covers:
- Signing in: Authorization Code with PKCE, PAR, DPoP-bound tokens, RFC 9207 mix-up defence, and full ID token validation.
- Tokens: caching and refreshing them before they expire, safely across tabs.
- Several accounts: several people signed in to one browser, with one active account per tab.
- CAEP: subscribing to the identity provider’s Shared Signals stream, with one handler per CAEP event and a hook for your own logic.
It uses Web APIs only and depends on nothing but jose.
Install
npm install oidc-caep-client
That adds the package to your package.json:
{
"dependencies": {
"oidc-caep-client": "^0.2.0"
}
}
It works from JavaScript and TypeScript: the package ships type declarations, so import { SSFReceiver, type SecurityEvent } from 'oidc-caep-client' is typed with no extra setup.
Versions are ordinary semver, so ^0.2.0 takes every later 0.2 release; npm install oidc-caep-client@next gets the latest pre-release built from develop. Installing in the reference covers pinning a version and loading the package with or without a bundler.
Where to read
- Building a browser SPA, the walkthrough:
- setting up the identity provider and serving the page;
- signing in and choosing where tokens live;
- calling your APIs;
- receiving CAEP events;
- signing out, errors and troubleshooting.
- API reference: the security features, every option, method and event, and the CAEP event table.
- Generated references, from the source’s JSDoc, for every exported class, function and type:
- TypeScript API (TypeDoc), with the TypeScript signatures;
- JavaScript API (JSDoc).
- Running the demo: a complete app built this way, with several accounts per browser, a CAEP event log and an event-type picker, and how to register it on iya-sts.
- Development and releases: tests, CI, branches and versioning.
Quick look
JavaScript
import { logEventReceived, OIDCClient, SSFReceiver, TokenManager } from 'oidc-caep-client';
const client = await OIDCClient.discover({
issuer: 'https://idp.example.com',
clientId: 'my-spa',
redirectUri: `${location.origin}/callback`,
dpop: true,
});
const tokens = new TokenManager(client);
// Sign in, then on /callback:
await tokens.set('user', await client.callback(location.href));
// React to security events about the signed-in person.
const caep = new SSFReceiver({
transmitterIssuer: 'https://idp.example.com',
authorizationHeaders: async (req) => client.resourceHeaders(await tokens.getTokenSet('user'), req),
// Runs for every event, before the handlers below. The default, logEventReceived, prints
// the event's name and jti; replace it with your own logic, or pass null to turn it off.
onEventReceived: async (evt) => {
logEventReceived(evt);
await audit.record(evt.name, evt.subject);
},
});
// One handler per event type. Register your own to decide what each event does.
caep
.onSessionRevoked(() => signOutLocally('Your session was ended.'))
.onTokenClaimsChange(() => tokens.refresh('user'))
.onCredentialChange((evt) => {
if (['revoke', 'delete'].includes(evt.payload.change_type)) signOutLocally('Sign in again.');
})
.onAssuranceLevelChange((evt) => {
if (evt.payload.change_direction === 'decrease') requireStepUp();
})
.onDeviceComplianceChange((evt) => {
if (evt.payload.current_status === 'not-compliant') signOutLocally('This device no longer meets policy.');
})
.onRiskLevelChange((evt) => {
if (evt.payload.current_level === 'HIGH') signOutLocally('Unusual activity was detected.');
})
.onError((err, ctx) => console.warn('CAEP', ctx.phase, err.message));
await caep.start({ catchUp: true });
TypeScript
The same app, typed. The package ships its declarations, so this needs no extra setup: set "module": "NodeNext" (or your bundler’s resolution) in tsconfig.json.
import {
logEventReceived, OIDCClient, SSFReceiver, TokenManager,
type SecurityEvent, type SSFErrorContext, type TokenSet,
} from 'oidc-caep-client';
// Your app's own functions.
declare function signOutLocally(reason: string): Promise<void>;
declare function requireStepUp(): void;
declare const audit: { record(name: string, subject: unknown): Promise<void> };
const client: OIDCClient = await OIDCClient.discover({
issuer: 'https://idp.example.com',
clientId: 'my-spa',
redirectUri: `${location.origin}/callback`,
dpop: true,
});
const tokens = new TokenManager(client);
// Sign in, then on /callback:
const signedIn: TokenSet = await client.callback(location.href);
await tokens.set('user', signedIn);
const caep = new SSFReceiver({
transmitterIssuer: 'https://idp.example.com',
authorizationHeaders: async (req) => client.resourceHeaders(await tokens.getTokenSet('user'), req),
onEventReceived: async (evt: SecurityEvent) => {
logEventReceived(evt);
await audit.record(evt.name, evt.subject);
},
});
caep
.onSessionRevoked(() => signOutLocally('Your session was ended.'))
.onTokenClaimsChange(() => tokens.refresh('user'))
.onCredentialChange((evt: SecurityEvent) => {
if (['revoke', 'delete'].includes(evt.payload.change_type)) return signOutLocally('Sign in again.');
})
.onAssuranceLevelChange((evt: SecurityEvent) => {
if (evt.payload.change_direction === 'decrease') requireStepUp();
})
.onDeviceComplianceChange((evt: SecurityEvent) => {
if (evt.payload.current_status === 'not-compliant') return signOutLocally('This device no longer meets policy.');
})
.onRiskLevelChange((evt: SecurityEvent) => {
if (evt.payload.current_level === 'HIGH') return signOutLocally('Unusual activity was detected.');
})
.onError((err: Error & { code?: string }, ctx: SSFErrorContext) => console.warn('CAEP', ctx.phase, err.code, err.message));
await caep.start({ catchUp: true });
Registering a handler for an event type adds to what runs; caep.off(CAEP.SESSION_REVOKED, fn) removes one again. onEvent(uri, fn) handles any other type, such as RISC events. The guide suggests a reaction for each event, and the reference lists every handler and its payload fields.
MIT licensed. © 2026 Iya CyberSecurity Solutions, LLC.