All skills

Generates authentication infrastructure with Sign in with Apple, biometrics, and Keychain storage. Use when user wants to add authentication, login, or Sign in with Apple.

Use this Skill: https://skilld.dev/gh/rshankras/claude-code-apple-skills/auth-flow

This session only. Nothing lands on disk.

auth-patterns.md

≈2.2k tokens on demand. Your agent reads this file only when SKILL.md points to it.

Authentication Patterns and Best Practices

Sign in with Apple

How It Works

  1. User taps "Sign in with Apple" button
  2. System shows Apple ID authorization sheet
  3. User authenticates (Face ID, Touch ID, or password)
  4. User chooses to share or hide email
  5. App receives user ID, name (first time only), and email

Key Points

  • User ID is stable - Same for your app across devices
  • Name only on first sign-in - Store it immediately
  • Email can be private - Apple provides relay address
  • Must handle revocation - User can revoke access

Credential State Check

Always check credential state on app launch:

func checkCredentialState(userID: String) async -> ASAuthorizationAppleIDProvider.CredentialState {
    await withCheckedContinuation { continuation in
        ASAuthorizationAppleIDProvider().getCredentialState(forUserID: userID) { state, _ in
            continuation.resume(returning: state)
        }
    }
}

// On app launch
let state = await checkCredentialState(userID: savedUserID)
switch state {
case .authorized:
    // User is still signed in
case .revoked:
    // User revoked - sign them out
case .notFound:
    // User doesn't exist - sign them out
case .transferred:
    // Account transferred - handle migration
}

Handling Name and Email

func handleAuthorization(_ authorization: ASAuthorization) {
    guard let credential = authorization.credential as? ASAuthorizationAppleIDCredential else {
        return
    }

    let userID = credential.user

    // Name is only provided on FIRST sign-in
    // Store it immediately!
    if let fullName = credential.fullName {
        let name = PersonNameComponentsFormatter().string(from: fullName)
        KeychainManager.shared.save(name, for: .userName)
    }

    // Email may be real or relay address
    if let email = credential.email {
        KeychainManager.shared.save(email, for: .userEmail)
    }

    // Always save user ID
    KeychainManager.shared.save(userID, for: .userID)
}

Biometric Authentication

Types

  • Face ID - iPhone X and later, iPad Pro
  • Touch ID - Older iPhones, MacBooks, Magic Keyboard

Policy Types

// .deviceOwnerAuthenticationWithBiometrics
// - Only biometrics (Face ID or Touch ID)
// - Fails if not enrolled

// .deviceOwnerAuthentication
// - Biometrics with passcode fallback
// - More reliable, recommended for most apps

Error Handling

func authenticate() async -> Bool {
    let context = LAContext()
    var error: NSError?

    guard context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) else {
        switch error?.code {
        case LAError.biometryNotAvailable.rawValue:
            // Device doesn't support biometrics
        case LAError.biometryNotEnrolled.rawValue:
            // User hasn't set up Face ID/Touch ID
        case LAError.biometryLockout.rawValue:
            // Too many failed attempts
        default:
            break
        }
        return false
    }

    do {
        return try await context.evaluatePolicy(
            .deviceOwnerAuthenticationWithBiometrics,
            localizedReason: "Sign in to your account"
        )
    } catch let error as LAError {
        switch error.code {
        case .userCancel:
            // User cancelled
        case .userFallback:
            // User chose to enter password
        case .authenticationFailed:
            // Biometric didn't match
        default:
            break
        }
        return false
    }
}

Biometry Type Detection

var biometryType: LABiometryType {
    let context = LAContext()
    _ = context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: nil)
    return context.biometryType
}

// Use for UI
switch biometryType {
case .faceID:
    Image(systemName: "faceid")
    Text("Sign in with Face ID")
case .touchID:
    Image(systemName: "touchid")
    Text("Sign in with Touch ID")
case .opticID:
    Image(systemName: "opticid")
    Text("Sign in with Optic ID")  // visionOS
case .none:
    // Biometrics not available
}

Keychain Storage

Why Keychain?

  • Encrypted - Data encrypted at rest
  • Persists - Survives app reinstall
  • Shared - Can share between apps (same team)
  • Secure Enclave - Hardware-backed security

Basic Operations

actor KeychainManager {
    func save(_ value: String, for key: KeychainKey) throws {
        let data = Data(value.utf8)

        let query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrAccount as String: key.rawValue,
            kSecValueData as String: data
        ]

        // Delete existing
        SecItemDelete(query as CFDictionary)

        // Add new
        let status = SecItemAdd(query as CFDictionary, nil)
        guard status == errSecSuccess else {
            throw KeychainError.saveFailed(status)
        }
    }

    func get(_ key: KeychainKey) throws -> String? {
        let query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrAccount as String: key.rawValue,
            kSecReturnData as String: true
        ]

        var result: AnyObject?
        let status = SecItemCopyMatching(query as CFDictionary, &result)

        guard status == errSecSuccess,
              let data = result as? Data,
              let string = String(data: data, encoding: .utf8) else {
            return nil
        }

        return string
    }

    func delete(_ key: KeychainKey) {
        let query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrAccount as String: key.rawValue
        ]
        SecItemDelete(query as CFDictionary)
    }
}

Access Control

// Require biometric authentication to read
let access = SecAccessControlCreateWithFlags(
    nil,
    kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
    .biometryCurrentSet,  // Requires current biometry
    nil
)

let query: [String: Any] = [
    kSecClass as String: kSecClassGenericPassword,
    kSecAttrAccount as String: key,
    kSecValueData as String: data,
    kSecAttrAccessControl as String: access as Any
]

Session Management

Token Storage

struct AuthToken: Codable {
    let accessToken: String
    let refreshToken: String?
    let expiresAt: Date?

    var isExpired: Bool {
        guard let expiresAt = expiresAt else { return false }
        return Date() > expiresAt
    }
}

Auto-Refresh Pattern

actor SessionManager {
    private var token: AuthToken?
    private var refreshTask: Task<AuthToken, Error>?

    func validToken() async throws -> AuthToken {
        // Return valid token
        if let token = token, !token.isExpired {
            return token
        }

        // Join existing refresh if in progress
        if let refreshTask = refreshTask {
            return try await refreshTask.value
        }

        // Start refresh
        let task = Task {
            let newToken = try await refreshToken()
            self.token = newToken
            return newToken
        }

        refreshTask = task
        defer { refreshTask = nil }

        return try await task.value
    }
}

Security Best Practices

Do

  • Store sensitive data in Keychain, not UserDefaults
  • Validate credential state on every app launch
  • Use .deviceOwnerAuthentication for reliability
  • Handle revoked credentials gracefully
  • Clear tokens on sign out

Don't

  • Store passwords in plain text
  • Trust client-side validation alone
  • Ignore authentication errors
  • Keep tokens after sign out
  • Skip credential state checks

SwiftUI Integration

Environment-Based Auth

@MainActor
@Observable
class AuthenticationManager {
    var isAuthenticated = false
    var currentUser: User?

    func signOut() {
        KeychainManager.shared.clearAll()
        isAuthenticated = false
        currentUser = nil
    }
}

// In App
@main
struct MyApp: App {
    @State private var authManager = AuthenticationManager()

    var body: some Scene {
        WindowGroup {
            Group {
                if authManager.isAuthenticated {
                    ContentView()
                } else {
                    AuthenticationView()
                }
            }
            .environment(authManager)
        }
    }
}

Auth State Handling

struct ContentView: View {
    @Environment(AuthenticationManager.self) var authManager

    var body: some View {
        NavigationStack {
            // Content
        }
        .task {
            // Check credential state on appear
            await authManager.checkCredentialState()
        }
    }
}

Source: SKILL.md on GitHub

No alerts2mo3 checks · Risk SAFE
  • Gen Agent Trust Hub2mo

    This skill provides a comprehensive and secure framework for generating iOS authentication flows using Sign in with Apple and biometric security. It correctly implements Apple's security best practices, including using the Keychain for encrypted storage and native frameworks for identity management.

  • Socket2mo

    No alerts

  • Snyk2mo

    Risk: LOW · No issues

Signed by skilld at 32566aa. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub 2 months ago.

Steadyupdated 3 months ago
What it can do
Reads files Edits files Runs commands
last_verified
2026-07-16
review_by
2027-06-22
All 7 allowed tools
ReadWriteEditGlobGrepBashAskUserQuestion

README badge

README badge for rshankras/claude-code-apple-skills/auth-flow