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