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.
Auth tokens in UserDefaults are one of the most common findings in iOS app reviews. 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.