iOS: Authentication
Authenticate an existing Passkey with the Service using a previously registered credential.
Overview
The authentication (assertion) process verifies ownership of a registered passkey by:
- Requesting assertion options from the Liquid Auth service
- Retrieving the associated P256 key pair from secure storage
- Signing the challenge with your Algorand Ed25519 key
- Creating a WebAuthn assertion response with P256 signature
- Submitting the credential to complete authentication
This is specifically for Liquid URIs/QR Codes - provided by the Liquid Auth backend.
To handle FIDO:/
URIs/QR Codes, refer to the Autofill Credential Extension.
Implementation
Complete Authentication Function
import AuthenticationServicesimport CryptoKitimport Foundationimport LiquidAuthSDK
func authentication( origin: String, requestId: String, algorandAddress: String, p256KeyPair: P256.Signing.PrivateKey, userAgent: String, device: String) async throws -> LiquidAuthResult { let assertionApi = AssertionApi()
// Step 1: Calculate credential ID from P256 public key let credentialId = Data([UInt8](Utility.hashSHA256(p256KeyPair.publicKey.rawRepresentation))) .base64URLEncodedString()
// Step 2: Request assertion options from service let (data, sessionCookie) = try await assertionApi.postAssertionOptions( origin: origin, userAgent: userAgent, credentialId: credentialId )
// Step 3: Parse server response guard let json = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any], let challengeBase64Url = json["challenge"] as? String else { throw LiquidAuthError.invalidServerResponse }
// Extract rpId (supports both formats) let rpId: String if let rp = json["rp"] as? [String: Any], let id = rp["id"] as? String { rpId = id } else if let id = json["rpId"] as? String { rpId = id } else { throw LiquidAuthError.missingRpId }
// Validate origin matches rpId if origin != rpId { print("⚠️ Origin (\(origin)) and rpId (\(rpId)) are different.") }
// Step 4: Sign challenge with Algorand key let challengeBytes = Data([UInt8](Utility.decodeBase64Url(challengeBase64Url)!))
// INTEGRATION POINT: Sign with your Algorand Ed25519 private key let signature = try signChallenge(challengeBytes, with: yourAlgorandPrivateKey)
// Step 5: Create Liquid extension let liquidExt = [ "type": "algorand", "requestId": requestId, "address": algorandAddress, "signature": signature.base64URLEncodedString(), "device": device, ]
// Step 6: Build WebAuthn assertion let assertionResponse = try buildAssertionResponse( challengeBase64Url: challengeBase64Url, rpId: rpId, credentialId: credentialId, p256KeyPair: p256KeyPair )
// Step 7: Submit assertion to service let responseData = try await assertionApi.postAssertionResult( origin: origin, userAgent: userAgent, credential: assertionResponse, liquidExt: liquidExt )
// Step 8: Handle response return try parseAuthenticationResponse(responseData) // Check for errors, return success if none.}
Building the Assertion Response
private func buildAssertionResponse( challengeBase64Url: String, rpId: String, credentialId: String, p256KeyPair: P256.Signing.PrivateKey) throws -> String {
// Create clientDataJSON let clientData: [String: Any] = [ "type": "webauthn.get", "challenge": challengeBase64Url, "origin": "https://\(rpId)", ]
guard let clientDataJSONData = try? JSONSerialization.data(withJSONObject: clientData, options: []) else { throw LiquidAuthError.clientDataCreationFailed }
let clientDataJSONBase64Url = clientDataJSONData.base64URLEncodedString()
// Create authenticator data let rpIdHash = Utility.hashSHA256(rpId.data(using: .utf8)!) let authenticatorData = AuthenticatorData.assertion( rpIdHash: rpIdHash, userPresent: true, userVerified: true, // Ensure user verification! backupEligible: false, backupState: false ).toData()
// Sign authenticatorData + clientDataHash with P256 key let clientDataHash = Utility.hashSHA256(clientDataJSONData) let dataToSign = authenticatorData + clientDataHash let p256Signature = try p256KeyPair.signature(for: dataToSign)
// Build assertion response let assertionResponse: [String: Any] = [ "id": credentialId, "type": "public-key", "userHandle": "tester", // Can be customized based on your needs "rawId": credentialId, "response": [ "clientDataJSON": clientDataJSONData.base64URLEncodedString(), "authenticatorData": authenticatorData.base64URLEncodedString(), "signature": p256Signature.derRepresentation.base64URLEncodedString(), ], ]
// Serialize to JSON string guard let responseData = try? JSONSerialization.data(withJSONObject: assertionResponse, options: []), let responseJSON = String(data: responseData, encoding: .utf8) else { throw LiquidAuthError.assertionSerializationFailed }
return responseJSON}
Key Retrieval
Before authentication, you need to retrieve the P256 key pair associated with the credentialId
.
Alternatively, you can deterministically regenerate the P256 keypair (passkey) on the fly. We refer you to the deterministic-P256-swift library.
User Verification
Implement proper user verification before completing authentication, prompting the user to confirm.
import LocalAuthentication
func authenticateUser() async throws -> Bool { let context = LAContext() var error: NSError?
// Check for biometric availability guard context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) else { // Fall back to device passcode guard context.canEvaluatePolicy(.deviceOwnerAuthentication, error: &error) else { throw LiquidAuthError.authenticationNotAvailable }
return try await context.evaluatePolicy( .deviceOwnerAuthentication, localizedReason: "Authenticate to sign in with passkey" ) }
return try await context.evaluatePolicy( .deviceOwnerAuthenticationWithBiometrics, localizedReason: "Use Touch ID or Face ID to sign in" )}
Security Considerations
- Always verify user presence before authentication
- Validate request origins against your allowlist
- Securely store or regenerate P256 key pairs (or any mnemonics)
- Implement proper timeouts for user verification
- Log authentication attempts for security monitoring
Next Steps
After successful authentication:
- Peer Communication: Set up WebRTC signaling for dApp communication
- Autofill Extension: Integrate with iOS native passkey management