iOS authentication expert for user sign-in. Use when working with Sign in with Apple, biometric authentication (Face ID, Touch ID), Keychain credentials, or password autofill.
Expert guidance for implementing authentication in iOS apps.
import AuthenticationServices
struct SignInView: View {
@Environment(\.colorScheme) var colorScheme
var body: some View {
SignInWithAppleButton(.signIn) { request in
request.requestedScopes = [.fullName, .email]
} onCompletion: { result in
handleSignIn(result)
}
.signInWithAppleButtonStyle(colorScheme == .dark ? .white : .black)
.frame(height: 50)
.padding()
}
private func handleSignIn(_ result: Result<ASAuthorization, Error>) {
switch result {
case .success(let authorization):
if let appleIDCredential = authorization.credential as? ASAuthorizationAppleIDCredential {
let userID = appleIDCredential.user
let email = appleIDCredential.email
let fullName = appleIDCredential.fullName
// First sign-in: email and name available
// Subsequent sign-ins: only userID
// Store userID securely
saveUserID(userID)
// Send identity token to your server
if let identityToken = appleIDCredential.identityToken,
let tokenString = String(data: identityToken, encoding: .utf8) {
authenticateWithServer(token: tokenString)
}
}
case .failure(let error):
print("Sign in failed: \(error)")
}
}
}
func checkAppleIDCredentialState(userID: String) async {
let provider = ASAuthorizationAppleIDProvider()
do {
let state = try await provider.credentialState(forUserID: userID)
switch state {
case .authorized:
// User is still authorized
break
case .revoked:
// User revoked authorization - sign out
signOut()
case .notFound:
// Credential not found - show sign in
break
case .transferred:
// User transferred to different team
break
@unknown default:
break
}
} catch {
print("Failed to check credential state: \(error)")
}
}
class AuthManager: ObservableObject {
init() {
NotificationCenter.default.addObserver(
self,
selector: #selector(handleCredentialRevoked),
name: ASAuthorizationAppleIDProvider.credentialRevokedNotification,
object: nil
)
}
@objc private func handleCredentialRevoked() {
signOut()
}
}
import LocalAuthentication
class BiometricAuthManager: ObservableObject {
@Published var isAuthenticated = false
@Published var biometricType: BiometricType = .none
enum BiometricType {
case none, touchID, faceID, opticID
}
init() {
biometricType = getBiometricType()
}
func getBiometricType() -> BiometricType {
let context = LAContext()
var error: NSError?
guard context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) else {
return .none
}
switch context.biometryType {
case .touchID:
return .touchID
case .faceID:
return .faceID
case .opticID:
return .opticID
case .none:
return .none
@unknown default:
return .none
}
}
func authenticate() async -> Bool {
let context = LAContext()
var error: NSError?
guard context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) else {
return false
}
do {
let success = try await context.evaluatePolicy(
.deviceOwnerAuthenticationWithBiometrics,
localizedReason: "Authenticate to access your account"
)
await MainActor.run {
isAuthenticated = success
}
return success
} catch {
print("Authentication failed: \(error)")
return false
}
}
// Fallback to passcode
func authenticateWithPasscode() async -> Bool {
let context = LAContext()
do {
let success = try await context.evaluatePolicy(
.deviceOwnerAuthentication,
localizedReason: "Authenticate to access your account"
)
return success
} catch {
return false
}
}
}
<key>NSFaceIDUsageDescription</key>
<string>We use Face ID to securely authenticate you.</string>
struct SecureView: View {
@StateObject private var authManager = BiometricAuthManager()
@State private var showContent = false
var body: some View {
Group {
if showContent {
ContentView()
} else {
VStack(spacing: 20) {
Image(systemName: authManager.biometricType == .faceID ? "faceid" : "touchid")
.font(.system(size: 64))
Button("Unlock") {
Task {
showContent = await authManager.authenticate()
}
}
.buttonStyle(.borderedProminent)
}
}
}
.task {
showContent = await authManager.authenticate()
}
}
}
// In your text fields
TextField("Email", text: $email)
.textContentType(.username)
.keyboardType(.emailAddress)
SecureField("Password", text: $password)
.textContentType(.password)
import Security
class CredentialManager {
static let shared = CredentialManager()
func saveCredentials(username: String, password: String, server: String) throws {
let passwordData = password.data(using: .utf8)!
let query: [String: Any] = [
kSecClass as String: kSecClassInternetPassword,
kSecAttrAccount as String: username,
kSecAttrServer as String: server,
kSecValueData as String: passwordData
]
// Delete existing
SecItemDelete(query as CFDictionary)
// Add new
let status = SecItemAdd(query as CFDictionary, nil)
guard status == errSecSuccess else {
throw KeychainError.saveFailed(status)
}
}
func getCredentials(server: String) throws -> (username: String, password: String)? {
let query: [String: Any] = [
kSecClass as String: kSecClassInternetPassword,
kSecAttrServer as String: server,
kSecReturnAttributes as String: true,
kSecReturnData as String: true,
kSecMatchLimit as String: kSecMatchLimitOne
]
var result: CFTypeRef?
let status = SecItemCopyMatching(query as CFDictionary, &result)
guard status == errSecSuccess,
let item = result as? [String: Any],
let username = item[kSecAttrAccount as String] as? String,
let passwordData = item[kSecValueData as String] as? Data,
let password = String(data: passwordData, encoding: .utf8) else {
return nil
}
return (username, password)
}
func deleteCredentials(server: String) throws {
let query: [String: Any] = [
kSecClass as String: kSecClassInternetPassword,
kSecAttrServer as String: server
]
let status = SecItemDelete(query as CFDictionary)
guard status == errSecSuccess || status == errSecItemNotFound else {
throw KeychainError.deleteFailed(status)
}
}
}
enum KeychainError: Error {
case saveFailed(OSStatus)
case deleteFailed(OSStatus)
}
import AuthenticationServices
class PasskeyManager: NSObject, ASAuthorizationControllerDelegate, ASAuthorizationControllerPresentationContextProviding {
func registerPasskey(username: String, challenge: Data, userID: Data) {
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()
}
func signInWithPasskey(challenge: Data) {
let provider = ASAuthorizationPlatformPublicKeyCredentialProvider(relyingPartyIdentifier: "example.com")
let request = provider.createCredentialAssertionRequest(challenge: challenge)
let controller = ASAuthorizationController(authorizationRequests: [request])
controller.delegate = self
controller.presentationContextProvider = self
controller.performRequests()
}
// MARK: - Delegate
func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) {
if let credential = authorization.credential as? ASAuthorizationPlatformPublicKeyCredentialRegistration {
// Registration successful
let credentialID = credential.credentialID
let attestationObject = credential.rawAttestationObject
// Send to server
} else if let credential = authorization.credential as? ASAuthorizationPlatformPublicKeyCredentialAssertion {
// Sign in successful
let signature = credential.signature
let authenticatorData = credential.rawAuthenticatorData
// Verify on server
}
}
func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) {
print("Authorization failed: \(error)")
}
func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor {
// Return your app's window
UIApplication.shared.connectedScenes
.compactMap { $0 as? UIWindowScene }
.flatMap { $0.windows }
.first { $0.isKeyWindow }!
}
}
import AuthenticationServices
class OAuthManager: NSObject, ASWebAuthenticationPresentationContextProviding {
func signIn(provider: OAuthProvider) async throws -> String {
let authURL = provider.authorizationURL
let callbackScheme = "myapp"
return try await withCheckedThrowingContinuation { continuation in
let session = ASWebAuthenticationSession(
url: authURL,
callbackURLScheme: callbackScheme
) { callbackURL, error in
if let error = error {
continuation.resume(throwing: error)
return
}
guard let url = callbackURL,
let components = URLComponents(url: url, resolvingAgainstBaseURL: false),
let code = components.queryItems?.first(where: { $0.name == "code" })?.value else {
continuation.resume(throwing: OAuthError.noAuthCode)
return
}
continuation.resume(returning: code)
}
session.presentationContextProvider = self
session.prefersEphemeralWebBrowserSession = true
session.start()
}
}
func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor {
UIApplication.shared.connectedScenes
.compactMap { $0 as? UIWindowScene }
.flatMap { $0.windows }
.first { $0.isKeyWindow }!
}
}
enum OAuthError: Error {
case noAuthCode
}