๐ŸŒ KO

๐Ÿ” Mastering Local Authentication

Secure your app with Face ID and Touch ID! Complete guide from biometric auth to password replacement.

โญ Difficulty: โญ โฑ๏ธ Est. Time: 30min ๐Ÿ“‚ System & Network

โœจ Local Authentication is?

Local Authentication is a framework that authenticates users using Face ID, Touch ID, or device passcode. It provides safe and convenient authentication for login, payment authorization, and accessing sensitive information. All biometric data is processed on-device only and never sent to a server.

๐Ÿ“ฆ Key Features

Feature Overview
โœ… Face ID / Touch ID authentication
โœ… Device passcode authentication
โœ… Check biometric availability
โœ… Handle authentication failure and retry
โœ… Custom authentication UI
โœ… Password fallback authentication

๐Ÿ” Checking Biometric Availability

BiometryCheck.swift
import LocalAuthentication

func checkBiometryAvailability() -> (Bool, String) {
    let context = LAContext()
    var error: NSError?

    // Check if biometric authentication is available
    let canEvaluate = context.canEvaluatePolicy(
        .deviceOwnerAuthenticationWithBiometrics,
        error: &error
    )

    if canEvaluate {
        // Check biometric type
        switch context.biometryType {
        case .faceID:
            return (true, "Face ID available")
        case .touchID:
            return (true, "Touch ID available")
        case .opticID:
            return (true, "Optic ID available")
        case .none:
            return (false, "Biometrics not supported")
        @unknown default:
            return (false, "Unknown type")
        }
    } else {
        // Error handling
        if let error = error {
            switch LAError.Code(rawValue: error.code) {
            case .biometryNotEnrolled:
                return (false, "Biometrics not enrolled")
            case .biometryNotAvailable:
                return (false, "Biometrics not supported")
            case .passcodeNotSet:
                return (false, "Device passcode not set")
            default:
                return (false, error.localizedDescription)
            }
        }
        return (false, "Unknown error")
    }
}

// Usage example
let (available, message) = checkBiometryAvailability()
print(message)  // "Face ID available"

๐Ÿ” Basic Biometric Authentication

BasicAuth.swift
import LocalAuthentication

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

    // Check if biometric authentication is available
    guard context.canEvaluatePolicy(
        .deviceOwnerAuthenticationWithBiometrics,
        error: &error
    ) else {
        print("biometric authentication ๋ถˆ๊ฐ€: \(error?.localizedDescription ?? "")")
        return false
    }

    do {
        // Authentication ์‹œ๋„
        let success = try await context.evaluatePolicy(
            .deviceOwnerAuthenticationWithBiometrics,
            localizedReason: "์•ฑ์— ๋กœ๊ทธ์ธํ•˜๋ ค๋ฉด ์ธ์ฆ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค"
        )
        return success
    } catch let error {
        // Authentication failed ์ฒ˜๋ฆฌ
        if let laError = error as? LAError {
            switch laError.code {
            case .authenticationFailed:
                print("Authentication failed (์–ผ๊ตด/์ง€๋ฌธ ๋ถˆ์ผ์น˜)")
            case .userCancel:
                print("์‚ฌ์šฉ์ž๊ฐ€ ์ทจ์†Œํ•จ")
            case .userFallback:
                print("enter password ์„ ํƒ")
            case .systemCancel:
                print("์‹œ์Šคํ…œ์ด ์ทจ์†Œ (์•ฑ ์ „ํ™˜ ๋“ฑ)")
            case .biometryLockout:
                print("๋„ˆ๋ฌด ๋งŽ์€ ์‹œ๋„๋กœ ์ž ๊น€")
            default:
                print("์ธ์ฆ Error: \(laError.localizedDescription)")
            }
        }
        return false
    }
}

// Usage example
let authenticated = await authenticateUser()
if authenticated {
    print("๋กœ๊ทธ์ธ ์„ฑ๊ณต!")
}

๐Ÿ”‘ Device Passcode Authentication

PasscodeAuth.swift
import LocalAuthentication

func authenticateWithPasscode() async -> Bool {
    let context = LAContext()

    // ์ƒ์ฒด Authentication failed ์‹œ ๊ธฐ๊ธฐ enter passcode ํ—ˆ์šฉ
    context.localizedFallbackTitle = "enter passcode"

    do {
        let success = try await context.evaluatePolicy(
            .deviceOwnerAuthentication,  // biometric authentication + ๊ธฐ๊ธฐ ์•”ํ˜ธ
            localizedReason: "๊ณ„์ •์— ์ ‘๊ทผํ•˜๋ ค๋ฉด ์ธ์ฆํ•˜์„ธ์š”"
        )
        return success
    } catch {
        print("Authentication failed: \(error.localizedDescription)")
        return false
    }
}

// Custom ์ทจ์†Œ ๋ฒ„ํŠผ ํ…์ŠคํŠธ
func authenticateWithCustomButton() async -> Bool {
    let context = LAContext()
    context.localizedCancelTitle = "๋‚˜์ค‘์—"
    context.localizedFallbackTitle = "Password๋กœ ๋กœ๊ทธ์ธ"

    do {
        return try await context.evaluatePolicy(
            .deviceOwnerAuthenticationWithBiometrics,
            localizedReason: "๋ฏผ๊ฐํ•œ ์ •๋ณด์— ์ ‘๊ทผํ•ฉ๋‹ˆ๋‹ค"
        )
    } catch {
        return false
    }
}

๐Ÿ“ฑ SwiftUI Integration

AuthView.swift
import SwiftUI
import LocalAuthentication

struct AuthenticationView: View {
    @State private var isAuthenticated = false
    @State private var showError = false
    @State private var errorMessage = ""

    var body: some View {
        VStack(spacing: 20) {
            if isAuthenticated {
                // Authentication ํ›„ ํ™”๋ฉด
                VStack(spacing: 16) {
                    Image(systemName: "checkmark.circle.fill")
                        .font(.system(size: 80))
                        .foregroundStyle(.green)

                    Text("Authentication successful")
                        .font(.title)
                        .bold()

                    Text("์•ˆ์ „ํ•˜๊ฒŒ ๋กœ๊ทธ์ธ๋˜์—ˆ์Šต๋‹ˆ๋‹ค")
                        .foregroundStyle(.secondary)

                    Button("๋กœ๊ทธ์•„์›ƒ") {
                        isAuthenticated = false
                    }
                    .buttonStyle(.borderedProminent)
                }
            } else {
                // ๋กœ๊ทธ์ธ ํ™”๋ฉด
                VStack(spacing: 16) {
                    Image(systemName: "faceid")
                        .font(.system(size: 80))
                        .foregroundStyle(.blue)

                    Text("์ƒ์ฒด Authentication required")
                        .font(.title)
                        .bold()

                    Text("Face ID๋กœ ์•ˆ์ „ํ•˜๊ฒŒ ๋กœ๊ทธ์ธํ•˜์„ธ์š”")
                        .foregroundStyle(.secondary)

                    Button("์ธ์ฆํ•˜๊ธฐ") {
                        Task {
                            await authenticate()
                        }
                    }
                    .buttonStyle(.borderedProminent)
                }
            }
        }
        .padding()
        .alert("Authentication failed", isPresented: $showError) {
            Button("OK", role: .cancel) { }
        } message: {
            Text(errorMessage)
        }
        .onAppear {
            // App ์‹œ์ž‘ ์‹œ ์ž๋™ ์ธ์ฆ ์‹œ๋„
            Task {
                await authenticate()
            }
        }
    }

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

        guard context.canEvaluatePolicy(
            .deviceOwnerAuthentication,
            error: &error
        ) else {
            errorMessage = error?.localizedDescription ?? "์ธ์ฆ ๋ถˆ๊ฐ€"
            showError = true
            return
        }

        do {
            let success = try await context.evaluatePolicy(
                .deviceOwnerAuthentication,
                localizedReason: "์•ฑ ์ ‘๊ทผ์„ for ์ธ์ฆ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค"
            )

            await MainActor.run {
                isAuthenticated = success
            }
        } catch let error {
            await MainActor.run {
                errorMessage = error.localizedDescription
                showError = true
            }
        }
    }
}

๐Ÿ”’ Practical Example: Protecting Sensitive Data

SecureDataView.swift
import SwiftUI
import LocalAuthentication

struct SecureDataView: View {
    @State private var isUnlocked = false
    @State private var sensitiveData = "****-****-****-1234"

    var body: some View {
        List {
            Section("๊ณ„์ขŒ ์ •๋ณด") {
                HStack {
                    Text("๊ณ„์ขŒ๋ฒˆํ˜ธ")
                    Spacer()
                    Text(isUnlocked ? "1234-5678-9012-3456" : "****-****-****-3456")
                        .font(.system(.body, design: .monospaced))
                }

                HStack {
                    Text("์ž”์•ก")
                    Spacer()
                    Text(isUnlocked ? "1,234,567์›" : "*******์›")
                }
            }

            Section {
                Button(isUnlocked ? "์ •๋ณด ์ˆจ๊ธฐ๊ธฐ" : "์ •๋ณด ๋ณด๊ธฐ") {
                    if isUnlocked {
                        isUnlocked = false
                    } else {
                        Task {
                            await authenticate()
                        }
                    }
                }
            }
        }
        .navigationTitle("๋‚ด ๊ณ„์ขŒ")
    }

    func authenticate() async {
        let context = LAContext()

        do {
            let success = try await context.evaluatePolicy(
                .deviceOwnerAuthentication,
                localizedReason: "๋ฏผ๊ฐํ•œ ๊ณ„์ขŒ ์ •๋ณด๋ฅผ ๋ณด๋ ค๋ฉด ์ธ์ฆํ•˜์„ธ์š”"
            )

            if success {
                await MainActor.run {
                    withAnimation {
                        isUnlocked = true
                    }
                }
            }
        } catch {
            print("Authentication failed")
        }
    }
}

โฑ๏ธ Reusing Authentication Context

ContextReuse.swift
import LocalAuthentication

class AuthenticationManager {
    private let context = LAContext()

    init() {
        // Authentication ์œ ํšจ ์‹œ๊ฐ„ ์„ค์ • (๊ธฐ๋ณธ 10์ดˆ)
        context.touchIDAuthenticationAllowableReuseDuration = 30  // 30์ดˆ
    }

    func authenticate(reason: String) async -> Bool {
        do {
            // 30์ดˆ ๋‚ด ์žฌ์ธ์ฆ ์‹œ biometric authentication ์ƒ๋žต
            return try await context.evaluatePolicy(
                .deviceOwnerAuthenticationWithBiometrics,
                localizedReason: reason
            )
        } catch {
            return false
        }
    }

    func invalidate() {
        // ์ปจํ…์ŠคํŠธ ๋ฌดํšจํ™” (์ฆ‰์‹œ ์žฌAuthentication required)
        context.invalidate()
    }
}

// Usage example
let authManager = AuthenticationManager()

// ์ฒซ ์ธ์ฆ (์ƒ์ฒด Authentication required)
let success1 = await authManager.authenticate(reason: "๊ฒฐ์ œ ์Šน์ธ")

// 30์ดˆ ๋‚ด ์žฌ์ธ์ฆ (biometric authentication ์ƒ๋žต)
let success2 = await authManager.authenticate(reason: "์ถ”๊ฐ€ ๊ฒฐ์ œ")

// ๋กœ๊ทธ์•„์›ƒ ์‹œ ์ปจํ…์ŠคํŠธ ๋ฌดํšจํ™”
authManager.invalidate()

๐ŸŽฏ Advanced Features: Domain State

DomainState.swift
import LocalAuthentication

class BiometricStateMonitor {
    private var domainState: Data?

    func saveBiometricState() {
        let context = LAContext()
        var error: NSError?

        guard context.canEvaluatePolicy(
            .deviceOwnerAuthenticationWithBiometrics,
            error: &error
        ) else {
            return
        }

        // ํ˜„์žฌ biometric authentication save state
        domainState = context.evaluatedPolicyDomainState
        UserDefaults.standard.set(domainState, forKey: "BiometricState")
    }

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

        guard context.canEvaluatePolicy(
            .deviceOwnerAuthenticationWithBiometrics,
            error: &error
        ) else {
            return true
        }

        // Save๋œ ์ƒํƒœ์™€ ๋น„๊ต
        guard let savedState = UserDefaults.standard.data(forKey: "BiometricState") else {
            return true
        }

        let currentState = context.evaluatedPolicyDomainState
        return savedState != currentState
    }

    func handleBiometricChange() {
        if hasBiometricChanged() {
            print("โš ๏ธ biometric authentication ์ •๋ณด๊ฐ€ ๋ณ€๊ฒฝ๋˜์—ˆ์Šต๋‹ˆ๋‹ค!")
            print("๋ณด์•ˆ์„ for ๋‹ค์‹œ ๋กœ๊ทธ์ธdo it.")

            // App ๋ฐ์ดํ„ฐ ์ดˆ๊ธฐํ™” or ์žฌ๋กœ๊ทธ์ธ ์š”๊ตฌ
            logoutUser()
            saveBiometricState()
        }
    }

    func logoutUser() {
        // ๋กœ๊ทธ์•„์›ƒ ๋กœ์ง
        UserDefaults.standard.set(false, forKey: "IsLoggedIn")
    }
}

// Usage example
let monitor = BiometricStateMonitor()
monitor.saveBiometricState()  // ๋กœ๊ทธ์ธ ์‹œ

// App ์‹œ์ž‘ ์‹œ ํ™•์ธ
monitor.handleBiometricChange()

๐Ÿ’ก HIG Guidelines

HIG Recommendations
โœ… DO
1. ๋ช…ํ™•ํ•œ ์ด์œ  ์ œ๊ณต
   - "๊ฒฐ์ œ๋ฅผ ์Šน์ธํ•˜๋ ค๋ฉด Face ID๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค"
   - ์‚ฌ์šฉ์ž์—๊ฒŒ ์™œ ์ธ์ฆ์ด ํ•„์š”ํ•œ์ง€ ๋ช…ํ™•ํžˆ ์„ค๋ช…

2. ๋Œ€์ฒด ์ˆ˜๋‹จ ์ œ๊ณต
   - ์ƒ์ฒด Authentication failed ์‹œ ๊ธฐ๊ธฐ ์•”ํ˜ธ ํ—ˆ์šฉ
   - .deviceOwnerAuthentication ์ •์ฑ… ์‚ฌ์šฉ

3. ์ ์ ˆํ•œ ํƒ€์ด๋ฐ
   - ๋ฏผ๊ฐํ•œ ์ž‘์—… ์ง์ „์—๋งŒ ์š”์ฒญ
   - ๋„ˆ๋ฌด ์ž์ฃผ ์š”์ฒญํ•˜์ง€ ์•Š๊ธฐ

4. Error handling
   - Authentication failed ์‹œ ๋ช…ํ™•ํ•œ ํ”ผ๋“œ๋ฐฑ
   - Retry ์˜ต์…˜ ์ œ๊ณต

โŒ DON'T
1. ์•ฑ ์‹œ์ž‘๋งˆ๋‹ค ์ธ์ฆ ์š”๊ตฌ (๋ถˆIf needed)
2. ์ƒ์ฒด ๋ฐ์ดํ„ฐ๋ฅผ ์„œ๋ฒ„๋กœ ์ „์†ก
3. ์ธ์ฆ ์—†์ด๋„ ๊ธฐ๋ณธ ๊ธฐ๋Šฅ์€ availableํ•˜๊ฒŒ
4. "Touch ID"๋ผ๊ณ  ๋ช…์‹œ (๊ธฐ๊ธฐ๋ณ„๋กœ ๋‹ค๋ฆ„)

๐Ÿ”ง Practical Tips

Real-World Patterns
import LocalAuthentication

// 1. ์ธ์ฆ ํ—ฌํผ ํด๋ž˜์Šค
class BiometricAuth {
    static let shared = BiometricAuth()
    private init() {}

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

    var biometricName: String {
        switch biometricType {
        case .faceID: return "Face ID"
        case .touchID: return "Touch ID"
        case .opticID: return "Optic ID"
        case .none: return "biometric authentication"
        @unknown default: return "biometric authentication"
        }
    }

    func authenticate(reason: String) async throws -> Bool {
        let context = LAContext()
        return try await context.evaluatePolicy(
            .deviceOwnerAuthentication,
            localizedReason: reason
        )
    }
}

// 2. SwiftUI์—์„œ ์‚ฌ์šฉ
struct SettingsView: View {
    @AppStorage("useBiometric") private var useBiometric = false

    var body: some View {
        Form {
            Section("๋ณด์•ˆ") {
                Toggle("\(BiometricAuth.shared.biometricName) ์‚ฌ์šฉ", isOn: $useBiometric)
            }
        }
    }
}

// 3. ๊ฒฐ์ œ ์Šน์ธ ์˜ˆ์‹œ
func processPayment(amount: Int) async -> Bool {
    do {
        let authenticated = try await BiometricAuth.shared.authenticate(
            reason: "\(amount)์› ๊ฒฐ์ œ๋ฅผ ์Šน์ธํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?"
        )

        if authenticated {
            // ๊ฒฐ์ œ ์ฒ˜๋ฆฌ
            await performPaymentTransaction(amount)
            return true
        }
    } catch {
        print("๊ฒฐ์ œ ์ทจ์†Œ๋จ")
    }
    return false
}

func performPaymentTransaction(_ amount: Int) async {
    // ์‹ค์ œ ๊ฒฐ์ œ API ํ˜ธ์ถœ
}

๐Ÿ” Info.plist Configuration

Info.plist
<!-- Face ID ์‚ฌ์šฉ ์‹œ ํ•„์ˆ˜ -->
<key>NSFaceIDUsageDescription</key>
<string>์•ˆ์ „ํ•œ ๋กœ๊ทธ์ธ์„ for Face ID is used</string>

<!-- or ํ•œ๊ตญ์–ด๋กœ -->
<key>NSFaceIDUsageDescription</key>
<string>๊ณ„์ • ๋ณด์•ˆ์„ for Face ID ์ธ์ฆ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค</string>

๐Ÿ’ก Local Authentication ํ•ต์‹ฌ
โœ… all ์ƒ์ฒด ๋ฐ์ดํ„ฐ๋Š” ๊ธฐ๊ธฐ ๋‚ด์—์„œ๋งŒ ์ฒ˜๋ฆฌ
โœ… Secure Enclave์— ์•ˆ์ „ํ•˜๊ฒŒ ์ €์žฅ
โœ… ์ƒ์ฒด Authentication failed ์‹œ ๊ธฐ๊ธฐ ์•”ํ˜ธ ๋Œ€์ฒด ์ˆ˜๋‹จ ์ œ๊ณต
โœ… ์•ฑ์€ Authentication successful/์‹คํŒจ๋งŒ ํ™•์ธ ๊ฐ€๋Šฅ
โœ… ์‹ค์ œ ์ƒ์ฒด ๋ฐ์ดํ„ฐ๋Š” ์•ฑ์—์„œ ์ ‘๊ทผ ๋ถˆ๊ฐ€

๐Ÿ“ฆ Learning Resources

๐Ÿ’ป
GitHub Project
๐Ÿ“–
Apple Official Docs
๐ŸŽจ
HIG ์ธ์ฆ ๊ฐ€์ด๋“œ

๐Ÿ“Ž Apple Official Resources

๐Ÿ“˜ Documentation ๐ŸŽฌ WWDC Sessions