Section 15/161 menit

15. Real Use Cases

15. Real Use Cases

Use Case 1: Banking App — Secure Login dengan Attestation

App perbankan yang menggunakan passkey dengan attestation untuk memverifikasi device yang digunakan, memenuhi requirements regulasi.

swift
import AuthenticationServices
import CryptoKit

struct VerifiedDevice {
    let credentialId: Data
    let publicKey: P256.Signing.PublicKey
    let aaguid: UUID
    let attestationCertificate: SecCertificate?
}

class BankingAuthCoordinator: NSObject {
    private var authController: ASAuthorizationController?
    private var continuation: CheckedContinuation<ASAuthorization, Error>?

    func registerSecureDevice(
        accountId: String,
        serverChallenge: Data
    ) async throws -> VerifiedDevice {
        let provider = ASAuthorizationPlatformPublicKeyCredentialProvider(
            relyingPartyIdentifier: "bankingapp.example.com"
        )
        let request = provider.createCredentialRegistrationRequest(
            challenge: serverChallenge,
            name: "Banking App on \(UIDevice.current.name)",
            userID: Data(accountId.utf8)
        )

        // Banking: wajib verifikasi biometrik
        request.userVerificationPreference = .required

        // Minta attestation untuk verifikasi device di server
        request.attestationPreference = .direct

        let authorization = try await withCheckedThrowingContinuation { continuation in
            self.continuation = continuation
            let controller = ASAuthorizationController(authorizationRequests: [request])
            controller.delegate = self
            controller.presentationContextProvider = self
            controller.performRequests()
            self.authController = controller
        }

        guard let registration = authorization.credential
                as? ASAuthorizationPlatformPublicKeyCredentialRegistration else {
            throw BankingAuthError.invalidCredential
        }

        // Kirim attestation ke server banking untuk verifikasi
        let verifiedDevice = try await bankingServer.verifyAndRegisterDevice(
            accountId: accountId,
            credentialId: registration.credentialID,
            attestationObject: registration.rawAttestationObject ?? Data(),
            clientDataJSON: registration.rawClientDataJSON,
            challenge: serverChallenge
        )

        return verifiedDevice
    }

    func authorizeTransaction(
        amount: Decimal,
        recipient: String,
        challenge: Data
    ) async throws -> TransactionAuthorization {
        // Tampilkan konfirmasi dulu sebelum minta biometrik
        guard await confirmTransaction(amount: amount, recipient: recipient) else {
            throw BankingAuthError.userDeclined
        }

        let provider = ASAuthorizationPlatformPublicKeyCredentialProvider(
            relyingPartyIdentifier: "bankingapp.example.com"
        )
        let request = provider.createCredentialAssertionRequest(challenge: challenge)
        request.userVerificationPreference = .required  // Wajib biometrik untuk transfer

        let authorization = try await withCheckedThrowingContinuation { continuation in
            self.continuation = continuation
            let controller = ASAuthorizationController(authorizationRequests: [request])
            controller.delegate = self
            controller.presentationContextProvider = self
            controller.performRequests()
            self.authController = controller
        }

        guard let assertion = authorization.credential
                as? ASAuthorizationPlatformPublicKeyCredentialAssertion else {
            throw BankingAuthError.invalidCredential
        }

        return try await bankingServer.verifyTransactionSignature(
            credentialId: assertion.credentialID,
            authenticatorData: assertion.rawAuthenticatorData,
            signature: assertion.signature,
            clientDataJSON: assertion.rawClientDataJSON,
            expectedChallenge: challenge,
            transactionAmount: amount,
            transactionRecipient: recipient
        )
    }

    private func confirmTransaction(amount: Decimal, recipient: String) async -> Bool {
        await withCheckedContinuation { continuation in
            Task { @MainActor in
                let alert = UIAlertController(
                    title: "Konfirmasi Transfer",
                    message: "Transfer \(amount.formatted(.currency(code: "IDR"))) ke \(recipient)?",
                    preferredStyle: .alert
                )
                alert.addAction(UIAlertAction(title: "Batal", style: .cancel) { _ in
                    continuation.resume(returning: false)
                })
                alert.addAction(UIAlertAction(title: "Lanjutkan", style: .default) { _ in
                    continuation.resume(returning: true)
                })
                topViewController()?.present(alert, animated: true)
            }
        }
    }
}

extension BankingAuthCoordinator: ASAuthorizationControllerDelegate {
    func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) {
        continuation?.resume(returning: authorization)
        continuation = nil; authController = nil
    }
    func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) {
        continuation?.resume(throwing: error)
        continuation = nil; authController = nil
    }
}

extension BankingAuthCoordinator: ASAuthorizationControllerPresentationContextProviding {
    func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor {
        UIApplication.shared.connectedScenes
            .compactMap { $0 as? UIWindowScene }
            .flatMap { $0.windows }
            .first(where: { $0.isKeyWindow }) ?? UIWindow()
    }
}

Use Case 2: E-Commerce App — Seamless Checkout dengan Passkey

Checkout flow yang menggunakan passkey untuk konfirmasi identitas sebelum pembelian, dengan fallback ke password yang graceful.

swift
import AuthenticationServices
import SwiftUI

@Observable
class CheckoutViewModel {
    var isAuthenticating = false
    var authError: String?
    var isOrderConfirmed = false

    private let coordinator = AuthenticationCoordinator()

    // Conditional AutoFill untuk layar checkout
    func setupPasskeyAutoFill() async {
        guard let challenge = try? await server.fetchCheckoutChallenge() else { return }

        let provider = ASAuthorizationPlatformPublicKeyCredentialProvider(
            relyingPartyIdentifier: "shop.example.com"
        )
        let request = provider.createCredentialAssertionRequest(challenge: challenge)

        let controller = ASAuthorizationController(authorizationRequests: [request])
        controller.delegate = coordinator
        controller.presentationContextProvider = coordinator
        controller.performAutoFillAssistedRequests()
    }

    func confirmPurchase(cart: Cart) async {
        isAuthenticating = true
        authError = nil

        do {
            let challenge = try await server.fetchCheckoutChallenge()
            let assertion = try await coordinator.signInWithPasskey(
                challenge: challenge,
                rpID: "shop.example.com"
            )

            // Verifikasi dan proses order
            let order = try await server.placeOrder(
                cart: cart,
                assertionCredentialId: assertion.credentialID,
                signature: assertion.signature,
                clientDataJSON: assertion.rawClientDataJSON,
                authenticatorData: assertion.rawAuthenticatorData
            )

            isOrderConfirmed = true
            Analytics.log("checkout_passkey_success", parameters: ["cart_value": cart.total])
        } catch let error as ASAuthorizationError {
            switch error.code {
            case .canceled:
                break  // User cancel — tidak tampilkan error
            case .notHandled:
                // Tidak ada passkey — fallback ke OTP atau password
                await fallbackToOTPConfirmation(cart: cart)
            default:
                authError = "Autentikasi gagal. Coba lagi."
                Analytics.log("checkout_passkey_failed", parameters: ["error": error.code.rawValue])
            }
        } catch {
            authError = error.localizedDescription
        }

        isAuthenticating = false
    }

    private func fallbackToOTPConfirmation(cart: Cart) async {
        // Kirim OTP via SMS dan tampilkan OTP input
    }
}

struct CheckoutConfirmationView: View {
    @State private var viewModel = CheckoutViewModel()
    let cart: Cart

    var body: some View {
        VStack(spacing: 24) {
            CartSummaryView(cart: cart)

            if let error = viewModel.authError {
                Text(error)
                    .foregroundStyle(.red)
                    .font(.caption)
            }

            Button {
                Task { await viewModel.confirmPurchase(cart: cart) }
            } label: {
                if viewModel.isAuthenticating {
                    ProgressView()
                } else {
                    Label("Bayar dengan Face ID", systemImage: "faceid")
                }
            }
            .buttonStyle(.borderedProminent)
            .disabled(viewModel.isAuthenticating)
        }
        .padding()
        .task { await viewModel.setupPasskeyAutoFill() }
        .navigationDestination(isPresented: $viewModel.isOrderConfirmed) {
            OrderSuccessView()
        }
    }
}