skills/authentication/SKILL.md
Implement iOS authentication flows with AuthenticationServices and LocalAuthentication. Use when building Sign in with Apple, passkey/WebAuthn registration or sign-in with ASAuthorizationPlatformPublicKeyCredentialProvider, ASAuthorizationController credential state and revocation handling, ASWebAuthenticationSession OAuth or third-party login, Password AutoFill, identity-token server validation, or local biometric re-authentication with LAContext.
npx skillsauth add dpearson2699/swift-ios-skills authenticationInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Implement authentication flows on iOS using the AuthenticationServices framework, including Sign in with Apple, passkeys, OAuth/third-party web auth, Password AutoFill, and biometric re-authentication.
Add the "Sign in with Apple" capability in Xcode before using these APIs.
import AuthenticationServices
final class LoginViewController: UIViewController {
func startSignInWithApple() {
let provider = ASAuthorizationAppleIDProvider()
let request = provider.createRequest()
request.requestedScopes = [.fullName, .email]
let controller = ASAuthorizationController(authorizationRequests: [request])
controller.delegate = self
controller.presentationContextProvider = self
controller.performRequests()
}
}
extension LoginViewController: ASAuthorizationControllerPresentationContextProviding {
func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor {
view.window!
}
}
extension LoginViewController: ASAuthorizationControllerDelegate {
func authorizationController(
controller: ASAuthorizationController,
didCompleteWithAuthorization authorization: ASAuthorization
) {
guard let credential = authorization.credential
as? ASAuthorizationAppleIDCredential else { return }
let userID = credential.user // Stable, unique, per-team identifier
let email = credential.email // nil after first authorization
let fullName = credential.fullName // nil after first authorization
let identityToken = credential.identityToken // JWT for server validation
let authCode = credential.authorizationCode // Short-lived code for server exchange
// Save userID to Keychain for credential state checks
// See references/keychain-biometric.md for Keychain patterns
saveUserID(userID)
// Send identityToken and authCode to your server
authenticateWithServer(identityToken: identityToken, authCode: authCode)
}
func authorizationController(
controller: ASAuthorizationController,
didCompleteWithError error: any Error
) {
let authError = error as? ASAuthorizationError
switch authError?.code {
case .canceled:
break // User dismissed
case .failed:
showError("Authorization failed")
case .invalidResponse:
showError("Invalid response")
case .notHandled:
showError("Not handled")
case .notInteractive:
break // Non-interactive request failed -- expected for silent checks
default:
showError("Unknown error")
}
}
}
ASAuthorizationAppleIDCredential properties and their behavior:
| Property | Type | First Auth | Subsequent Auth |
|---|---|---|---|
| user | String | Always | Always |
| email | String? | Provided if requested | nil |
| fullName | PersonNameComponents? | Provided if requested | nil |
| identityToken | Data? | JWT encoded as UTF-8 data | JWT encoded as UTF-8 data |
| authorizationCode | Data? | Short-lived code | Short-lived code |
| realUserStatus | ASUserDetectionStatus | Fraud-prevention signal | Do not rely on later attempts |
Critical: email and fullName are provided ONLY on the first
authorization. Cache them immediately during the initial sign-up flow. If the
user later deletes and re-adds the app, these values will not be returned.
func handleCredential(_ credential: ASAuthorizationAppleIDCredential) {
// Always persist the user identifier
let userID = credential.user
// Cache name and email IMMEDIATELY -- only available on first auth
if let fullName = credential.fullName {
let name = PersonNameComponentsFormatter().string(from: fullName)
UserProfile.saveName(name) // Persist to your backend
}
if let email = credential.email {
UserProfile.saveEmail(email) // Persist to your backend
}
}
Check credential state on every app launch. The user may revoke access at any time via Settings > Apple Account > Sign-In & Security.
func checkCredentialState() {
let provider = ASAuthorizationAppleIDProvider()
guard let userID = loadSavedUserID() else {
showLoginScreen()
return
}
provider.getCredentialState(forUserID: userID) { state, _ in
DispatchQueue.main.async {
switch state {
case .authorized:
proceedToMainApp()
case .revoked:
// User revoked -- sign out and clear local data
signOut()
showLoginScreen()
case .notFound:
showLoginScreen()
case .transferred:
// App transferred to new team -- migrate user identifier
migrateUser()
@unknown default:
showLoginScreen()
}
}
}
}
NotificationCenter.default.addObserver(
forName: ASAuthorizationAppleIDProvider.credentialRevokedNotification,
object: nil,
queue: .main
) { _ in
// Sign out immediately
AuthManager.shared.signOut()
}
The identityToken is a JWT. Send it to your server for validation --
never trust it client-side alone.
func sendTokenToServer(credential: ASAuthorizationAppleIDCredential) async throws {
guard let tokenData = credential.identityToken,
let token = String(data: tokenData, encoding: .utf8),
let authCodeData = credential.authorizationCode,
let authCode = String(data: authCodeData, encoding: .utf8) else {
throw AuthError.missingToken
}
var request = URLRequest(url: URL(string: "https://api.example.com/auth/apple")!)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpBody = try JSONEncoder().encode(
["identityToken": token, "authorizationCode": authCode]
)
let (data, response) = try await URLSession.shared.data(for: request)
guard (response as? HTTPURLResponse)?.statusCode == 200 else {
throw AuthError.serverValidationFailed
}
let session = try JSONDecoder().decode(SessionResponse.self, from: data)
// Store session token in Keychain -- see references/keychain-biometric.md
try KeychainHelper.save(session.accessToken, forKey: "accessToken")
}
Server-side, validate the JWT against Apple's public keys at
https://appleid.apple.com/auth/keys (JWKS). Verify: iss is
https://appleid.apple.com, aud matches your bundle ID, exp not passed.
On launch, silently check for existing Sign in with Apple and password credentials before showing a login screen:
func performExistingAccountSetupFlows() {
let appleIDRequest = ASAuthorizationAppleIDProvider().createRequest()
let passwordRequest = ASAuthorizationPasswordProvider().createRequest()
let controller = ASAuthorizationController(
authorizationRequests: [appleIDRequest, passwordRequest]
)
controller.delegate = self
controller.presentationContextProvider = self
controller.performRequests(
options: .preferImmediatelyAvailableCredentials
)
}
Call this in viewDidAppear or on app launch. If no existing credentials
are found, the delegate receives a .notInteractive error -- handle it
silently and show your normal login UI.
Use passkeys for passwordless WebAuthn-style registration and sign-in. The
app must have an Associated Domains entitlement for the relying party domain
using the webcredentials: service; passkey requests fail for services the app
has not configured as associated domains.
For platform passkeys synced through iCloud Keychain, request a server-provided
challenge and create requests with ASAuthorizationPlatformPublicKeyCredentialProvider:
let challenge: Data = try await server.registrationChallenge()
let userID: Data = try await server.passkeyUserID()
let provider = ASAuthorizationPlatformPublicKeyCredentialProvider(
relyingPartyIdentifier: "example.com"
)
let request = provider.createCredentialRegistrationRequest(
challenge: challenge,
name: username,
userID: userID
)
let controller = ASAuthorizationController(authorizationRequests: [request])
controller.delegate = self
controller.presentationContextProvider = self
controller.performRequests()
For sign-in, use createCredentialAssertionRequest(challenge:) with a fresh
server challenge, then send the resulting registration or assertion object to
the relying-party server for verification:
let request = provider.createCredentialAssertionRequest(challenge: challenge)
switch authorization.credential {
case let registration as ASAuthorizationPlatformPublicKeyCredentialRegistration:
try await server.finishPasskeyRegistration(registration)
case let assertion as ASAuthorizationPlatformPublicKeyCredentialAssertion:
try await server.finishPasskeySignIn(assertion)
default:
break
}
For inline passkey suggestions, set the username field's textContentType to
.username, include the passkey assertion request in the controller, and call
performAutoFillAssistedRequests(). Use ASAuthorizationSecurityKeyPublicKeyCredentialProvider
only when the user must authenticate with a physical security key. See
references/passkeys.md for complete registration,
assertion, AutoFill, and security-key patterns.
Use ASWebAuthenticationSession for OAuth and third-party authentication
(Google, GitHub, etc.). Never use WKWebView for auth flows.
import AuthenticationServices
final class OAuthController: NSObject, ASWebAuthenticationPresentationContextProviding {
private weak var presentationAnchor: ASPresentationAnchor?
init(presentationAnchor: ASPresentationAnchor) {
self.presentationAnchor = presentationAnchor
}
func startOAuthFlow() {
let authURL = URL(string:
"https://provider.com/oauth/authorize?client_id=YOUR_ID&redirect_uri=myapp://callback&response_type=code"
)!
let session = ASWebAuthenticationSession(
url: authURL, callback: .customScheme("myapp")
) { callbackURL, error in
guard let callbackURL, error == nil,
let code = URLComponents(url: callbackURL, resolvingAgainstBaseURL: false)?
.queryItems?.first(where: { $0.name == "code" })?.value else { return }
Task { await self.exchangeCodeForTokens(code) }
}
session.presentationContextProvider = self
session.prefersEphemeralWebBrowserSession = true // No shared cookies
session.start()
}
func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor {
guard let presentationAnchor else {
fatalError("ASWebAuthenticationSession needs the active window")
}
return presentationAnchor
}
}
In SwiftUI, use @Environment(\.webAuthenticationSession) and call
authenticate(using:callback:preferredBrowserSession:additionalHeaderFields:)
with .customScheme("myapp") or .https(host:path:); prefer .ephemeral
only when the provider flow should avoid shared browser cookies.
Use ASAuthorizationPasswordProvider to offer saved keychain credentials
alongside Sign in with Apple:
func performSignIn() {
let appleIDRequest = ASAuthorizationAppleIDProvider().createRequest()
appleIDRequest.requestedScopes = [.fullName, .email]
let passwordRequest = ASAuthorizationPasswordProvider().createRequest()
let controller = ASAuthorizationController(
authorizationRequests: [appleIDRequest, passwordRequest]
)
controller.delegate = self
controller.presentationContextProvider = self
controller.performRequests()
}
// In delegate:
func authorizationController(
controller: ASAuthorizationController,
didCompleteWithAuthorization authorization: ASAuthorization
) {
switch authorization.credential {
case let appleIDCredential as ASAuthorizationAppleIDCredential:
handleAppleIDLogin(appleIDCredential)
case let passwordCredential as ASPasswordCredential:
// User selected a saved password from keychain
signInWithPassword(
username: passwordCredential.user,
password: passwordCredential.password
)
default:
break
}
}
Set textContentType on text fields for AutoFill to work:
usernameField.textContentType = .username
passwordField.textContentType = .password
Use LAContext from LocalAuthentication for local re-authentication before
showing account settings or starting sensitive actions. Do not treat a returned
Bool as proof to unlock a stored secret; protect secrets with Keychain access
control instead. See references/keychain-biometric.md
for SecAccessControl and .biometryCurrentSet patterns.
import LocalAuthentication
func authenticateWithBiometrics() async throws -> Bool {
let context = LAContext()
var error: NSError?
guard context.canEvaluatePolicy(
.deviceOwnerAuthenticationWithBiometrics, error: &error
) else {
throw AuthError.biometricsUnavailable
}
return try await context.evaluatePolicy(
.deviceOwnerAuthenticationWithBiometrics,
localizedReason: "Sign in to your account"
)
}
Required: Add NSFaceIDUsageDescription to Info.plist. Missing this
key crashes on Face ID devices.
This skill owns user-facing account authentication: Sign in with Apple,
passkeys, Password AutoFill, ASAuthorizationController, OAuth session
presentation, credential state, and local biometric re-authentication. Route
deep security work to swift-security: Keychain architecture/migration,
CryptoKit, Secure Enclave, certificate pinning/trust, keychain sharing, storage
hardening, and OWASP MASVS/MASTG. Keep only the storage minimum here: tokens and
secrets belong in Keychain; LAContext.evaluatePolicy alone must not release
protected secrets.
Use SignInWithAppleButton in SwiftUI views when the login surface is SwiftUI.
Request .fullName and .email, handle .success and .failure, downcast to
ASAuthorizationAppleIDCredential, and send the credential through the same
server-validation flow as UIKit. Style with .signInWithAppleButtonStyle(...).
.notInteractive as the normal "no local credential" path.email or fullName. Cache them on first authorization and
handle nil later.ASAuthorizationController without a presentation context
provider. Authorization UI needs the active presentation anchor.UserDefaults, files, or Core Data. Store secrets in
Keychain and keep relying-party passkey verification server-side.webcredentials: Associated Domains for the
relying-party domain, or trying to use app-native passkeys for unrelated
websites.swift-security.ASAuthorizationControllerPresentationContextProviding implementedgetCredentialState(forUserID:completion:))credentialRevokedNotification observer registered; sign-out handledemail and fullName cached on first authorization (not assumed available later)identityToken sent to server for validation, not trusted client-side onlyperformExistingAccountSetupFlows called before showing login UI.canceled, .failed, .notInteractiveNSFaceIDUsageDescription in Info.plist for biometric authASWebAuthenticationSession used for OAuth (not WKWebView)prefersEphemeralWebBrowserSession set for OAuth when appropriatetextContentType set on username/password fields for AutoFillwebcredentials: Associated Domains configuredswift-securitydevelopment
Implement, review, or improve data visualizations using Swift Charts. Use when building bar, line, area, point, pie, donut, or iOS 26 3D charts; when adding chart selection, scrolling, annotations, axes, scales, legends, or foregroundStyle grouping; when plotting functions with BarPlot, LinePlot, AreaPlot, PointPlot, Chart3D, or SurfacePlot; or when creating heat maps, Gantt charts, grouped bars, sparklines, threshold lines, or spatial visualizations.
data-ai
Select, implement, or migrate between app architecture patterns for Apple platform apps. Use when choosing between MV (Model-View with @Observable), MVVM, MVI, TCA (The Composable Architecture), Clean Architecture, VIPER, or Coordinator patterns; when evaluating architecture fit for a feature's complexity; when migrating from one pattern to another; or when reviewing whether an app's current architecture is appropriate. Scoped to Apple-platform patterns using Swift 6.3, SwiftUI, and UIKit.
development
Apply Swift API Design Guidelines to name, label, and document Swift APIs. Covers argument label rules (prepositional phrase rule, grammatical phrase rule, first-label omission), mutating/nonmutating pair naming (-ed/-ing participle pattern, form- prefix, sort/sorted, formUnion/union), side-effect naming (noun for pure, verb for mutating), documentation comment structure (summary by declaration kind, O(1) complexity rule), clarity at call site, role-based naming, protocol naming (-able/-ible/-ing), default arguments over method families, casing conventions, and terminology. Use when designing new Swift APIs, reviewing naming and argument labels, writing documentation comments, or refactoring for call site clarity.
development
Implement, review, or improve in-app purchases and subscriptions using StoreKit 2. Use when building paywalls with SubscriptionStoreView or ProductView, processing transactions with Product and Transaction APIs, verifying entitlements, handling purchase flows (consumable, non-consumable, auto-renewable), implementing offer codes or promotional/win-back/introductory offers, managing subscription status and renewal state, setting up StoreKit testing with configuration files, or integrating Family Sharing, Ask to Buy, refund handling, and billing retry logic.