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()
    }
}