CtrlK
BlogDocsLog inGet started
Tessl Logo

dpearson2699/swift-ios-skills

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

71

Quality

89%

Does it follow best practices?

Impact

No eval scenarios have been run

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 biometric authentication (Face ID / Touch ID). Covers the patterns most commonly needed alongside Sign in with Apple and OAuth flows.

Contents

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)
    }
}

kSecAttrAccessible Values

ValueWhen AvailableDevice-OnlyUse For
kSecAttrAccessibleWhenUnlockedDevice unlockedNoGeneral credentials
kSecAttrAccessibleWhenUnlockedThisDeviceOnlyDevice unlockedYesSensitive credentials
kSecAttrAccessibleAfterFirstUnlockAfter first unlockNoBackground-accessible tokens
kSecAttrAccessibleAfterFirstUnlockThisDeviceOnlyAfter first unlockYesBackground tokens, no backup
kSecAttrAccessibleWhenPasscodeSetThisDeviceOnlyPasscode set + unlockedYesHighest security

Rules:

  • Use ThisDeviceOnly variants for sensitive data. Prevents backup/restore to other devices.
  • Use AfterFirstUnlock for tokens needed by background operations.
  • Use WhenPasscodeSetThisDeviceOnly for most sensitive data. Item is deleted if passcode is removed.
  • NEVER use kSecAttrAccessibleAlways (deprecated and insecure).

Biometric Authentication with LAContext

Use LAContext from LocalAuthentication for Face ID / Touch ID prompts before accessing sensitive data or performing protected actions.

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,
    kSecUseAuthenticationContext as String: LAContext()
]

SecAccessControl Flags

FlagBehavior
.biometryCurrentSetRequires biometry, invalidated if enrollment changes. Most secure.
.biometryAnyRequires biometry, survives enrollment changes.
.userPresenceBiometry or passcode. Most flexible.

Use .biometryCurrentSet for high-security items (tokens, keys). Use .userPresence when you want to allow passcode fallback without a separate LAContext evaluation.

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"
        }
    }
}

skills

authentication

CHANGELOG.md

README.md

tile.json