Session & tokens
Observing auth state
Section titled “Observing auth state”RakomiAuth never throws across its async API surface for runtime auth outcomes — state is
observed on an AsyncStream:
for await state in auth.authStateChanges() { switch state { case .loading: break case .signedOut: break case .authenticated(let user, let session): break case .error(let sdkError): break }}AuthState is a sealed enum: .loading → .signedOut → .authenticated(User, Session) →
.error(SdkError). state.user, state.session, and state.isAuthenticated are convenience
accessors.
Fine-grained events
Section titled “Fine-grained events”A second, separate stream carries discrete lifecycle events for analytics/audit pipelines — it is consumer-pull only; the SDK never auto-forwards it to any sink:
for await event in auth.events() { // .signedIn(User), .signedOut(reason:), .tokenRefreshed, // .mfaChallengeRequired(challengeId:), .mfaChallengeVerified, // .sessionRevoked, .userUpdated(User)}Each event carries an auditSignificance (.high / .medium / .low) as a hint for which events
are worth wiring into your own audit log:
| Event | Audit significance |
|---|---|
signedIn | high |
signedOut | high |
mfaChallengeVerified | high |
sessionRevoked | high |
mfaChallengeRequired | medium |
userUpdated | medium |
tokenRefreshed | low |
Getting a token
Section titled “Getting a token”let token = try await auth.getToken()getToken() returns a valid access token, transparently refreshing it first if the current one is
close to expiry. It throws if there is no session.
let stillValid = await auth.hasValidSession()hasValidSession() is offline-tolerant: it checks the cached token’s expiry and verifies its
signature against the locally cached signing keys, without requiring a network round trip.
try await auth.refreshTokens()Forces a refresh regardless of the current token’s remaining lifetime. Concurrent refresh calls are deduplicated — the SDK never issues two simultaneous refresh requests for the same session.
Token storage
Section titled “Token storage”Refresh tokens are persisted in the platform Keychain, namespaced per tenant so that switching
tenants (or having multiple RakomiAuth configurations in one process) never mixes credentials.
Stored values are protected against iCloud Keychain sync, so a refresh token never leaves the
device it was issued on. Access tokens are held in memory for the life of the session and are not
separately persisted — they are recovered on cold start by refreshing from the stored refresh
token.
On sign-out, only the signing-out configuration’s local state is cleared — a sibling configuration for a different tenant is unaffected. On the SDK’s first hydration after a fresh install, it detects whether the app was previously installed and force-purges any Keychain residue that survived the uninstall (Keychain entries are not always cleared by iOS on app deletion), so a reinstall never inherits a stranger’s session.
Automatic refresh
Section titled “Automatic refresh”getToken() refreshes automatically when the cached token is close to expiring, and the SDK
retries a failed refresh with an increasing backoff before giving up and clearing the session. A
clock-skew correction, captured from the server’s response headers, keeps expiry checks accurate
even when the device clock has drifted — bounded to a small maximum skew, beyond which the SDK falls
back to online verification rather than trusting an implausible correction.
Offline-tolerant verification
Section titled “Offline-tolerant verification”Signature verification uses a locally cached JSON Web Key Set (JWKS), refreshed periodically and
whenever the cache misses a key id the SDK hasn’t seen before. This means hasValidSession() and
routine token verification work without a network call in the common case, while still recovering
automatically the first time the signing keys rotate.
DPoP-bound refresh (advanced, opt-in)
Section titled “DPoP-bound refresh (advanced, opt-in)”For deployments that enable it, the SDK supports RFC 9449 DPoP-bound refresh tokens — binding each refresh to a device-held key so a stolen refresh token cannot be replayed from another device:
try auth.controller.enableDpop()Once enabled, the SDK attaches a proof to each refresh request bound to a session-scoped key
(Secure Enclave-backed where available), confirmed by the server’s response on each token issuance.
A session that opted in but that the server serves as a plain bearer token is surfaced through an
onDowngrade callback rather than silently proceeding — see Errors for the
distinct failure classes this can produce. Most integrations do not need to enable this directly.