Skip to content

Session & tokens

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.

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:

EventAudit significance
signedInhigh
signedOuthigh
mfaChallengeVerifiedhigh
sessionRevokedhigh
mfaChallengeRequiredmedium
userUpdatedmedium
tokenRefreshedlow
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.

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.

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.

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.

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.

  • Errors — the full SdkErrorCode / SdkErrorReason taxonomy, including the refresh- and DPoP-specific reasons mentioned above.
  • Security — the SDK’s threat model and what remains the host app’s responsibility.