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