Sandboxed – iOS Security for Builders

Episode 3

Storing Tokens Safely: Keychain vs Files vs UserDefaults

“Should I put my access token in Keychain, a file, or just UserDefaults?” It's one of the most common questions—and one of the most commonly answered wrong.

In Episode 3, we compare all three storage options, walk through real-world tradeoffs, and give you a simple decision framework you can use with your team this week.

🎯This episode: Where should your tokens live?

Your app probably has several kinds of little secrets that decide what the backend will let you do:

  • Access tokens – short-lived OAuth tokens for API calls
  • Refresh tokens – longer-lived credentials that mint new access tokens
  • Session identifiers or cookies – from legacy web backends
  • Device or installation IDs – used for fraud checks, push, or rate limiting

They all look roughly the same when you log them: long random strings. But they don't all have the same impact if they leak.

  • If your access token leaks, an attacker might get a few minutes or hours of access.
  • If your refresh token leaks, they might get access for weeks or months.
  • If your session cookie leaks, they might bypass your login screen entirely.

📦The three storage options

UserDefaults – Great for settings, bad for secrets

UserDefaults was never designed as a secret store. It's basically a property list file inside your app's container.

If someone can read your app's container—through a jailbreak, a device backup, or a compromised Mac—they can read those preference files.

If a value lets you act as the user, it's not a UserDefaults value.

Use UserDefaults for: feature flags, “hasShownOnboarding”, selected theme, analytics opt-in.

Files and databases – Powerful, but extra work needed

Storing tokens directly in your database is almost always a smell. If someone copies that database from a backup or jailbroken device, they now have a pile of tokens.

Better approach: store an opaque identifier in the database, and keep the actual secret in the Keychain.

Keychain – The right place for tokens

The Keychain is a system-wide, encrypted store for small pieces of sensitive data. Each item can have its own access control and accessibility settings.

Why Keychain is better:

  • Items are encrypted at rest using keys managed by the system
  • Fine-grained control over when items are available (e.g., only when unlocked)
  • Items are tied to your app or access group
  • Choose whether items are device-only or synced via iCloud

🧭A simple decision framework

For any value you're about to persist, ask:

  1. Does this value let someone act as the user or impersonate the device?
    If yes, treat it as a secret.
  2. Can I easily recreate it if it's lost?
    You can always ask the user to log in again.
  3. How long is it valid, and what's the blast radius if it leaks?
    Short-lived, heavily-scoped tokens are less risky than long-lived “super tokens”.

Practical mapping

Token TypeStorageAccessibility
Access tokensKeychainAfter first unlock, this device only
Refresh tokensKeychainSame or stricter than access token
Session cookiesKeychain or system cookie store
Device IDs (security-sensitive)KeychainAfter first unlock, this device only
Feature flags, preferencesUserDefaults

💻Code samples

Official Implementation References

📚 Apple Developer Documentation

🔧 Apple Sample Projects

Key API Pattern

The Keychain Services API follows a dictionary-based pattern. You build a query dictionary with keys like kSecClass,kSecAttrService, andkSecAttrAccessible, then pass it to one of four functions:

  • SecItemAdd — Store a new item
  • SecItemCopyMatching — Retrieve an item
  • SecItemUpdate — Update an existing item
  • SecItemDelete — Remove an item

See Apple's documentation links above for complete, copy-paste-ready implementations.

What NOT to do

❌ Never store tokens in UserDefaults. Values end up in a plist file inside the app container, which is not designed to store secrets. These values are:

  • Unencrypted on disk
  • Included in device backups
  • Accessible to anyone with filesystem access (jailbroken devices, forensic tools)

Migrating from UserDefaults

If your app currently stores tokens in UserDefaults, migrate them to Keychain on first launch:

  1. Check if the legacy UserDefaults key exists
  2. Read the token value
  3. Save it to Keychain using SecItemAdd
  4. Delete the UserDefaults entry with removeObject(forKey:)

Call this once early in your app lifecycle, for example in application(_:didFinishLaunchingWithOptions:).

Four things you can do this week

1. Audit where your tokens live today

Search your codebase for "accessToken" or "refreshToken" in UserDefaults code. Make a table: token type, current storage, desired storage.

2. Move the high-impact tokens to Keychain

Start with access tokens that can hit financial or health endpoints, and refresh tokens that keep sessions alive for a long time.

3. Choose sensible Keychain accessibility settings

As a default, “after first unlock, this device only” is a good balance for most apps. Document this decision so future teammates don't have to guess.

4. Document what is allowed in UserDefaults

Add one line to your README:

“UserDefaults must not contain access tokens, refresh tokens, passwords, session cookies, or secrets. It is only for preferences, feature flags, and non-sensitive state.”

📱About Sandboxed

Sandboxed is a podcast for people who actually ship iOS apps and care about how secure they are in the real world.

Each episode, we take one practical security topic — like secrets, auth, or hardening your build chain — and walk through how it really works on iOS, what can go wrong, and what you can do about it this week.

If that sounds like your kind of thing, subscribe to stay ahead of the quiet, boring changes that add up to real security wins.

Ready to dive deeper?

In Episode 4, we ask a slightly uncomfortable question: “What happens to all of this if the device is jailbroken?”

Listen to Episode 4 →

Stay in the Loop

Get iOS security insights, new episode alerts, and exclusive content delivered to your inbox.

No spam. Unsubscribe anytime.

Storing Tokens Safely: Keychain vs Files vs UserDefaults | Sandboxed Podcast