How to store and refresh session tokens correctly with the WorkOS iOS SDK
A practical guide to Keychain storage, refresh token rotation, and terminal vs. transient failures on iOS
Our tutorial on native AuthKit sign-in covered wiring up the WorkOS iOS SDK's PublicClient to authenticate a user. That flow ends with an authenticated session: an access token and a refresh token. What you do with those tokens next matters as much as the sign-in flow itself.
Two mistakes are common on iOS: storing tokens in UserDefaults instead of the Keychain, and treating every failed refresh as a reason to sign the user out. Both are avoidable once you understand how WorkOS handles token rotation and failure classification.
What you get back
A successful authentication returns a JSON Web Token (JWT) access token and a refresh token. The access token carries several claims worth knowing:
sub: the WorkOS user IDsid: the session ID, used when signing outiss: the issuer (https://api.workos.com/, or your custom auth domain)org_id: the organization selected at sign-in, if applicableroleandpermissions: the user's role and permissions for that organizationexpandiat: standard expiry and issued-at claims
Keep the access token short-lived. WorkOS recommends a short access token duration precisely so that changes to a session (a revoked role, a suspended user) show up quickly rather than staying valid for hours.
Where to store tokens on iOS
Store both tokens in the Keychain, not UserDefaults. UserDefaults is backed by an unencrypted plist file; the Keychain is hardware-backed and the standard place for credentials on Apple platforms. A minimal wrapper looks like this:
Refresh tokens rotate on every use
Every time you exchange a refresh token for a new access token, WorkOS returns a new refresh token too, and retires the one you sent. Always persist the newly returned refresh token and discard the old one; never reuse a refresh token you've already exchanged.
There's one deliberate exception: a 30-second grace period after each exchange. If the same refresh token is replayed within that window (two concurrent requests both noticing an expired access token, or a retried request after a slow network), WorkOS returns the same rotated tokens instead of an error, so retries and race conditions don't tear down the session. After the grace period, replaying a spent token is terminal and returns an invalid_grant error.
Terminal vs. transient failures
This is the part most integrations get wrong: not every failed refresh means the session is over. WorkOS draws a hard line between two categories of failure.
- Terminal failures mean the session is genuinely done and the user needs to sign in again. This is an OAuth
invalid_granterror at HTTP 400, for a revoked, expired, or already-consumed refresh token. - Transient failures mean the request itself didn't complete, but the session is still valid. The correct response is to retry, not to sign the user out. These include network errors and timeouts,
429 Too Many Requestsfrom brief refresh contention, and5xxerrors from a temporary upstream issue.
Backend-style SDKs, including workos-ios, are bring-your-own-handling: the refresh call returns a typed result and never clears the session for you. Transient transport errors get retried internally first; what surfaces to you is either success or a failure you still have to classify. That's different from the turnkey web framework integrations (authkit-nextjs, authkit-js, and similar), which handle this distinction automatically.
In practice, that means your refresh handling on iOS should look roughly like this:
Treat the method name and the shape of result above as illustrative. Check the SDK's current DocC reference for the exact refresh method signature and result type in the version you install, generated SDKs iterate quickly, and the point that matters (classify before you clear the session) holds regardless of the exact API surface.
Signing out
When a user signs out, pull the sid claim from the access token, clear both tokens from the Keychain, and end the session on the WorkOS side through the logout endpoint so the session doesn't just disappear locally while staying valid on the server.
Configure session lifetimes to match your app
In the WorkOS dashboard, under your application's Sessions tab, you control three things:
- maximum session length (how long before a user must sign in again, full stop)
- access token duration (how long a single access token is valid before it needs refreshing)
- and inactivity timeout (how long a session can go without a refresh before it's considered abandoned)
Shorter access token durations mean permission and role changes propagate faster; just weigh that against the extra refresh traffic on a mobile network.
The takeaway
Two rules cover most of what matters here: store tokens in the Keychain, not UserDefaults, and only clear a session on a terminal invalid_grant, never on a timeout or a 5xx. Get those two right and the rest of the session lifecycle mostly takes care of itself.
Further reading: Sessions and Session resilience in the WorkOS docs.