Section 6/171 menit

6. Autentikasi dengan Passkey

6. Autentikasi dengan Passkey

Authentication Manager

swift
// MARK: - Authentication Manager

struct PasskeyAssertionChallenge {
    let challenge: Data
    let relyingPartyID: String
    let allowedCredentialIDs: [Data]  // kosong = biarkan system pilih
}

struct PasskeyAssertionResponse {
    let credentialID: Data
    let rawClientDataJSON: Data
    let rawAuthenticatorData: Data
    let signature: Data
    let userID: Data?
}

@MainActor
final class PasskeyAuthenticationManager: NSObject {
    private var authController: ASAuthorizationController?
    private var assertionContinuation: CheckedContinuation<PasskeyAssertionResponse, Error>?

    func authenticate(with challenge: PasskeyAssertionChallenge) async throws -> PasskeyAssertionResponse {
        return try await withCheckedThrowingContinuation { continuation in
            self.assertionContinuation = continuation

            let provider = ASAuthorizationPlatformPublicKeyCredentialProvider(
                relyingPartyIdentifier: challenge.relyingPartyID
            )

            let request = provider.createCredentialAssertionRequest(
                challenge: challenge.challenge
            )

            // Jika punya credential ID yang diketahui, sertakan
            if !challenge.allowedCredentialIDs.isEmpty {
                request.allowedCredentials = challenge.allowedCredentialIDs.map {
                    ASAuthorizationPlatformPublicKeyCredentialDescriptor(credentialID: $0)
                }
            }

            request.userVerificationPreference = .required

            let controller = ASAuthorizationController(authorizationRequests: [request])
            controller.delegate = self
            controller.presentationContextProvider = self
            controller.performRequests()

            self.authController = controller
        }
    }
}

extension PasskeyAuthenticationManager: ASAuthorizationControllerDelegate {
    func authorizationController(
        controller: ASAuthorizationController,
        didCompleteWithAuthorization authorization: ASAuthorization
    ) {
        guard let credential = authorization.credential as? ASAuthorizationPlatformPublicKeyCredentialAssertion else {
            assertionContinuation?.resume(throwing: PasskeyError.unexpectedCredentialType)
            return
        }

        let response = PasskeyAssertionResponse(
            credentialID: credential.credentialID,
            rawClientDataJSON: credential.rawClientDataJSON,
            rawAuthenticatorData: credential.rawAuthenticatorData,
            signature: credential.signature,
            userID: credential.userID
        )

        assertionContinuation?.resume(returning: response)
        assertionContinuation = nil
    }

    func authorizationController(
        controller: ASAuthorizationController,
        didCompleteWithError error: Error
    ) {
        if let authError = error as? ASAuthorizationError, authError.code == .canceled {
            assertionContinuation?.resume(throwing: PasskeyError.userCancelled)
        } else {
            assertionContinuation?.resume(throwing: error)
        }
        assertionContinuation = nil
    }
}

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

Penggunaan Autentikasi

swift
// MARK: - Login ViewModel

@MainActor
@Observable
final class LoginViewModel {
    var authState: AuthState = .unauthenticated
    private let authManager = PasskeyAuthenticationManager()
    private let authAPI: AuthAPIClient

    enum AuthState {
        case unauthenticated, authenticating, authenticated(User), error(String)
    }

    init(authAPI: AuthAPIClient) {
        self.authAPI = authAPI
    }

    func loginWithPasskey() async {
        authState = .authenticating
        do {
            // 1. Minta challenge dari server
            let serverChallenge = try await authAPI.beginPasskeyAuthentication()

            let challenge = PasskeyAssertionChallenge(
                challenge: Data(base64URLEncoded: serverChallenge.challenge)!,
                relyingPartyID: "yourdomain.com",
                allowedCredentialIDs: []  // kosong = tampilkan semua passkey yang tersedia
            )

            // 2. Tampilkan Face ID / Touch ID sheet
            let response = try await authManager.authenticate(with: challenge)

            // 3. Verifikasi signature di server
            let user = try await authAPI.finishPasskeyAuthentication(
                credentialID: response.credentialID.base64URLEncodedString(),
                clientDataJSON: response.rawClientDataJSON.base64URLEncodedString(),
                authenticatorData: response.rawAuthenticatorData.base64URLEncodedString(),
                signature: response.signature.base64URLEncodedString(),
                userID: response.userID?.base64URLEncodedString()
            )

            authState = .authenticated(user)
        } catch PasskeyError.userCancelled {
            authState = .unauthenticated
        } catch {
            authState = .error(error.localizedDescription)
        }
    }
}