CtrlK
BlogDocsLog inGet started
Tessl Logo

dpearson2699/swift-ios-skills

Agent skills for iOS, iPadOS, Swift, SwiftUI, and modern Apple framework development.

90

Quality

90%

Does it follow best practices?

Impact

Average score across 248 eval scenarios

SecuritybySnyk

Advisory

Suggest reviewing before use

Overview
Quality
Evals
Security
Files

keychain-biometric.mdskills/authentication/references/

Keychain Token Storage & Biometric Authentication

Self-contained reference for storing authentication tokens in Keychain and protecting them with Keychain-bound biometric authentication (Face ID / Touch ID). Covers the patterns most commonly needed alongside Sign in with Apple, passkey, and OAuth flows.

This is an authentication-adjacent quick reference, not a full security architecture guide. Route Keychain migration, access-control policy design, CryptoKit encryption, Secure Enclave keys, certificate pinning, keychain sharing, storage-hardening strategy, and OWASP MASVS/MASTG mapping to swift-security.

Contents

  • Storing Tokens in Keychain
  • Reading Tokens from Keychain
  • Deleting Tokens from Keychain
  • Biometric Authentication with LAContext
  • Biometric-Protected Keychain Items
  • Keychain Error Handling
  • References

Storing Tokens in Keychain

The Keychain is the ONLY correct place to store tokens, passwords, API keys, or secrets. Never store these in UserDefaults, files, or Core Data.

func saveToKeychain(account: String, data: Data, service: String) throws {
    let query: [String: Any] = [
        kSecClass as String: kSecClassGenericPassword,
        kSecAttrAccount as String: account,
        kSecAttrService as String: service,
        kSecValueData as String: data,
        kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
    ]

    let status = SecItemAdd(query as CFDictionary, nil)

    if status == errSecDuplicateItem {
        let updateQuery: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrAccount as String: account,
            kSecAttrService as String: service
        ]
        let updates: [String: Any] = [kSecValueData as String: data]
        let updateStatus = SecItemUpdate(updateQuery as CFDictionary, updates as CFDictionary)
        guard updateStatus == errSecSuccess else {
            throw KeychainError.updateFailed(updateStatus)
        }
    } else if status != errSecSuccess {
        throw KeychainError.saveFailed(status)
    }
}

Reading Tokens from Keychain

func readFromKeychain(account: String, service: String) throws -> Data {
    let query: [String: Any] = [
        kSecClass as String: kSecClassGenericPassword,
        kSecAttrAccount as String: account,
        kSecAttrService as String: service,
        kSecReturnData as String: true,
        kSecMatchLimit as String: kSecMatchLimitOne
    ]

    var result: AnyObject?
    let status = SecItemCopyMatching(query as CFDictionary, &result)

    guard status == errSecSuccess, let data = result as? Data else {
        throw KeychainError.readFailed(status)
    }
    return data
}

Deleting Tokens from Keychain

func deleteFromKeychain(account: String, service: String) throws {
    let query: [String: Any] = [
        kSecClass as String: kSecClassGenericPassword,
        kSecAttrAccount as String: account,
        kSecAttrService as String: service
    ]

    let status = SecItemDelete(query as CFDictionary)
    guard status == errSecSuccess || status == errSecItemNotFound else {
        throw KeychainError.deleteFailed(status)
    }
}

Use a ThisDeviceOnly accessibility class for sensitive app credentials unless the product explicitly requires restore or sharing behavior. Choose broader Keychain accessibility, migration, or sharing strategy in swift-security, not in this authentication skill.

Do not use kSecAttrAccessibleAlways for app credentials; Apple marks it as not recommended for application use because items remain accessible regardless of lock state.

Biometric Authentication with LAContext

Use LAContext from LocalAuthentication for Face ID / Touch ID prompts before showing sensitive screens or performing protected actions. This is a local interaction gate, not proof that a stored secret is safe to release. For tokens, private keys, or high-value credentials, bind access to the Keychain item with SecAccessControl and let SecItemCopyMatching perform the authentication.

import LocalAuthentication

func authenticateWithBiometrics() async throws -> Bool {
    let context = LAContext()
    var error: NSError?

    guard context.canEvaluatePolicy(
        .deviceOwnerAuthenticationWithBiometrics, error: &error
    ) else {
        // Biometrics not available -- fall back to passcode
        if context.canEvaluatePolicy(.deviceOwnerAuthentication, error: &error) {
            return try await context.evaluatePolicy(
                .deviceOwnerAuthentication,
                localizedReason: "Authenticate to access your account"
            )
        }
        throw AuthError.biometricsUnavailable
    }

    return try await context.evaluatePolicy(
        .deviceOwnerAuthenticationWithBiometrics,
        localizedReason: "Authenticate to access your account"
    )
}

Info.plist Requirement

You MUST include NSFaceIDUsageDescription in Info.plist:

<key>NSFaceIDUsageDescription</key>
<string>Authenticate to access your secure data</string>

Missing this key causes a crash on Face ID devices.

LAContext Configuration

let context = LAContext()
context.localizedFallbackTitle = "Use Passcode"
context.touchIDAuthenticationAllowableReuseDuration = 30
let currentState = context.evaluatedPolicyDomainState // Compare to detect enrollment changes

Biometric-Protected Keychain Items

Protect keychain items so they require biometric authentication to read:

let access = SecAccessControlCreateWithFlags(
    nil,
    kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly,
    .biometryCurrentSet,
    nil
)!

let query: [String: Any] = [
    kSecClass as String: kSecClassGenericPassword,
    kSecAttrAccount as String: "auth-token",
    kSecValueData as String: tokenData,
    kSecAttrAccessControl as String: access
]

Read the protected item with an authentication context so Keychain, not app logic, controls release of the secret:

let context = LAContext()
context.localizedReason = "Authenticate to access your account"

let query: [String: Any] = [
    kSecClass as String: kSecClassGenericPassword,
    kSecAttrAccount as String: "auth-token",
    kSecReturnData as String: true,
    kSecMatchLimit as String: kSecMatchLimitOne,
    kSecUseAuthenticationContext as String: context
]

var result: AnyObject?
let status = SecItemCopyMatching(query as CFDictionary, &result)
guard status == errSecSuccess, let tokenData = result as? Data else {
    throw KeychainError.readFailed(status)
}

Use .biometryCurrentSet when an auth token must be invalidated after biometric enrollment changes. Route broader SecAccessControl flag selection and policy tradeoffs to swift-security.

Keychain Error Handling

enum KeychainError: Error {
    case saveFailed(OSStatus)
    case updateFailed(OSStatus)
    case readFailed(OSStatus)
    case deleteFailed(OSStatus)

    var localizedDescription: String {
        switch self {
        case .saveFailed(let status),
             .updateFailed(let status),
             .readFailed(let status),
             .deleteFailed(let status):
            return SecCopyErrorMessageString(status, nil) as String? ?? "Unknown error"
        }
    }
}

References

skills

README.md

tile.json