Section 9/171 menit
9. Fallback ke Password
9. Fallback ke Password
Untuk backward compatibility, app harus mendukung login dengan password untuk:
- User yang belum mendaftar passkey
- Perangkat tanpa Touch ID / Face ID
- Skenario pemulihan akun
Multi-Method Authentication
swift
// MARK: - Unified Auth Controller: Passkey + Password dalam satu request
@MainActor
final class UnifiedAuthManager: NSObject {
private var continuation: CheckedContinuation<AuthResult, Error>?
enum AuthResult {
case passkey(PasskeyAssertionResponse)
case password(username: String, password: String)
}
func authenticate(relyingPartyID: String, challenge: Data) async throws -> AuthResult {
return try await withCheckedThrowingContinuation { continuation in
self.continuation = continuation
// Request 1: Passkey
let passkeyProvider = ASAuthorizationPlatformPublicKeyCredentialProvider(
relyingPartyIdentifier: relyingPartyID
)
let passkeyRequest = passkeyProvider.createCredentialAssertionRequest(challenge: challenge)
// Request 2: Password (AutoFill password manager)
let passwordRequest = ASAuthorizationPasswordProvider().createRequest()
// System otomatis menampilkan yang tersedia:
// - Jika ada passkey → tampilkan passkey (prioritas)
// - Jika ada saved password → tampilkan password
// - Jika keduanya → tampilkan keduanya, user memilih
let controller = ASAuthorizationController(
authorizationRequests: [passkeyRequest, passwordRequest]
)
controller.delegate = self
controller.presentationContextProvider = self
controller.performRequests()
}
}
}
extension UnifiedAuthManager: ASAuthorizationControllerDelegate {
func authorizationController(
controller: ASAuthorizationController,
didCompleteWithAuthorization authorization: ASAuthorization
) {
switch authorization.credential {
case let passkey as ASAuthorizationPlatformPublicKeyCredentialAssertion:
let response = PasskeyAssertionResponse(
credentialID: passkey.credentialID,
rawClientDataJSON: passkey.rawClientDataJSON,
rawAuthenticatorData: passkey.rawAuthenticatorData,
signature: passkey.signature,
userID: passkey.userID
)
continuation?.resume(returning: .passkey(response))
case let password as ASPasswordCredential:
continuation?.resume(returning: .password(
username: password.user,
password: password.password
))
default:
continuation?.resume(throwing: PasskeyError.unexpectedCredentialType)
}
continuation = nil
}
func authorizationController(
controller: ASAuthorizationController,
didCompleteWithError error: Error
) {
if let authError = error as? ASAuthorizationError, authError.code == .canceled {
continuation?.resume(throwing: PasskeyError.userCancelled)
} else {
continuation?.resume(throwing: error)
}
continuation = nil
}
}
extension UnifiedAuthManager: ASAuthorizationControllerPresentationContextProviding {
func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor {
UIApplication.shared.connectedScenes
.compactMap { $0 as? UIWindowScene }
.flatMap { $0.windows }
.first { $0.isKeyWindow } ?? UIWindow()
}
}