用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
直接命令不会经过审查 Prompt;运行前请先检查来源。
npx skills add https://github.com/tomevault-io/skills-registry --skill swift-auth-security-checklist命令会保持在同一行。复制前请横向滚动并检查完整内容。
想先保存到本地?可下载 SkillsMP 当前能够提供的文件。
正在显示 SKILL.md
基于 SOC 职业分类
| name | swift-auth-security-checklist |
| description | >- Use when this capability is needed. |
Auth security on iOS is not one decision — it's a chain across where the token lives (Keychain class), how long it lives (lifecycle + refresh), what you let in (input validation), how the network fails (retry buckets), what the App Switcher sees (background masking), and how the user signs in (system browser, not embedded WebView). This skill is the checklist that closes each link, each mapped to an Apple doc or RFC. It is provider-agnostic.
kSecAttrAccessibleAlways, plain kSecAttrAccessibleWhenUnlocked for a device-bound
secret).state shape.Announce on invoke: "Using swift-auth-security-checklist to apply the iOS auth defense-in-depth checklist (Keychain class, token lifecycle, OAuth via ASWebAuthenticationSession) per Apple docs + RFC 8252."
Do not use this as a crypto reference — for constant-time comparison, HPKE, ML-KEM, and pinning
internals, defer to swift-post-quantum-security-ios26. This skill is about configuration and
lifecycle, not primitive selection.
| Link | Canonical choice (verified) | Anti-pattern to reject |
|---|---|---|
| Token storage class | kSecClassGenericPassword + kSecAttrAccessibleWhenUnlockedThisDeviceOnly | UserDefaults; kSecAttrAccessibleAlways (deprecated); plain WhenUnlocked for device-bound secrets |
| Biometric-bound (L2) | SecAccessControlCreateWithFlags(..., .biometryCurrentSet, ...) for refresh tokens | gating with app-level passcode in UserDefaults |
| JWT validation | check exp with 30–60 s leeway; negative-only iat leeway; verify iss/aud | accepting future-dated tokens (positive iat leeway) |
| Refresh | single-flight via an actor caching the in-flight Task | one network refresh per concurrent caller (thundering herd) |
| Graceful degradation | notConnectedToInternet → keep UI, disable mutations; 401/invalid_grant → sign out | signing out on a transient network blip |
| Input validation | bound length, reject NUL/control chars, normalize Unicode (NFKC) before send | trusting client validation as authoritative |
| Network retry | retry-with-backoff vs no-retry buckets (below); honor Retry-After on 429 | retrying 4xx; retrying a user-cancelled auth |
| Background masking | swap to an opaque placeholder when scenePhase != .active | leaving token-bearing UI in the App Switcher snapshot |
| Reinstall hygiene | first-launch UserDefaults sentinel; purge stale Keychain on missing sentinel | inheriting a previous install's tokens silently |
| OAuth flow | ASWebAuthenticationSession + PKCE (S256) + state | / for the auth step |
kSecAttrAccessibleWhenUnlockedThisDeviceOnly is the default for tokensApple's docs: items with this class do not migrate to a new device and are absent after restoring
another device's backup — exactly the property you want for a session token. The Always* family is
deprecated (removed in iOS 12); never recommend it. If a token genuinely must be readable before first
unlock, use the AfterFirstUnlockThisDeviceOnly class — not Always. For refresh tokens, layer
SecAccessControlCreateWithFlags with .biometryCurrentSet so the item invalidates on biometric
enrollment changes.
iat leeway must be negative-onlyexp gets a small positive leeway (30–60 s, clock skew). iat must never get positive leeway —
accepting a future-dated iat opens a replay window. Allow slightly-past iat only.
Task, shared by all callersA burst of 401s must trigger exactly one refresh. Cache the in-flight refresh Task inside an actor;
concurrent callers await the same task and receive the same new token. (The audit suite proves this;
the perf suite proves it doesn't cost N×.)
RFC 8252 (OAuth 2.0 for Native Apps) and Apple's guidance both require the system browser for the
authorization step. Use ASWebAuthenticationSession: it shares the system cookie jar (enabling SSO and
fewer password prompts) and guarantees only your app receives the callback. WKWebView is for non-auth
web content only. Set prefersEphemeralWebBrowserSession = true (default is false) only for flows the
user explicitly wants isolated. Always send PKCE and state.
The App Switcher snapshots your foreground view. In SwiftUI, read @Environment(\.scenePhase) and
overlay an opaque placeholder when it isn't .active. (UIKit: observe
UIApplication.willResignActiveNotification.)
import Foundation
import Security
import SwiftUI
// 1. Token storage — canonical class, ThisDeviceOnly.
struct SecureTokenStore {
let service: String, account: String
func save(_ token: Data) throws {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrAccount as String: account,
kSecValueData as String: token,
kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
]
SecItemDelete(query as CFDictionary)
let status = SecItemAdd(query as CFDictionary, nil)
guard status == errSecSuccess else { throw KeychainError.status(status) }
}
}
// 2. Network retry classification — transient (backoff) vs terminal.
extension URLError {
var isTransientRetryable: Bool {
code {
.timedOut, .networkConnectionLost, .dnsLookupFailed,
.cannotConnectToHost, .notConnectedToInternet:
:
}
}
}
{
inFlight: <, >?
( : () -> ) -> {
inFlight { inFlight.value }
task { refresh() }
inFlight task
{ inFlight }
task.value
}
}
: {
(\.scenePhase) scenePhase
body: {
{
()
scenePhase .active {
(.systemBackground).overlay((systemName: ))
}
}
}
}
(: , : ,
: ) {
session (url: authURL,
callbackURLScheme: callbackScheme) { callback, error
}
session.presentationContextProvider (anchor: anchor)
session.prefersEphemeralWebBrowserSession
session.start()
}
URLError.notConnectedToInternet / transient → keep the session, disable mutating actions, retry with backoff.401 / invalid_grant → the refresh token is dead → sign out, route to login.429 → back off, honor Retry-After.4xx other than 401 → no retry (server is authoritative); surface a generic error.global-skills/apple-auth/swift-post-quantum-security-ios26/SKILL.md — constant-time state/MAC
comparison and certificate pinning that this checklist references.global-skills/apple-auth/swift-auth-security-audit-suite/SKILL.md — the tests that prove each
checklist link (storage class, single-flight, anti-enumeration, first-launch purge).global-skills/apple/apple-anti-patterns/SKILL.md — registers "WKWebView for OAuth" and
"kSecAttrAccessibleAlways" as anti-patterns this checklist rejects.Last verified: 2026-06-03 against Apple Security/AuthenticationServices/SwiftUI docs (live) +
RFC 8252. kSecAttrAccessibleWhenUnlockedThisDeviceOnly confirmed "does not migrate to a new device";
prefersEphemeralWebBrowserSession confirmed default false. kSecAttrAccessibleAlways is deprecated
and WKWebView for OAuth violates RFC 8252 — both guarded against here.
Re-check after: WWDC26 + any CryptoKit/Security release, or by 2026-12-01. Decay risk: low.
Found a drift? Run /skill-pattern-freshness-audit apple-auth.
Source: esaldgut/ai-native-engineering-workspace — distributed by TomeVault.
WKWebViewSFSafariViewController