Where a session token should live on iOS
UserDefaults is convenient, and it's a plain property list. Tokens belong in the Keychain, with an accessibility class chosen on purpose.
We still find auth tokens in UserDefaults in apps we review. It's a property list inside the app container: it ends up in unencrypted device backups and is readable on a jailbroken phone.
The Keychain is encrypted by the system, and the accessibility attribute decides when the item can be read and whether it may leave the device:
func saveToken(_ token: String, for account: String) throws {
let base: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: "com.example.app.session",
kSecAttrAccount as String: account,
]
SecItemDelete(base as CFDictionary) // replace any previous value
var item = base
item[kSecValueData as String] = Data(token.utf8)
// Readable after the first unlock (so background refresh works),
// never restored onto a different device from a backup.
item[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
let status = SecItemAdd(item as CFDictionary, nil)
guard status == errSecSuccess else { throw KeychainError.unhandled(status) }
}ThisDeviceOnly means a user restoring a backup to a new phone signs in again. That's usually what you want for a session token, and usually not what you want for a user's saved preferences. It's a product decision, so we raise it rather than pick silently.
This comes from our mobile apps work.