Session & tokens
This page describes what the SDK does with tokens and sessions, not the platform-specific storage implementation — see Security for the threat model.
Reading the current session
Section titled “Reading the current session”final auth = RakomiProvider.of(context);
User? user = auth.user;Session? session = auth.session;AuthState state = auth.authState; // current snapshotbool valid = await auth.hasValidSession(); // in-memory token, not expired, JWKS-verifiedString? token = await auth.getToken(); // current or freshly refreshed access tokenSession carries the access token’s lifetime metadata (id, userId, tenantId, issuedAt,
expiresAt) — never the access token itself. The access token is held only in the
controller’s in-memory state.
getToken() returns the current access token if it is not within the refresh lead-time of
expiry (kDefaultRefreshLeadtime, 60 seconds by default, configurable via
RakomiProvider(refreshLeadtime:)); otherwise it refreshes first.
Refresh
Section titled “Refresh”Refresh is:
- Single-flight — concurrent callers awaiting
getToken()/refreshTokens()during an in-progress refresh share the same outcome; the SDK never issues two concurrent refresh requests for one provider tree. - Triggered proactively — the controller listens for the app returning to the foreground and forces a refresh-if-stale check.
- Rotating — a successful refresh persists the server-issued refresh token (which may differ from the one presented).
- Verified end-to-end — the new access token is verified (RS256 against the cached JWKS) before the SDK considers the session authenticated; if verification fails, the refresh is treated as a failure, not a silent partial success.
refreshTokens() forces a refresh — useful after your app detects it has resumed from the
background and wants an up-to-date token before making a request.
A 401/403 from the refresh endpoint is not retried: the SDK clears local session state
and moves to AuthStateUnauthenticated. Any other non-2xx or network failure surfaces as
SdkError(code: SdkErrorCode.refreshFailed | networkError, …) without clearing local state,
so a transient failure does not force a re-login.
App-restart session restore
Section titled “App-restart session restore”On mount, RakomiProvider calls hydrate() in the background (never blocking the first
frame): it reads the persisted refresh token and, if present, performs a refresh to validate it
end-to-end before considering the session restored. There is no separate “trust the cached
session” path — the persisted refresh token is always re-validated against the API on cold
start.
Secure storage
Section titled “Secure storage”Tokens and session metadata are persisted through the pluggable RakomiNativeAdapter, which
the default adapter wires to platform-native secure storage:
- iOS — Keychain.
- Android — encrypted storage via
flutter_secure_storage. - Flutter Web — see Web security considerations; this platform does not have OS-level secure storage.
The access token is held in memory only and is never written to durable storage. Only the
refresh token, a cached user snapshot, and cache metadata (JWKS, clock skew) are persisted, and
every persisted key is namespaced per tenant so multiple RakomiProviders in the same app
never collide.
On sign-out, or when a fresh-install is detected (see below), the SDK clears its persisted state for that tenant rather than relying on process memory being discarded.
Fresh-install detection (iOS)
Section titled “Fresh-install detection (iOS)”iOS Keychain entries survive app uninstall. On mount, the SDK compares a paired identifier held in Keychain against a marker in non-secure storage; if the Keychain identifier is present but the marker is missing (or does not match), the SDK treats this as a post-uninstall reinstall and purges the tenant’s residual Keychain entries before reading any auth state — so a fresh install never silently inherits a previous install’s session.
JWKS verification
Section titled “JWKS verification”Access tokens are RS256-signed JWTs, verified against the tenant’s JSON Web Key Set:
- The signing algorithm is hardcoded to RS256 and is never read from the token header — a
token whose header names any other algorithm (including
none) is rejected before any signature check. - The JWKS document is cached with a 24-hour TTL and revalidated with
ETag/If-None-Match; an oversized response (over a fixed cap) is rejected rather than accepted. - If a token references an unknown
kid, the SDK invalidates the cache and retries once with a forced refetch before giving up. RakomiProvider(jwksUrl:)lets you override the JWKS endpoint; likeapiBaseUrl, it must behttps://(or the same dev-loopback exception — see Configuration).
Clock skew
Section titled “Clock skew”On the first successful network response, the SDK compares the server’s Date header against
the device clock and applies the resulting (capped) offset to subsequent token expiry checks,
so a device with an inaccurate clock does not spuriously reject valid tokens or accept
already-expired ones. A skew beyond a small threshold is surfaced as
AuthEventType.clockSkewDetected with AuthEventSeverity.security.
DPoP sender-constrained sessions (advanced)
Section titled “DPoP sender-constrained sessions (advanced)”rakomi_flutter can bind a session’s refresh calls to a native-keystore key pair per
RFC 9449 (DPoP) — the access token becomes useless to an attacker who
exfiltrates it without also holding the device’s secure-element key. This is opt-in and
session-scoped: construct a DpopSession with a DpopProver and pass it to your session setup.
The production prover (NativeKeystoreDpopProver) never holds the private key in Dart — every
signature is produced by the platform keystore (Android Keystore/StrongBox, iOS
Keychain/Secure Enclave) over a native bridge you provide.
If the session is DPoP-bound and the native signer cannot produce a proof, the SDK never
falls back to a proof-less request — it surfaces a distinct dpop_prover_unavailable failure
so the app can prompt a re-login rather than silently downgrading security.
Audit events
Section titled “Audit events”The SDK emits fine-grained lifecycle events over the events broadcast stream — sign-in
attempts and outcomes, refresh attempts and outcomes, biometric failures, clock-skew detection,
and more. The SDK does not forward these anywhere on its own; your app is responsible for
forwarding audit-significant events to its own audit log. Recommended events to forward:
signedIn / signedOut (with user and tenant id), refreshSucceeded (user and session id),
signInFailed (with only hashed/redacted identifiers), and the security-severity events
(clockSkewDetected, biometricFailed, deepLinkReceived).
Publisher-webhook verification
Section titled “Publisher-webhook verification”If your backend receives Rakomi publisher-app webhooks and you want to verify them from Dart,
verifyPublisherWebhook implements the Standard Webhooks HMAC-SHA256 contract (constant-time
comparison, bounded replay window, no throw on malformed input). This is a standalone function —
it does not depend on RakomiProvider or an authenticated session. See the
publisher-webhook receiver contract for the wire format.