Authentication libraries
These are client libraries that have been tested against iya-sts. Each one is the relying party’s side of a flow this service implements, packaged so an application can use it without writing the protocol code itself.
This list will grow. More libraries will be added as they are tested against iya-sts or written for it. A library listed here has been run against iya-sts; that does not mean it works only with iya-sts. Both libraries below are written to the specifications and also work with other OpenID providers and SSF transmitters.
The two libraries listed so far have the same use case on two platforms:
- OpenID Connect Authorization Code flow with PKCE, as a public client. The
application holds no client secret (
token_endpoint_auth_method: none). It signs a person in, keeps their tokens and refreshes them before they expire. - CAEP signals over the Shared Signals Framework. With the signed-in
person’s own access token, the application creates or reuses a Shared
Signals stream at iya-sts, polls it (RFC 8936), validates each Security Event
Token, and calls one handler per CAEP event type. For example, a
session-revokedevent signs the person out of the application at once.
| Library | Platform | Language | Repository |
|---|---|---|---|
| oidc-caep-client | Single-page applications in the browser | JavaScript, with TypeScript declarations | rcbj/oidc-caep-client (documentation, npm oidc-caep-client) |
swift-oidc-caep-native-app (OIDCCAEPClient) |
Native macOS apps; Linux desktops too | Swift | rcbj/swift-oidc-caep-native-app (documentation) |
The Swift library is a port of the JavaScript one. It keeps the JavaScript library’s names where Swift allows, and its protections are the same. The feature lists below are therefore nearly identical. The differences are in the comparison at the end.
This page covers what each library does and what to set up in iya-sts for it. How the libraries are built and called in detail is in each library’s own documentation. How iya-sts behaves as an OpenID provider and as a Shared Signals transmitter is on OAuth 2.0 and OpenID Connect and Shared Signals.
oidc-caep-client: single-page applications
An OpenID Connect client for browser single-page applications, written in
JavaScript and shipped with TypeScript declarations. It runs in the page as a
public client and receives CAEP events through the Shared Signals Framework.
It uses only Web APIs (fetch, WebCrypto, URL, Web Storage, IndexedDB), so it
runs in every current browser, and also in Node.js 24.22.1 or later for tests
and tools. Its one dependency is jose.
Use it for a single-page application that signs people in at iya-sts directly from the browser, calls APIs with the person’s access token, and reacts to CAEP signals about that person while the page is open.
Install
npm install oidc-caep-client # latest: built from main
npm install oidc-caep-client@next # next: pre-releases built from develop
The package is ES modules only. With a bundler (Vite, webpack, esbuild, Rollup)
import it as usual. Without one, serve node_modules/oidc-caep-client/src/ and
jose’s browser build, and map both in an import map. TypeScript finds the
declarations with nothing to install.
A minimal sketch
Taken from the library’s README:
import { OIDCClient, TokenManager, IndexedDBKeyStore, SSFReceiver, Subject }
from 'oidc-caep-client';
const client = await OIDCClient.discover({
issuer: 'https://sts.example.com', // iya-sts's issuer
clientId: 'my-spa', // a PUBLIC client
redirectUri: `${location.origin}/callback`,
postLogoutRedirectUri: `${location.origin}/`,
dpop: { keyStore: new IndexedDBKeyStore() }, // optional, recommended
});
const tokens = new TokenManager(client); // in memory, auto-refreshing
// Sign in: PKCE, state and nonce are made for you; PAR is used if offered.
location.assign((await client.createAuthorizationRequest()).url);
// On /callback: validate the response and the ID Token, keep the tokens.
await tokens.set('user', await client.callback(location.href));
// Call an API with a fresh token (and a DPoP proof when bound).
await client.fetchResource(await tokens.getTokenSet('user'),
'https://api.example.com/me');
// CAEP: one handler per event, over a poll stream.
const caep = new SSFReceiver({
transmitterIssuer: 'https://sts.example.com',
authorizationHeaders: async (req) =>
client.resourceHeaders(await tokens.getTokenSet('user'), req),
});
caep.onSessionRevoked(async (evt) => { /* sign the person out */ });
await caep.start({ catchUp: true }); // create or reuse the stream, poll
Building a browser single-page app
walks through a whole application. The repository’s demo/ is a working one,
run in one container against iya-sts.
Features
Sign-in and tokens
- OpenID Connect discovery (
OIDCClient.discover()), or a metadata document you supply (fromMetadata()). - Authorization Code flow only (
response_type=code), with PKCES256. It refuses a provider whose metadata does not offerS256. state: 256 random bits per request, kept with the transaction insessionStorage. On return it is required, compared in constant time, used once, expires aftertransactionTtlSec(ten minutes by default), and is bound to the issuer and client that started it.nonce: 256 random bits per request, compared in constant time with the ID Token’s.- Mix-up defence (RFC 9207): the
issresponse parameter must match, and is required when the provider advertises it. - Authorization response hygiene: tokens in the front channel (query or fragment) and repeated parameters are refused.
- Pushed Authorization Requests (RFC 9126): used automatically when the
provider advertises them (
par: 'auto'), and always when it requires them. - DPoP (RFC 9449): access and refresh tokens, and the authorization code
(
dpop_jkt), are bound to a non-extractable WebCrypto key. Proofs go to the token endpoint, UserInfo, the SSF endpoints and your own APIs. Server nonces (use_dpop_nonce) are handled. With DPoP on, aBearertoken response is refused (requireDpop), and a bound grant is never refreshed into a Bearer one. - ID Token validation: all of OpenID Connect Core section 3.1.3.7 (signature
with the expected
algonly,iss,aud,azp,exp,iatage,nonce,at_hash,max_age/auth_time), and the refresh rules of section 12.2. Other JWT types presented as ID Tokens, unknowncritheaders, and anacroutside the requestedacr_valuesare refused. - Resource indicators (RFC 8707):
resourceon the authorization, token and refresh requests. - Token refresh: refresh-token rotation, concurrent refreshes merged into one
request, scheduled refresh before expiry (
TokenManager), and a Web Lock so two tabs never present the same rotating refresh token. - UserInfo, with the
subchecked against the ID Token’s; a signed UserInfo response is verified. - Token revocation (RFC 7009).
- RP-Initiated Logout, with its own single-use, expiring
statechecked on return (validateEndSessionCallback()). - Dynamic Client Registration (RFC 7591) as a public client, and RFC 7592 read and delete of the registration.
- HTTPS required for every provider and transmitter URL and every redirect
URI. Loopback
httpis allowed for development.
Several accounts in one browser (AccountManager)
- Several people signed in at once, one cached token set each, in IndexedDB shared by every tab of the origin. One active account per tab; the others stay dormant and are not refreshed.
signIn(),chooseAccount()(prompt=select_account),addAccount()(prompt=login),resume()(prompt=none) andreauthenticate(). A request that names an account accepts only that account (account_mismatch).- Signing one account out ends only its session at the provider;
signOutAll()lets the provider ask which to end. - Changes to the account list are announced to the other tabs
(
BroadcastChannel) under a lock. - Hooks (
onActivate,onDeactivate,onLeave) where an application starts, stops or deletes the account’s signal stream. - This matches iya-sts’s several accounts in one browser, which is on by default.
Storage
- Tokens: in memory by default (
MemoryTokenStore), or Web Storage or IndexedDB.AccountManageruses IndexedDB by default only when DPoP is on, and warns when tokens go to persistent storage without DPoP. - DPoP key:
IndexedDBKeyStore(non-extractable, kept across page loads),WebStorageKeyStoreorMemoryKeyStore. - Sign-ins in progress:
sessionStorage.
CAEP and the Shared Signals Framework (SSFReceiver)
- Transmitter discovery from
/.well-known/ssf-configuration. - Poll delivery (RFC 8936). A browser cannot receive a push, so push is not offered.
- Stream management (SSF 1.0): create, list, read, update, replace and delete a stream; read and change its status; add and remove subjects; request a verification event.
- Stream reuse. If iya-sts refuses a new stream because the person already
has one (409) or holds as many as
ssf.maxStreamsallows (403),start()reuses one of their streams: the one whosedescriptionmatches, or else the first. It points that stream at this receiver’s delivery settings. start({ catchUp: true })handles the events queued while nobody was polling before it returns.stop()stops polling and sends the acknowledgements still owed;stop({ deleteStream: true })also deletes the stream (on sign-out, withssf.manage). The opt-indeleteOnPageHidedeletes the stream, best effort, when the page is unloaded for good. It is off by default, because a kept stream holds the events that arrive while no tab is open.- One handler per CAEP 1.0 event:
session-revoked,token-claims-change,credential-change,assurance-level-change,device-compliance-change,session-established,session-presentedandrisk-level-change. Also the SSFverificationandstream-updatedevents, any event by URI (the RISC 1.0 event types are exported asRISC), every event (onAnyEvent) and events with no handler (onUnhandledEvent). - An
onEventReceivedhook runs before the typed handlers for every accepted event. By default it logs one line with the event’s name andjtionly. - SET validation: a compact JWS with
typsecevent+jwt, signed with an asymmetric algorithm, verified against the transmitter’s JWKS;iss,aud,jti,iatand exactly one event checked; a replayedjtidropped. - Careful acknowledgement: a SET is acknowledged only after its handlers
succeed, and is retried up to
maxDeliveryAttemptstimes otherwise. Only a definitely invalid SET is reported back as an error. Polls never overlap. - Subject matching (
subjectMatches(),accountsForSubject()): byiss/sub, a verified email or phone number, andsid. A session-scoped subject matches only the account that holds that session. - The poll endpoint must be
httpsand on the transmitter’s origin, because every poll carries the person’s access token.
Not included: confidential-client authentication (client secrets,
private_key_jwt, mutual TLS), JAR and JARM, front- and back-channel logout,
and CAEP push delivery.
swift-oidc-caep-native-app: native macOS apps
An OpenID Connect client library for native macOS apps, written in Swift:
the OIDCCAEPClient product of a Swift package. It is the native counterpart
of oidc-caep-client. It signs the person in through the system browser
(RFC 8252), keeps their tokens in the Keychain, binds them to a key in the
Secure Enclave, and receives CAEP events through the Shared Signals Framework.
It needs macOS 13 or later and Swift 5.9 or later. Its only dependency is
Apple’s swift-crypto, which is
CryptoKit on Apple platforms.
The core also builds and runs on Linux desktops (GNOME, KDE). There, sign-in uses a loopback redirect, and tokens and the DPoP key are held in memory only. The repository’s Linux and macOS page lists every difference.
Use it for a desktop app that signs people in at iya-sts, calls APIs with a sender-constrained token, keeps people signed in across launches, and reacts to CAEP signals about them.
Install
With Swift Package Manager, in Package.swift:
dependencies: [
.package(url: "https://github.com/rcbj/swift-oidc-caep-native-app.git",
from: "0.1.0"),
],
targets: [
.target(name: "MyApp", dependencies: [
.product(name: "OIDCCAEPClient",
package: "swift-oidc-caep-native-app"),
]),
]
In Xcode: File → Add Package Dependencies…, the same URL, and the OIDCCAEPClient library for the app target.
A minimal sketch
Taken from the library’s README and reference. The app’s settings (the
provider’s metadata URL, the client_id, the scopes and the redirect URI) are
in OIDCConfiguration.plist:
import OIDCCAEPClient
let config = try OIDCAppConfiguration.load() // OIDCConfiguration.plist
let client = try await config.makeClient(
dpopKeyStore: await preferredDPoPKeyStore()) // the Secure Enclave
let accounts = try await AccountManager(client: client).restore() // Keychain
try await accounts.signIn(using: SystemWebAuthenticator()) // system browser
let tokens = try await accounts.getTokenSet() // refreshed when due
// CAEP: one handler per event, over a poll stream.
let receiver = SSFReceiver(SSFReceiverConfiguration(
transmitterIssuer: client.issuer,
authorization: .oidc(client) { try await accounts.getTokenSet() }))
receiver.onSessionRevoked { event in /* sign the person out */ }
try await receiver.start(catchUp: true) // create or reuse the stream, poll
Building a native macOS app
walks through a whole app, including sandboxing and signing. The repository’s
Demo/ is a working one (SwiftUI on macOS, GTK 4 on Linux) configured for
iya-sts at https://localhost:8081.
Features
Sign-in and tokens
- The system browser, never an embedded web view.
SystemWebAuthenticatorusesASWebAuthenticationSession, so the app never sees the password and the browser’s session is shared: single sign-on,prompt=noneand several accounts work.prefersEphemeralWebBrowserSessionturns the sharing off. - Redirect URIs: a private-use scheme in reverse-domain form
(
com.example.app:/callback, RFC 8252 section 7.1). The scheme must contain a period.httpswith an associated domain works on macOS 14.4 and later.LoopbackWebAuthenticatorreceives a loopback redirect (http://127.0.0.1:<port>/callback, RFC 8252 section 7.3) by listening on the port and opening the default browser; it is how the Linux build signs in, and it works on macOS too. - Configuration from
OIDCConfiguration.plist(or anOIDCConfigurationdictionary inInfo.plist): metadata URL, client ID, scopes, redirect and post-logout redirect URIs, resources, DPoP, PAR, the ID Token algorithm, ACR values, the SSF transmitter, and extra TLS trust anchors for a provider with a private CA. - OpenID Connect discovery, or a metadata document you supply.
- Authorization Code flow only, with PKCE
S256. It refuses a provider that does not offerS256or does not accept public clients (none). stateandnonceas in the browser library.stateis required, compared in constant time, used once, expires, and is bound to the issuer and client. Sign-ins in progress are held in memory, so the redirect must come back to the process that started the sign-in.- Mix-up defence (RFC 9207) and authorization response hygiene.
- Pushed Authorization Requests (RFC 9126):
auto,alwaysornever. - DPoP (RFC 9449), on by default in the configuration file. The key is made in
the Secure Enclave when the Mac has one (
SecureEnclaveKeyStore: the app can ask it to sign, but nothing can read it out), else in the Keychain (KeychainKeyStore). The authorization code is bound too (dpop_jkt). Proofs go to the token endpoint, UserInfo, the SSF endpoints and your own APIs. Server nonces are handled, and aBearerresponse is refused while DPoP is on. - ID Token validation: all of OpenID Connect Core section 3.1.3.7 and the refresh rules of section 12.2, as in the browser library.
- Resource indicators (RFC 8707).
- Token refresh with rotation, concurrent refreshes merged, and a refresh
scheduled before expiry (
TokenManager). - UserInfo with the
subchecked; a signed response verified. - Token revocation (RFC 7009).
- RP-Initiated Logout in the browser, with its
statechecked (endSession(using:request:)). - Dynamic Client Registration (RFC 7591) as a public client, and RFC 7592 read and delete.
- HTTP through an ephemeral
URLSessionwith no cookies and no cache, which never follows a redirect. HTTPS is required for every provider and transmitter URL; loopbackhttpis allowed for development.
Several accounts in one app (AccountManager)
- The same model as the browser library: several people signed in, one active account, the others dormant. The cache is the Keychain, so every account, and which one was active, survives a relaunch.
signIn,chooseAccount,addAccount,resume(prompt=none) andreauthenticate, each with ausing:form that runs the browser step for you. A request that names an account accepts only that account.- Signing one account out ends only its session;
signOutAlllets the provider ask which to end. - Hooks (
onActivate,onDeactivate,onLeave) for starting, stopping and deleting the account’s stream.
Storage
- Tokens:
KeychainTokenStoreby default on Apple platforms (the login keychain, or the data protection keychain withkSecAttrAccessibleAfterFirstUnlockThisDeviceOnly);MemoryTokenStoreelsewhere. - DPoP key:
SecureEnclaveKeyStore,KeychainKeyStoreorMemoryKeyStore;preferredDPoPKeyStore()picks the strongest available. - Your own stores implement
TokenStore,DPoPKeyStoreorTransactionStore, and your own networking implementsHTTPTransport.
CAEP and the Shared Signals Framework (SSFReceiver)
- Everything the browser library’s receiver does: discovery, poll delivery
(RFC 8936), the full SSF 1.0 stream management, one handler per CAEP 1.0
event and per SSF event, any event by URI (RISC included),
onAnyEvent,onUnhandledEvent, theonEventReceivedhook, the same SET validation and acknowledgement rules, and subject matching. - Stream reuse on a 409, or a 403 at
ssf.maxStreams: the stream whosedescriptionmatches, or else the first, is reused and re-pointed at this receiver. start(catchUp: true),stop(), andstop(deleteStream: true)on sign-out. The guidance is not to delete the stream when the app quits or goes to the background, but to keep its id and pass it asstreamIDon the next launch.- The receiver authorizes with the signed-in person’s token and a DPoP proof
(
.oidc(client)), a static bearer token (.bearer), or headers you build (.headers). - SETs obtained some other way can be passed to
receiveSet(_:).
Not included: confidential-client authentication, JAR and JARM, front- and back-channel logout, CAEP push delivery, an embedded web view (on purpose), and a sign-in that survives a relaunch part way through.
Setting up iya-sts for either library
Both libraries need the same things from iya-sts. Only the redirect URI and
CORS differ between them. Each library’s repository has a script,
scripts/register-iya-sts-client.mjs, that does all of the steps below
for its demo through the Management API. It is also a
worked example of the calls.
-
Register a public client. Create an application declared for OAuth 2.0 and OpenID Connect, with:
Field SPA (oidc-caep-client) Native app (swift-oidc-caep-native-app) oauthTokenEndpointAuthMethodnonenoneoauthResponseTypecodecodeoauthGrantTypeauthorization_code,refresh_tokenauthorization_code,refresh_tokenoauthRedirectUrithe page’s callback, e.g. https://app.example.com/callbacka private-use scheme, e.g. com.example.myapp:/callback; or loopbackhttp://127.0.0.1:<port>/callbackon LinuxoauthPostLogoutRedirectUrie.g. https://app.example.com/e.g. com.example.myapp:/logged-outoauthAllowedScopeopenid,profile,email,offline_access,ssf.read,ssf.managethe same appCorsOriginrequired: the page’s origin, e.g. https://app.example.comnot needed: a native app is not subject to CORS Configuring OAuth 2.0 grants → A public client gives the console steps and the API call. Applications describes every field,
appCorsOriginincluded. The browser calls discovery, PAR, the token endpoint, UserInfo, revocation and every SSF endpoint directly, so withoutappCorsOriginthose calls fail as network errors. - Let the client be issued the Shared Signals scopes. In iya-sts
ssf.readandssf.manageare gated by role. The application must be an application member of the native roleSSF_READforssf.read, and ofSSF_MANAGEforssf.manage. Ask for both, becauseSSF_MANAGEdoes not authorizessf.read. Nothing adds an application to the roles for you (apiis the shell helper from Configuring OAuth 2.0 grants → Before you start; the console page is/admin/roles):api roles/add-member '{"role":"SSF_READ","kind":"application","member":"my-spa"}' api roles/add-member '{"role":"SSF_MANAGE","kind":"application","member":"my-spa"}'A library that only creates and deletes its own stream needs
ssf.manage. One that only reads a stream somebody else created needs onlyssf.read. Shared Signals → Authentication explains the two scopes, and Configuring Shared Signals → Register a receiver the receiver side. - Put the people in the matching groups. A person is issued
ssf.readonly while they are in the groupssf-read, andssf.manageonly while they are inssf-manage. Every realm has these roles and groups by default:api groups/add-member '{"group":"ssf-read","member":"alice"}' api groups/add-member '{"group":"ssf-manage","member":"alice"}'When the person or the application lacks a role, the scope is left off the token (or the request fails with
invalid_scope), and iya-sts logsSTS-ADMIN-0821, saying which condition failed. The stream calls then fail for want of the scope. -
Consent. Consent is on by default, so the first sign-in stops at
/oauth2/consentuntil the person approves the client. For a first-party application, grant consent for everybody in advance, as Configuring OpenID Connect flows describes. - The issuer. Point the library at the issuer the realm’s discovery
document names: the address the service is reached at, with
/realm/<id>in front of the path for any realm but the default one (Trust realms). The SSF transmitter has the same issuer, and its metadata is at/.well-known/ssf-configuration.
Settings that affect the libraries’ streams. Each library creates a
person-owned poll stream with the signed-in person’s own token. iya-sts
sends such a stream only events about that person while
ssf.personStreamsSelfOnly is on (the default). The stream is deleted after
ssf.personStreamInactivityTimeoutS (a day by default) with no poll, which
cleans up after a tab or app that never comes back. ssf.maxStreams (25) caps
how many streams one receiver may hold, and the libraries reuse a stream
rather than fail at that cap. Every setting is on
Shared Signals → Configuration. What fires
each CAEP event a library will receive is on CAEP events.
TLS. In development, iya-sts’s certificate is issued under its own Root,
which is rebuilt on every start. The browser, or the Mac, has to trust it:
open the URL once and accept it, or trust the Root as
TLS → The server certificate describes. The
Swift library’s TrustedCertificates setting adds a trust anchor without
touching the system.
Optional extras the libraries use when iya-sts offers them. PAR is used whenever the discovery document advertises it. DPoP-bound tokens need nothing configured in iya-sts, and can be required of a client (Security profiles). Several accounts in one browser is on by default (Several accounts).
Comparison
| oidc-caep-client | swift-oidc-caep-native-app | |
|---|---|---|
| Kind of application | Browser single-page application | Native macOS app (Linux desktops too) |
| Language | JavaScript, with TypeScript declarations | Swift 5.9+ |
| Distribution | npm (latest, and next from develop) |
Swift Package Manager |
| Runtime requirement | A current browser; Node.js 24.22.1+ outside one | macOS 13+ (glibc 2.35 and GTK 4.6 for the Linux demo) |
| Dependency | jose |
swift-crypto |
| Client type | Public (none) |
Public (none) |
| Flow | Authorization Code + PKCE S256 |
Authorization Code + PKCE S256 |
| Where sign-in happens | The page redirects itself | The system browser (ASWebAuthenticationSession), or the default browser with a loopback listener |
| Redirect URI | https on the page’s origin |
Private-use scheme; https with an associated domain; loopback |
| CORS origin in iya-sts | Required | Not needed |
| PAR, RFC 9207, RFC 8707 | Yes | Yes |
| DPoP key | Non-extractable WebCrypto key in IndexedDB | Secure Enclave, else Keychain |
| DPoP default | Off; turn it on | On in OIDCConfiguration.plist |
| Token cache | Memory by default; IndexedDB for several accounts | Keychain |
| Survives a reload or relaunch | With IndexedDB | Yes, on macOS |
| Several accounts | Yes, one active account per tab, shared across tabs | Yes, one active account per app |
| RP-Initiated Logout, revocation | Yes | Yes |
| Dynamic Client Registration | Yes (RFC 7591, RFC 7592) | Yes (RFC 7591, RFC 7592) |
| SSF delivery | Poll (RFC 8936) | Poll (RFC 8936) |
| CAEP events with their own handler | All eight CAEP 1.0 events, plus SSF verification and stream-updated | The same |
| RISC events | Through onEvent(RISC.…) |
Through onEvent(RISC.…) |
| Stream reuse at 409/403 | Yes | Yes |
| Stream deletion | On sign-out; opt-in on page unload | On sign-out |
| Not included | Confidential clients, JAR/JARM, front- and back-channel logout, push delivery | The same, and an embedded web view |
| Licence | MIT | MIT |