Running the demo
The demo, CAEPDemo, is a macOS SwiftUI app built on OIDCCAEPClient. It is the native counterpart of oidc-caep-client’s demo, with the same screens and the same reactions to CAEP events.
It lives in Demo/, a Swift package of its own. It depends on the library by path (..), the same way your app depends on it by URL, so the library and the demo stay separate: the library’s Package.swift doesn’t mention the demo. It needs macOS 14 or later.
Start it
From the repository root:
cd Demo && swift run CAEPDemo
Or build an app bundle and open it:
Demo/scripts/build-app.sh
open Demo/build/CAEPDemo.app
build-app.sh builds the demo and wraps it in Demo/build/CAEPDemo.app, signed ad hoc. An app bundle behaves more like a shipped app than swift run does: it has a bundle identifier and a code signature, which the Keychain ties its items to. Because an ad-hoc signature changes with every build, macOS may ask whether a new build may use the items the previous one stored; choose Always Allow.
The app reads Demo/Sources/CAEPDemo/Resources/OIDCConfiguration.plist:
| Key | Value |
|---|---|
MetadataURL |
https://localhost:8081/.well-known/openid-configuration |
ClientID |
oidc-caep-native-demo |
Scopes |
openid profile email offline_access ssf.read ssf.manage |
RedirectURI |
net.rcbj.caepdemo:/callback |
PostLogoutRedirectURI |
net.rcbj.caepdemo:/logged-out |
CAEPEnabled |
true |
Edit it to point at another provider; Building a native macOS app lists every key. The Mac must trust the provider’s TLS certificate, since both the app and the browser connect to it. For iya-sts’s development certificate on localhost:8081, add it to the login keychain in Keychain Access and set it to Always Trust, or issue one from a local CA such as mkcert.
Register the client on iya-sts
The default plist points at iya-sts on https://localhost:8081, client oidc-caep-native-demo. Register it in the default realm, from the repository root:
ADMIN_API_CLIENT_SECRET=… node scripts/register-iya-sts-client.mjs
scripts/register-iya-sts-client.mjs needs Node.js and nothing installed. It reads the plist, gets a management API token as sts-api-client (the client iya-sts seeds for its management API), and:
- creates the public client (
token_endpoint_auth_method: none) with the plist’s redirect URI and post-logout redirect URI, and the plist’s scopes. A native app needs no CORS origin; - adds the client to
SSF_READandSSF_MANAGE, for whichever ofssf.readandssf.managethe plist asks for (iya-sts issues each only when the application is a member of the role and the person is in its group,ssf-readorssf-manage); - creates a test user,
caep-demo-user, in those groups. Its password isCAEP_DEMO_USER_PASSWORD, or one is generated and printed once.
| Option | Use |
|---|---|
--config path |
Another OIDCConfiguration.plist, such as your own app’s. |
--test-user name, --no-test-user |
Name the test user, or skip it. |
The secret is ADMIN_API_CLIENT_SECRET or STS_ADMIN_API_CLIENT_SECRET. Every step can be repeated safely.
What it does
- Sign-in happens in the system browser through
ASWebAuthenticationSession(RFC 8252), never in an embedded web view. The browser’s session is shared, so a second sign-in can use the provider’s account picker. - DPoP binds tokens to a key in the Secure Enclave when the Mac has one, and otherwise to a key kept in the Keychain.
- PAR is used whenever the provider offers it.
- Tokens for every signed-in account are cached in the Keychain, so they survive a relaunch.
- CAEP: the stream is created with the person’s access token (with a DPoP proof) and polled from the app. With
ssf.manage, the person is added as the stream’s subject, and the stream asks for the event types picked on the CAEP events tab.CAEPEnableddefaults to true, and the shipped plists leave it true and still listssf.readandssf.manage, so the demo still receives events. Set it to false to turn off CAEP and RISC: the receiver is not started, and those two scopes are left out of the authorization request, so the same app can sign in at an identity provider that does not implement Shared Signals. The plist can keep listing the scopes.
The window has a top bar and two tabs.
The top bar shows the active account. Its menu switches to another signed-in account, signs in with another account, signs out, and signs out of all accounts.
Identity:
- Sign in, when nobody is signed in.
- Protections in effect: PKCE,
state,nonce, issuer identification, PAR, the DPoP algorithm, key thumbprint and whether the key is in the Secure Enclave, resource indicators, TLS and the ID token checks (client.securityFeatures). - Session claims: the validated ID token’s claims for the active account.
- Token cache: the cached token set, with a meter showing the time left before the access token expires, and Call UserInfo and Force refresh.
- Tokens: the tokens issued at sign-in beside the current ones, decoded.
CAEP events:
- The stream: the transmitter, the
stream_idand the stream’s status. - Event types: a picker of the CAEP event types, and Update subscription, which applies them to the stream (
updateStream). - Request verification asks the transmitter for a verification event; Reconnect restarts the receiver.
- The event log: each event received for the active account, with the action taken.
It reacts to events about the signed-in person:
| Event | Action |
|---|---|
| session-revoked | Ends the account and discards its tokens. |
| token-claims-change | Refreshes the tokens. |
| credential-change | Revoked or deleted: ends the account. Otherwise tells the person. |
| assurance-level-change | Decrease: asks for step-up (re-authentication). |
| device-compliance-change | Not compliant: ends the account. |
| risk-level-change | HIGH: ends the account. MEDIUM: asks for step-up. |
| session-established, session-presented | Logged only. |
Several accounts
iya-sts can keep several people signed in to one browser (authn.multipleAccounts). The app has one active account: only its tokens are used and refreshed, and only its stream is polled. The others are dormant.
- Switching (the account menu) needs no password. The account’s stream first catches up on the events queued while it was dormant.
- A signed-out or ended account in the menu starts a sign-in that names it (
login_hint) and refuses anybody else. - Sign in with another account is a fresh sign-in (
prompt=login). - The event log shows the active account’s events only, and is cleared for an account when it signs out.
- An event that ends a session ends that account only. A
session-revokednaming another of the person’s sessions is ignored. - Sign out revokes that account’s tokens, ends its session at the provider in the browser, and keeps the other accounts. Sign out of all ends them all.
- Quitting the app keeps every account: the cache is in the Keychain, and the active account is reactivated at the next launch.