Swift auth security checklist
Skill esaldgut/ai-native-engineering-workspace/global-skills/apple-auth/swift-auth-security-checklist
AI-native engineering workspace — 42 Claude Code agent skills, platform-base workflow docs, and a freshness system that re-verifies each pattern against vendor docs.
npx -y skills add esaldgut/ai-native-engineering-workspace --skill swift-auth-security-checklistAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
A defense-in-depth checklist for iOS auth across storage, transport, lifecycle, and UX — Keychain protection levels (kSecAttrAccessibleWhenUnlockedThisDeviceOnly is canonical; the Always* family is deprecated), token lifecycle (validate JWT `exp` with leeway, NEVER positive `iat` leeway, coalesce refresh), input-validation defense-in-depth, URLError retry buckets, background masking via scenePhase, reinstall hygiene, and OAuth hardening (ASWebAuthenticationSession + PKCE + state — NOT WKWebView, per RFC 8252). Use when designing or reviewing token storage, refresh, or federated sign-in.
SKILL.md
12.4 KB, ~2.5k tokens by cl100k_base, as published. Nobody here has run it
Auth security checklist (iOS, defense-in-depth)
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.
When to invoke
- You're choosing a Keychain accessibility class for a token, or reviewing one that looks too
permissive (
kSecAttrAccessibleAlways, plainkSecAttrAccessibleWhenUnlockedfor a device-bound secret). - You're implementing token refresh, JWT validation, or graceful degradation on network failure.
- You're adding federated/OAuth sign-in and need the system-browser + PKCE +
stateshape. - You're hardening the app against shoulder-surfing / App Switcher snapshots.
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.
The checklist
| 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 | WKWebView / SFSafariViewController for the auth step |
The rules
1. kSecAttrAccessibleWhenUnlockedThisDeviceOnly is the default for tokens
Apple'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.
2. JWT iat leeway must be negative-only
exp 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.
3. Coalesce refresh — one in-flight Task, shared by all callers
A 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×.)
4. Use the system browser for OAuth — never an embedded WebView
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.
5. Mask sensitive UI in the background
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.)
Canonical example
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 {
switch code {
case .timedOut, .networkConnectionLost, .dnsLookupFailed,
.cannotConnectToHost, .notConnectedToInternet: return true
default: return false // .userCancelledAuthentication, .badServerResponse, .cancelled → NO retry
}
}
}
// 3. Single-flight refresh.
actor TokenRefresher {
private var inFlight: Task<String, Error>?
func freshToken(using refresh: @escaping () async throws -> String) async throws -> String {
if let inFlight { return try await inFlight.value } // coalesce
let task = Task { try await refresh() }
inFlight = task
defer { inFlight = nil }
return try await task.value
}
}
// 4. Background masking via scenePhase.
struct RootView: View {
@Environment(\.scenePhase) private var scenePhase
var body: some View {
ZStack {
MainAppView()
if scenePhase != .active {
Color(.systemBackground).overlay(Image(systemName: "lock.fill")) // App Switcher safe
}
}
}
}
// 5. OAuth via the SYSTEM browser — not WKWebView.
func startOAuth(authURL: URL, callbackScheme: String,
anchor: ASPresentationAnchor) {
let session = ASWebAuthenticationSession(url: authURL, // authURL carries PKCE S256 + state
callbackURLScheme: callbackScheme) { callback, error in
// validate `state` (constant-time) + exchange `code` with PKCE verifier
}
session.presentationContextProvider = PresentationProvider(anchor: anchor)
session.prefersEphemeralWebBrowserSession = false // default; set true only for isolated flows
session.start()
}
Decision aid: graceful degradation on refresh failure
URLError.notConnectedToInternet/ transient → keep the session, disable mutating actions, retry with backoff.- HTTP
401/invalid_grant→ the refresh token is dead → sign out, route to login. - HTTP
429→ back off, honorRetry-After. 4xxother than 401 → no retry (server is authoritative); surface a generic error.
Related skills
global-skills/apple-auth/swift-post-quantum-security-ios26/SKILL.md— constant-timestate/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.
Sources
- kSecAttrAccessibleWhenUnlockedThisDeviceOnly · SecAccessControlCreateWithFlags · Apple Platform Security — Keychain data protection
- ASWebAuthenticationSession · prefersEphemeralWebBrowserSession · ScenePhase · URLError
- RFC 8252 OAuth 2.0 for Native Apps · RFC 7636 PKCE · OpenID Connect Core 1.0 · OWASP MASTG Certificate Pinning
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.
Gives 0 of the 12 instructions most security skills give in ~2.5k tokens
Counted across 648 of the 828 authors here whose files we hold, read 2026-08-06
- parameterize all database queriesin 67 of 648, across 49 files
- hash passwords using bcrypt scrypt or argon2in 48 of 648, across 35 files
- apply rate limiting to authentication endpointsin 48 of 648, across 24 files
- Configure security headersin 35 of 648, across 18 files
- validate all inputsin 32 of 648, across 24 files
- validate all external input at the system boundaryin 29 of 648, across 18 files
- run containers as a non-root userin 28 of 648, across 15 files
- use httponly secure samesite cookies for sessionsin 26 of 648, across 15 files
- run dependency audits before every releasein 21 of 648, across 10 files
- encode output to prevent cross-site scriptingin 21 of 648, across 10 files
- copy dependencies before source codein 20 of 648, across 9 files
- store secrets in environment variablesin 20 of 648, across 17 files
Said here and by no other author read
- Store tokens using ThisDeviceOnly accessibility classes
- Bind refresh tokens with biometric access control flags
- Validate JWT exp with positive leeway
- Coalesce concurrent token refreshes into one task
- Use the system browser for OAuth authentication flows
- Mask sensitive UI when scene phase is inactive
Grouped from the skills themselves: near-identical wordings counted once, and counted by distinct author, so one author publishing three of these counts once. Length counted with cl100k_base; the agent that loads this file may tokenize it differently.