一键导入
ios-auth-keychain-storage
MANDATORY for auth token storage. Use before writing any code that stores, reads, or deletes authentication credentials.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
MANDATORY for auth token storage. Use before writing any code that stores, reads, or deletes authentication credentials.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
MANDATORY for API client and Services work. Use before writing APIClient, any file under Services/, or any networking code.
MANDATORY when adding or updating icons in AppIcon.appiconset/. Enforces 1024x1024 sizing and the per-variant alpha rules (primary opaque, dark transparent, tinted opaque grayscale).
MANDATORY when the user asks to add or scaffold a new feature or screen. Chains Model + ViewModel + View + APIClient extension + test stub in one pass.
MANDATORY for Info.plist work. Use before adding imports that require user permission (PhotosUI, AVFoundation camera, CoreLocation, ATT, HealthKit, Contacts, EventKit).
MANDATORY for iOS project initialization and project.yml work. Use before creating or editing project.yml, Package.swift, or the top-level directory layout.
MANDATORY for any AsyncImage rendering a URL from a JSON response. Use before writing AsyncImage(url:) anywhere in the View layer.
| name | ios-auth-keychain-storage |
| description | MANDATORY for auth token storage. Use before writing any code that stores, reads, or deletes authentication credentials. |
userdefaults-token hook.kSecClassGenericPassword with a fixed kSecAttrAccount value (e.g. "auth_token"). One token per account name.kSecAttrAccessible: kSecAttrAccessibleAfterFirstUnlock — the token is readable when the app is backgrounded, but not before first unlock after reboot. Required for background refresh to work.save(token:), getToken() -> String?, deleteToken(). No variants, no convenience overloads.SecItemCopyMatching returns non-zero, return nil from getToken(). Don't throw — the only sane response to a broken Keychain is to force re-login.print, not in error messages, not in analytics. Even in debug builds.APIClient calls KeychainService.shared.getToken() on every request. Feature code should never read the token directly.Services/KeychainService.swift.UserDefaults writes to ~/Library/Preferences/<bundle-id>.plist as unencrypted XML. Anyone with filesystem access (jailbroken device, backup extraction, or xcrun simctl on simulator) reads tokens in plain text.
Keychain:
final class KeychainService {
static let shared = KeychainService()
private let account = "auth_token"
private let service = Bundle.main.bundleIdentifier ?? "com.yourcompany.yourapp"
func save(token: String) {
let data = Data(token.utf8)
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: account,
kSecAttrService as String: service,
]
SecItemDelete(query as CFDictionary) // remove any existing
var attrs = query
attrs[kSecValueData as String] = data
attrs[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlock
SecItemAdd(attrs as CFDictionary, nil)
}
func getToken() -> String? {
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 { return nil }
return String(data: data, encoding: .utf8)
}
func deleteToken() {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: account,
kSecAttrService as String: service,
]
SecItemDelete(query as CFDictionary)
}
}
See <plugin-root>/templates/KeychainService.swift for the canonical drop-in implementation.
func login(email: String, password: String) async throws {
let response: DataResponse<LoginResponse> = try await APIClient.shared.post(
path: "/auth/login",
body: LoginRequest(email: email, password: password)
)
KeychainService.shared.save(token: response.data.token)
AppState.shared.isAuthenticated = true
}
func logout() {
KeychainService.shared.deleteToken()
AppState.shared.isAuthenticated = false
}
Anti-pattern (blocked by the userdefaults-token hook):
// ❌ NEVER
UserDefaults.standard.set(token, forKey: "auth_token")
Fix: KeychainService.shared.save(token:). There is no scenario where UserDefaults for a token is correct. The userdefaults-token hook rejects any Write/Edit that stores token, password, secret, apiKey, bearer, or credential values in UserDefaults.
kSecAttrAccessibleAlways or WhenUnlockedkSecAttrAccessibleAlways is deprecated and downgrades security.kSecAttrAccessibleWhenUnlocked breaks background requests because the token is unreadable while the device is locked.Fix: kSecAttrAccessibleAfterFirstUnlock.
Symptom: Logging in with a new account returns the old account's token.
Cause: SecItemAdd fails silently with errSecDuplicateItem if the account already exists.
Fix: Always SecItemDelete before SecItemAdd (as shown above), or use SecItemUpdate.
// ❌
print("Auth failed with token: \(token)")
Fix: Log token presence (token == nil), not the token itself.
See <plugin-root>/templates/KeychainService.swift for the canonical implementation. Copy into Services/KeychainService.swift on day 1 — before writing any auth flow.