Section 2/161 menit
2. Masalah yang Dipecahkan di Level Lanjutan
2. Masalah yang Dipecahkan di Level Lanjutan
Masalah 1: Associated Domains yang Salah Konfigurasi — Silent Failure
Kesalahan paling umum yang menyebabkan passkey tidak bekerja adalah konfigurasi associated domains yang tidak tepat, dan error-nya tidak selalu jelas.
swift
// ❌ TANPA konfigurasi yang benar: ASAuthorization gagal dengan error generik
// Error: "The operation couldn't be completed (com.apple.AuthenticationServices.AuthorizationError error 1004)"
// Tidak ada petunjuk bahwa masalahnya di Associated Domains
let provider = ASAuthorizationPlatformPublicKeyCredentialProvider(
relyingPartyIdentifier: "app.example.com" // RP ID tidak match domain asli
)
// Akan gagal karena file .well-known/apple-app-site-association di https://app.example.com
// tidak mengandung bundle ID app ini, atau format webcredentials salah
swift
// ✓ DENGAN konfigurasi yang benar: RP ID harus domain yang ada AASA file-nya
// https://example.com/.well-known/apple-app-site-association:
// {
// "webcredentials": {
// "apps": ["ABCDE12345.com.example.myapp"]
// }
// }
let provider = ASAuthorizationPlatformPublicKeyCredentialProvider(
relyingPartyIdentifier: "example.com" // Match domain AASA
)
// Bundle ID di entitlements: com.apple.developer.associated-domains = webcredentials:example.com
Masalah 2: Conditional UI Dipanggil Terlalu Lambat
AutoFill passkey hanya muncul jika performAutoFillAssistedRequests() dipanggil sebelum keyboard muncul dan field aktif.
swift
// ❌ TANPA timing yang tepat: AutoFill passkey tidak muncul di keyboard
class LoginViewController: UIViewController {
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// Terlalu lambat — keyboard mungkin sudah muncul
setupPasskeyAutoFill()
}
}
swift
// ✓ DENGAN timing yang tepat: panggil di viewDidLoad atau viewWillAppear
class LoginViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Sebelum view muncul — AutoFill siap saat keyboard pertama kali muncul
beginConditionalPasskeySignIn()
}
private func beginConditionalPasskeySignIn() {
let challenge = Data(repeating: 0, count: 32) // Dari server
let provider = ASAuthorizationPlatformPublicKeyCredentialProvider(
relyingPartyIdentifier: "example.com"
)
let request = provider.createCredentialAssertionRequest(challenge: challenge)
let authController = ASAuthorizationController(authorizationRequests: [request])
authController.delegate = self
authController.presentationContextProvider = self
authController.performAutoFillAssistedRequests() // Non-blocking
}
}
Masalah 3: Server Tidak Memvalidasi Challenge dengan Benar
Banyak implementasi yang lupa bahwa challenge di clientDataJSON harus di-Base64URL decode dan diverifikasi, bukan hanya di-trust begitu saja.
swift
// ❌ Server tidak verifikasi — rentan replay attack
func handleRegistrationResponse(attestationObject: Data, clientDataJSON: Data) throws {
// JANGAN: langsung simpan credential tanpa verifikasi
database.saveCredential(rawAttestation: attestationObject) // TIDAK AMAN
}
// ✓ Alur verifikasi yang benar (kode Swift untuk ilustrasi, biasanya di server)
func verifyRegistration(
attestationObject: Data,
clientDataJSON: Data,
expectedChallenge: Data,
expectedOrigin: String,
expectedRPID: String
) throws -> VerifiedCredential {
// 1. Decode clientDataJSON
let clientData = try JSONDecoder().decode(ClientData.self, from: clientDataJSON)
// 2. Verifikasi type
guard clientData.type == "webauthn.create" else {
throw PasskeyError.invalidType
}
// 3. Verifikasi challenge (Base64URL decode lalu compare)
guard let receivedChallenge = Data(base64URLEncoded: clientData.challenge),
receivedChallenge == expectedChallenge else {
throw PasskeyError.challengeMismatch // Tolak jika challenge tidak cocok
}
// 4. Verifikasi origin
guard clientData.origin == expectedOrigin else {
throw PasskeyError.originMismatch
}
// 5. Parse dan verifikasi attestationObject (CBOR decode)
let authData = try parseAuthenticatorData(from: attestationObject)
guard authData.rpIdHash == SHA256.hash(data: Data(expectedRPID.utf8)) else {
throw PasskeyError.rpIdMismatch
}
return VerifiedCredential(
credentialId: authData.credentialId,
publicKey: authData.publicKey,
signCount: authData.signCount
)
}