Skip to content

Secure token storage ​

Hub › iOS › Advanced › Secure token storage

Goal

Build a Keychain wrapper to store Apple identity tokens securely, and an @Observable token manager that surfaces auth state to the view layer.

Prerequisites

Why Keychain ​

UserDefaults is insecure for tokens — they're stored in plaintext in the app's preferences plist. The iOS Keychain is encrypted at rest, accessible only by your app, and automatically protected by the device passcode.

Keychain wrapper ​

Create KeychainService.swift:

swift
import Foundation
import Security

enum KeychainService {
    static func store(key: String, data: Data) {
        let query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrAccount as String: key,
            kSecValueData as String: data,
            kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
        ]
        SecItemDelete(query as CFDictionary)
        SecItemAdd(query as CFDictionary, nil)
    }

    static func read(key: String) -> Data? {
        let query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrAccount as String: key,
            kSecReturnData as String: true,
        ]
        var result: CFTypeRef?
        SecItemCopyMatching(query as CFDictionary, &result)
        return result as? Data
    }

    static func delete(key: String) {
        let query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrAccount as String: key,
        ]
        SecItemDelete(query as CFDictionary)
    }
}

Token manager ​

Create TokenManager.swift:

swift
import Foundation
import Observation

@Observable
final class TokenManager {
    static let shared = TokenManager()

    private(set) var isSignedIn = false
    private(set) var userName: String = ""
    private(set) var userID: String = ""

    private init() {
        loadFromKeychain()
    }

    func storeAppleToken(userID: String, name: String, token: Data) {
        self.userID = userID
        self.userName = name
        self.isSignedIn = true

        let dict = ["userID": userID, "name": name]
        if let data = try? JSONSerialization.data(withJSONObject: dict) {
            KeychainService.store(key: "apple_user", data: data)
        }
        KeychainService.store(key: "apple_token", data: token)
    }

    func signOut() {
        KeychainService.delete(key: "apple_user")
        KeychainService.delete(key: "apple_token")
        isSignedIn = false
        userName = ""
        userID = ""
    }

    private func loadFromKeychain() {
        guard let data = KeychainService.read(key: "apple_user"),
              let dict = try? JSONSerialization.jsonObject(with: data) as? [String: String],
              let uid = dict["userID"]
        else { return }
        userID = uid
        userName = dict["name"] ?? ""
        isSignedIn = true
    }
}

Use across the app ​

Now any view can observe auth state reactively:

swift
struct ContentView: View {
    @Environment(\.modelContext) private var modelContext
    @State private var viewModel: ItemViewModel?
    @State private var showSignIn = false

    var body: some View {
        VStack {
            if TokenManager.shared.isSignedIn {
                HStack {
                    Text("Hi, \(TokenManager.shared.userName)")
                        .font(.caption)
                    Button("Sign Out") { TokenManager.shared.signOut() }
                        .font(.caption)
                }
            }
            // ... list ...
        }
        .sheet(isPresented: $showSignIn) {
            SignInView()
        }
        .onAppear {
            if !TokenManager.shared.isSignedIn { showSignIn = true }
            viewModel = ItemViewModel(modelContext: modelContext)
        }
    }
}

Checkpoint ​

Run the app. Sign in with Apple — close and reopen the app. The user stays signed in (no sign-in prompt). Tap "Sign Out" — the token is deleted from the Keychain, and the sign-in sheet reappears.

Next: Unit tests — XCTest for ViewModel + Model layers.