๐ 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/์คํจ๋ง ํ์ธ ๊ฐ๋ฅ
โ
์ค์ ์์ฒด ๋ฐ์ดํฐ๋ ์ฑ์์ ์ ๊ทผ ๋ถ๊ฐ