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