Agent skills for iOS, iPadOS, Swift, SwiftUI, and modern Apple framework development.
90
90%
Does it follow best practices?
Impact
—
Average score across 248 eval scenarios
Advisory
Suggest reviewing before use
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.
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)
}
}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
}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.
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"
)
}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.
let context = LAContext()
context.localizedFallbackTitle = "Use Passcode"
context.touchIDAuthenticationAllowableReuseDuration = 30
let currentState = context.evaluatedPolicyDomainState // Compare to detect enrollment changesProtect 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.
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"
}
}
}.tessl-plugin
skills
accessorysetupkit
references
activitykit
references
adattributionkit
references
alarmkit
references
app-clips
app-intents
references
app-store-optimization
app-store-review
apple-on-device-ai
appmigrationkit
references
audioaccessorykit
references
authentication
references
avkit
references
background-processing
references
browserenginekit
references
callkit
references
carplay
references
cloudkit
references
contacts-framework
references
core-bluetooth
references
core-data
core-motion
references
core-nfc
references
coreml
references
cryptokit
references
cryptotokenkit
references
debugging-instruments
device-integrity
references
dockkit
references
energykit
references
eventkit
references
financekit
references
focus-engine
gamekit
references
healthkit
references
homekit
references
ios-accessibility
ios-localization
ios-networking
ios-simulator
references
mapkit
metrickit
references
musickit
references
natural-language
references
paperkit
references
passkit
references
pdfkit
references
pencilkit
references
permissionkit
references
photokit
push-notifications
realitykit
references
relevancekit
references
scenekit
references
sensorkit
references
speech-recognition
references
spritekit
references
storekit
swift-api-design-guidelines
swift-architecture
swift-charts
references
swift-codable
swift-concurrency
swift-formatstyle
swift-language
swift-security
references
swift-testing
swiftdata
swiftlint
swiftui-animation
swiftui-gestures
references
swiftui-layout-components
swiftui-liquid-glass
references
swiftui-patterns
swiftui-performance
swiftui-uikit-interop
swiftui-webkit
tabletopkit
references
tipkit
references
vision-framework
weatherkit
references
widgetkit
references