PVPipe Biometric Authentication API - Complete Documentation
This document provides comprehensive documentation for the PVPipe biometric authentication API, including detailed endpoint specifications, authentication flows, and E2E API testing examples.
Table of Contents
- API Overview
- Authentication & Security
- API Endpoints
- Authentication Flows
- Error Handling
- E2E API Testing Examples
- Integration Examples
- Rate Limiting & Security
API Overview
Base URLs
- Production:
https://auth.pvpipe.com - Staging:
https://auth-staging.pvpipe.com - Local Development:
http://localhost:8080
API Version
- Version: v1
- Base Path:
/api/v1/auth
Supported Features
- ✅ Device registration with cryptographic challenge-response
- ✅ Mobile biometric authentication (Touch ID, Face ID, Fingerprint)
- ✅ Action confirmation with push notifications
- ✅ Device management and FCM token updates
- ✅ Token refresh with family rotation
- ✅ Comprehensive audit logging
Authentication & Security
Security Model
The biometric API uses a multi-layered security approach:
- JWT Bearer Authentication: Required for most endpoints
- Cryptographic Challenge-Response: For device registration and biometric operations
- Device Binding: Tokens are bound to specific registered devices
- Field-Level Encryption: Sensitive data encrypted with AES-256-GCM
Supported Cryptographic Algorithms
- ES256: ECDSA with P-256 curve and SHA-256 (recommended for mobile)
- RS256: RSA PKCS#1 v1.5 with SHA-256
- PS256: RSA PSS with SHA-256
Authorization Headers
Authorization: Bearer <jwt_access_token>API Endpoints
1. Device Registration
1.1 Create Registration Challenge
POST /api/v1/auth/devices/register/challenge
Creates a cryptographic challenge for registering a new biometric device.
Authentication: Required (JWT Bearer)
Request Body:
{
"deviceName": "John's iPhone 15",
"deviceType": "mobile",
"deviceFingerprint": "iOS-16.2-A16-TouchID-12345",
"publicKey": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE...",
"keyAlgorithm": "ES256"
}Request Schema:
| Field | Type | Required | Description |
|---|---|---|---|
deviceName |
string | ✅ | Human-readable device name (max 255 chars) |
deviceType |
string | ✅ | Device type: mobile, desktop, tablet |
deviceFingerprint |
string | ✅ | Unique device identifier |
publicKey |
string | ✅ | PEM-encoded public key |
keyAlgorithm |
string | ✅ | Signature algorithm: ES256, RS256, PS256 |
Success Response (200 OK):
{
"data": {
"challenge": "Zm9vYmFyYmF6cXV4",
"expiresAt": "2025-08-20T15:30:00Z",
"deviceId": "123e4567-e89b-12d3-a456-426614174000",
"sessionId": "987fcdeb-51a2-43d7-8f9e-123456789abc"
}
}Error Responses:
400 Bad Request: Invalid request parameters or public key format401 Unauthorized: Invalid or missing JWT token409 Conflict: Device already registered for this user
1.2 Verify Registration
POST /api/v1/auth/devices/register/verify
Completes device registration by verifying the signed challenge.
Authentication: Required (JWT Bearer)
Request Body:
{
"sessionId": "987fcdeb-51a2-43d7-8f9e-123456789abc",
"signedChallenge": "MEUCIQDKn8+..."
}Request Schema:
| Field | Type | Required | Description |
|---|---|---|---|
sessionId |
string (UUID) | ✅ | Session ID from challenge response |
signedChallenge |
string | ✅ | Base64-encoded signature of challenge |
Success Response (200 OK):
{
"data": {
"success": true,
"deviceId": "123e4567-e89b-12d3-a456-426614174000",
"device": {
"id": "123e4567-e89b-12d3-a456-426614174000",
"deviceName": "John's iPhone 15",
"deviceType": "mobile",
"deviceFingerprint": "iOS-16.2-A16-TouchID-12345",
"isActive": true,
"lastUsedAt": null,
"createdAt": "2025-08-20T14:30:00Z",
"updatedAt": "2025-08-20T14:30:00Z"
}
}
}2. Device Management
2.1 List Registered Devices
GET /api/v1/auth/devices
Lists all biometric devices registered by the authenticated user.
Authentication: Required (JWT Bearer)
Success Response (200 OK):
{
"data": {
"devices": [
{
"id": "123e4567-e89b-12d3-a456-426614174000",
"deviceName": "John's iPhone 15",
"deviceType": "mobile",
"deviceFingerprint": "iOS-16.2-A16-TouchID-12345",
"isActive": true,
"lastUsedAt": "2025-08-20T14:30:00Z",
"createdAt": "2025-08-20T14:30:00Z",
"updatedAt": "2025-08-20T14:30:00Z"
}
]
}
}2.2 Delete Device
DELETE /api/v1/auth/devices/{deviceId}
Removes a registered device and revokes all associated tokens.
Authentication: Required (JWT Bearer)
Path Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
deviceId |
string (UUID) | ✅ | Device ID to delete |
Success Response (200 OK):
{
"data": {
"success": true,
"message": "Device deleted successfully"
}
}2.3 Update FCM Token
PUT /api/v1/auth/devices/fcm-token
Updates the Firebase Cloud Messaging token for a registered device.
Authentication: Required (JWT Bearer)
Request Body:
{
"deviceId": "123e4567-e89b-12d3-a456-426614174000",
"fcmToken": "fGzJ8F2B3xF9ZqR8V3Rm7KzQj8F2B3xF9ZqR8V3Rm7KzQj8F2B3xF9ZqR8V3Rm7"
}Success Response (200 OK):
{
"data": {
"success": true,
"message": "FCM token updated successfully"
}
}3. Mobile Biometric Authentication
3.1 Request Authentication Challenge
POST /api/v1/auth/mobile/challenge
Creates a cryptographic challenge for mobile biometric authentication.
Authentication: None (public endpoint)
Request Body:
{
"deviceFingerprint": "iOS-16.2-A16-TouchID-12345"
}Success Response (200 OK):
{
"data": {
"challenge": "Y2hhbGxlbmdlZGF0YQ==",
"expiresAt": "2025-08-20T14:32:00Z",
"sessionId": "auth-session-uuid"
}
}3.2 Complete Biometric Login
POST /api/v1/auth/mobile/biometric
Verifies the signed challenge and returns authentication tokens.
Authentication: None (public endpoint)
Request Body:
{
"sessionId": "auth-session-uuid",
"signedChallenge": "MEUCIQDKn8+...",
"rememberMe": true
}Request Schema:
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
sessionId |
string (UUID) | ✅ | - | Session ID from challenge response |
signedChallenge |
string | ✅ | - | Base64-encoded signature |
rememberMe |
boolean | ❌ | false | Extended token validity (30 days vs 3 days) |
Success Response (200 OK):
{
"data": {
"success": true,
"tokens": {
"accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"accessTokenExpiresAt": "2025-08-20T15:30:00Z",
"refreshTokenExpiresAt": "2025-09-19T14:30:00Z"
}
}
}3.3 Refresh Tokens
POST /api/v1/auth/mobile/refresh
Refreshes access token using biometric refresh token with family rotation.
Authentication: None (uses refresh token)
Request Body:
{
"refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}Success Response (200 OK):
{
"data": {
"accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"accessTokenExpiresAt": "2025-08-20T15:30:00Z",
"refreshTokenExpiresAt": "2025-09-19T14:30:00Z"
}
}4. Biometric Confirmation
4.1 Initiate Confirmation
POST /api/v1/auth/confirmation/initiate
Creates a biometric confirmation session for sensitive actions.
Authentication: Required (JWT Bearer)
Request Body:
{
"actionType": "transfer_money",
"actionPayload": {
"amount": 50000,
"toAccount": "VCB-123456789",
"currency": "VND",
"description": "Payment to supplier"
}
}Request Schema:
| Field | Type | Required | Description |
|---|---|---|---|
actionType |
string | ✅ | Type of action (max 100 chars) |
actionPayload |
object | ❌ | Action-specific data (max 4KB when serialized) |
Common Action Types:
transfer_money: Financial transfersapprove_document: Document approvalsdelete_user: User managementpurchase_order: Purchase approvalssystem_config: System configuration changes
Success Response (200 OK):
{
"data": {
"confirmationId": "conf-123e4567-e89b-12d3-a456-426614174000",
"challenge": "Y29uZmlybWF0aW9uY2hhbGxlbmdl",
"expiresAt": "2025-08-20T14:35:00Z"
}
}4.2 Check Confirmation Status
GET /api/v1/auth/confirmation/{id}/status
Polls the status of a confirmation session.
Authentication: Required (JWT Bearer)
Path Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
id |
string (UUID) | ✅ | Confirmation session ID |
Success Response (200 OK):
{
"data": {
"confirmationId": "conf-123e4567-e89b-12d3-a456-426614174000",
"status": "pending",
"actionType": "transfer_money",
"actionPayload": {
"amount": 50000,
"toAccount": "VCB-123456789",
"currency": "VND"
},
"createdAt": "2025-08-20T14:30:00Z",
"expiresAt": "2025-08-20T14:35:00Z",
"updatedAt": "2025-08-20T14:30:00Z"
}
}Possible Status Values:
pending: Waiting for confirmationapproved: Confirmed by biometric signaturerejected: Explicitly rejectedexpired: Session expired without action
4.3 Verify Confirmation
POST /api/v1/auth/confirmation/{id}/verify
Approves a confirmation session using biometric signature.
Authentication: Required (JWT Bearer)
Request Body:
{
"deviceId": "123e4567-e89b-12d3-a456-426614174000",
"signedChallenge": "MEUCIQDKn8+..."
}Success Response (200 OK):
{
"data": {
"success": true,
"confirmationId": "conf-123e4567-e89b-12d3-a456-426614174000",
"status": "approved"
}
}4.4 Reject Confirmation
POST /api/v1/auth/confirmation/{id}/reject
Explicitly rejects a confirmation session.
Authentication: Required (JWT Bearer)
Request Body:
{
"reason": "Suspicious activity detected"
}Success Response (200 OK):
{
"data": {
"success": true,
"confirmationId": "conf-123e4567-e89b-12d3-a456-426614174000",
"status": "rejected"
}
}Authentication Flows
Flow 1: Device Registration
Flow 2: Mobile Biometric Login
Flow 3: Action Confirmation
Error Handling
Standard Error Response Format
{
"message": "Error description",
"statusCode": 400
}Common Error Codes
| Status Code | Description | Common Causes |
|---|---|---|
400 Bad Request |
Invalid request parameters | Invalid JSON, missing required fields, invalid signatures |
401 Unauthorized |
Authentication required/invalid | Missing JWT token, expired token, invalid refresh token |
403 Forbidden |
Insufficient permissions | Valid token but no access to resource |
404 Not Found |
Resource not found | Invalid device ID, confirmation ID, or user not found |
409 Conflict |
Resource conflict | Device already registered, duplicate fingerprint |
429 Too Many Requests |
Rate limit exceeded | Too many challenge requests, login attempts |
500 Internal Server Error |
Server error | Database connection, Redis unavailable, FCM errors |
Error Examples
Invalid Public Key Format
{
"message": "Invalid public key format: not valid PEM encoding",
"statusCode": 400
}Device Not Found
{
"message": "Device not found or inactive",
"statusCode": 404
}Signature Verification Failed
{
"message": "Invalid signature: signature verification failed",
"statusCode": 401
}Session Expired
{
"message": "Session expired or not found",
"statusCode": 400
}E2E API Testing Examples
Testing Environment Setup
Prerequisites
- Valid JWT access token for authenticated endpoints
- Registered test devices for biometric operations
- Firebase project for push notifications
- Test user account with appropriate permissions
Test Data
{
"testUser": {
"id": 12345,
"email": "[email protected]",
"jwt": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
},
"testDevice": {
"deviceName": "Test iPhone 15 Pro",
"deviceType": "mobile",
"deviceFingerprint": "TEST-iOS-16.2-A16-TouchID-12345",
"keyAlgorithm": "ES256"
}
}Test Suite 1: Device Registration Flow
Test 1.1: Complete Device Registration
// Step 1: Generate test key pair (ES256)
const keyPair = await generateES256KeyPair();
const publicKeyPEM = await exportPublicKeyToPEM(keyPair.publicKey);
// Step 2: Request registration challenge
const challengeResponse = await fetch('/api/v1/auth/devices/register/challenge', {
method: 'POST',
headers: {
'Authorization': `Bearer ${testUser.jwt}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
deviceName: testDevice.deviceName,
deviceType: testDevice.deviceType,
deviceFingerprint: testDevice.deviceFingerprint,
publicKey: publicKeyPEM,
keyAlgorithm: testDevice.keyAlgorithm
})
});
const challenge = await challengeResponse.json();
assert.equal(challengeResponse.status, 200);
assert.exists(challenge.data.sessionId);
assert.exists(challenge.data.challenge);
// Step 3: Sign challenge
const challengeBytes = base64Decode(challenge.data.challenge);
const signature = await signWithPrivateKey(keyPair.privateKey, challengeBytes);
const signatureBase64 = base64Encode(signature);
// Step 4: Verify registration
const verifyResponse = await fetch('/api/v1/auth/devices/register/verify', {
method: 'POST',
headers: {
'Authorization': `Bearer ${testUser.jwt}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
sessionId: challenge.data.sessionId,
signedChallenge: signatureBase64
})
});
const verification = await verifyResponse.json();
assert.equal(verifyResponse.status, 200);
assert.equal(verification.data.success, true);
assert.exists(verification.data.deviceId);
// Store device ID for subsequent tests
const deviceId = verification.data.deviceId;Test 1.2: Duplicate Device Registration (Error Case)
// Attempt to register the same device again
const duplicateResponse = await fetch('/api/v1/auth/devices/register/challenge', {
method: 'POST',
headers: {
'Authorization': `Bearer ${testUser.jwt}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
deviceName: testDevice.deviceName,
deviceType: testDevice.deviceType,
deviceFingerprint: testDevice.deviceFingerprint, // Same fingerprint
publicKey: publicKeyPEM,
keyAlgorithm: testDevice.keyAlgorithm
})
});
assert.equal(duplicateResponse.status, 409);
const error = await duplicateResponse.json();
assert.include(error.message.toLowerCase(), 'already registered');Test Suite 2: Mobile Biometric Authentication
Test 2.1: Complete Login Flow
// Step 1: Request authentication challenge
const authChallengeResponse = await fetch('/api/v1/auth/mobile/challenge', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
deviceFingerprint: testDevice.deviceFingerprint
})
});
const authChallenge = await authChallengeResponse.json();
assert.equal(authChallengeResponse.status, 200);
assert.exists(authChallenge.data.sessionId);
// Step 2: Sign challenge and complete login
const authChallengeBytes = base64Decode(authChallenge.data.challenge);
const authSignature = await signWithPrivateKey(keyPair.privateKey, authChallengeBytes);
const loginResponse = await fetch('/api/v1/auth/mobile/biometric', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
sessionId: authChallenge.data.sessionId,
signedChallenge: base64Encode(authSignature),
rememberMe: true
})
});
const loginResult = await loginResponse.json();
assert.equal(loginResponse.status, 200);
assert.equal(loginResult.data.success, true);
assert.exists(loginResult.data.tokens.accessToken);
assert.exists(loginResult.data.tokens.refreshToken);
// Store tokens for subsequent tests
const { accessToken, refreshToken } = loginResult.data.tokens;Test 2.2: Token Refresh
// Test token refresh with family rotation
const refreshResponse = await fetch('/api/v1/auth/mobile/refresh', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
refreshToken: refreshToken
})
});
const refreshResult = await refreshResponse.json();
assert.equal(refreshResponse.status, 200);
assert.exists(refreshResult.data.accessToken);
assert.exists(refreshResult.data.refreshToken);
assert.notEqual(refreshResult.data.refreshToken, refreshToken); // New tokenTest Suite 3: Action Confirmation Flow
Test 3.1: Money Transfer Confirmation
// Step 1: Initiate confirmation
const confirmationRequest = {
actionType: "transfer_money",
actionPayload: {
amount: 50000,
toAccount: "VCB-123456789",
currency: "VND",
description: "Test transfer"
}
};
const initiateResponse = await fetch('/api/v1/auth/confirmation/initiate', {
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(confirmationRequest)
});
const confirmation = await initiateResponse.json();
assert.equal(initiateResponse.status, 200);
assert.exists(confirmation.data.confirmationId);
assert.exists(confirmation.data.challenge);
const confirmationId = confirmation.data.confirmationId;
// Step 2: Check initial status
const statusResponse = await fetch(`/api/v1/auth/confirmation/${confirmationId}/status`, {
headers: { 'Authorization': `Bearer ${accessToken}` }
});
const status = await statusResponse.json();
assert.equal(status.data.status, 'pending');
assert.equal(status.data.actionType, 'transfer_money');
// Step 3: Verify confirmation with biometric
const confirmationChallengeBytes = base64Decode(confirmation.data.challenge);
const confirmationSignature = await signWithPrivateKey(keyPair.privateKey, confirmationChallengeBytes);
const verifyConfirmationResponse = await fetch(`/api/v1/auth/confirmation/${confirmationId}/verify`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
deviceId: deviceId,
signedChallenge: base64Encode(confirmationSignature)
})
});
const verifyResult = await verifyConfirmationResponse.json();
assert.equal(verifyConfirmationResponse.status, 200);
assert.equal(verifyResult.data.success, true);
assert.equal(verifyResult.data.status, 'approved');
// Step 4: Verify final status
const finalStatusResponse = await fetch(`/api/v1/auth/confirmation/${confirmationId}/status`, {
headers: { 'Authorization': `Bearer ${accessToken}` }
});
const finalStatus = await finalStatusResponse.json();
assert.equal(finalStatus.data.status, 'approved');Test 3.2: Document Approval Confirmation
const documentConfirmation = {
actionType: "approve_document",
actionPayload: {
documentId: "doc-12345",
documentName: "Contract Amendment",
documentType: "contract",
requiredApprovals: 2,
currentApprovals: 1
}
};
const docResponse = await fetch('/api/v1/auth/confirmation/initiate', {
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(documentConfirmation)
});
const docConfirmation = await docResponse.json();
assert.equal(docResponse.status, 200);
assert.equal(docConfirmation.data.actionType, 'approve_document');Test Suite 4: Device Management
Test 4.1: List Devices
const devicesResponse = await fetch('/api/v1/auth/devices', {
headers: { 'Authorization': `Bearer ${accessToken}` }
});
const devices = await devicesResponse.json();
assert.equal(devicesResponse.status, 200);
assert.isArray(devices.data.devices);
assert.isAtLeast(devices.data.devices.length, 1);
const device = devices.data.devices.find(d => d.id === deviceId);
assert.exists(device);
assert.equal(device.deviceName, testDevice.deviceName);Test 4.2: Update FCM Token
const fcmTokenRequest = {
deviceId: deviceId,
fcmToken: "test-fcm-token-" + Date.now()
};
const fcmResponse = await fetch('/api/v1/auth/devices/fcm-token', {
method: 'PUT',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(fcmTokenRequest)
});
const fcmResult = await fcmResponse.json();
assert.equal(fcmResponse.status, 200);
assert.equal(fcmResult.data.success, true);Test 4.3: Delete Device
const deleteResponse = await fetch(`/api/v1/auth/devices/${deviceId}`, {
method: 'DELETE',
headers: { 'Authorization': `Bearer ${accessToken}` }
});
const deleteResult = await deleteResponse.json();
assert.equal(deleteResponse.status, 200);
assert.equal(deleteResult.data.success, true);
// Verify device is no longer listed
const verifyDevicesResponse = await fetch('/api/v1/auth/devices', {
headers: { 'Authorization': `Bearer ${accessToken}` }
});
const verifyDevices = await verifyDevicesResponse.json();
const deletedDevice = verifyDevices.data.devices.find(d => d.id === deviceId);
assert.notExists(deletedDevice);Test Suite 5: Error Handling
Test 5.1: Invalid Signature
const invalidSignature = "invalid-signature-data";
const invalidResponse = await fetch('/api/v1/auth/mobile/biometric', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
sessionId: authChallenge.data.sessionId,
signedChallenge: invalidSignature,
rememberMe: false
})
});
assert.equal(invalidResponse.status, 401);
const error = await invalidResponse.json();
assert.include(error.message.toLowerCase(), 'signature');Test 5.2: Expired Session
// Wait for session to expire (or use expired session ID)
await new Promise(resolve => setTimeout(resolve, 5000));
const expiredResponse = await fetch('/api/v1/auth/mobile/biometric', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
sessionId: authChallenge.data.sessionId,
signedChallenge: base64Encode(authSignature),
rememberMe: false
})
});
assert.equal(expiredResponse.status, 400);
const expiredError = await expiredResponse.json();
assert.include(expiredError.message.toLowerCase(), 'expired');Test Suite 6: Performance & Load Testing
Test 6.1: Concurrent Device Registration
const concurrentRegistrations = Array(10).fill().map(async (_, index) => {
const uniqueKeyPair = await generateES256KeyPair();
const uniquePublicKey = await exportPublicKeyToPEM(uniqueKeyPair.publicKey);
return fetch('/api/v1/auth/devices/register/challenge', {
method: 'POST',
headers: {
'Authorization': `Bearer ${testUser.jwt}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
deviceName: `Test Device ${index}`,
deviceType: testDevice.deviceType,
deviceFingerprint: `TEST-DEVICE-${index}-${Date.now()}`,
publicKey: uniquePublicKey,
keyAlgorithm: testDevice.keyAlgorithm
})
});
});
const results = await Promise.all(concurrentRegistrations);
const successCount = results.filter(r => r.status === 200).length;
assert.isAtLeast(successCount, 8); // Allow some failures under loadTest 6.2: Challenge Request Rate Limiting
const rapidRequests = Array(20).fill().map(() =>
fetch('/api/v1/auth/mobile/challenge', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
deviceFingerprint: testDevice.deviceFingerprint
})
})
);
const rapidResults = await Promise.all(rapidRequests);
const rateLimitedCount = rapidResults.filter(r => r.status === 429).length;
assert.isAtLeast(rateLimitedCount, 1); // Should hit rate limitIntegration Examples
React Native Mobile App Integration
Key Pair Generation (ES256)
import { generateKeyPair, exportKey, sign } from 'react-native-crypto';
// Generate ES256 key pair in Secure Enclave/Hardware Security Module
export const generateBiometricKeyPair = async () => {
const keyPair = await generateKeyPair('ECDSA', {
namedCurve: 'P-256',
// Store in secure hardware
keyUsages: ['sign'],
extractable: false, // Cannot be exported from secure storage
secureStorage: true
});
return keyPair;
};
// Export public key for registration
export const exportPublicKeyForRegistration = async (publicKey) => {
const exported = await exportKey('spki', publicKey);
return btoa(String.fromCharCode(...new Uint8Array(exported)));
};Device Registration Flow
import BiometricAuth from 'react-native-biometric-auth';
export class BiometricRegistrationService {
constructor(apiClient) {
this.apiClient = apiClient;
}
async registerDevice(deviceName, accessToken) {
try {
// Step 1: Generate key pair in secure hardware
const keyPair = await generateBiometricKeyPair();
const publicKeyPEM = await exportPublicKeyForRegistration(keyPair.publicKey);
// Step 2: Create device fingerprint
const deviceFingerprint = await this.createDeviceFingerprint();
// Step 3: Request challenge
const challengeResponse = await this.apiClient.post('/devices/register/challenge', {
deviceName,
deviceType: 'mobile',
deviceFingerprint,
publicKey: publicKeyPEM,
keyAlgorithm: 'ES256'
}, {
headers: { Authorization: `Bearer ${accessToken}` }
});
const { challenge, sessionId } = challengeResponse.data.data;
// Step 4: Request biometric authentication
const biometricResult = await BiometricAuth.authenticate({
promptMessage: 'Register this device for secure access',
fallbackPromptMessage: 'Use your device passcode'
});
if (!biometricResult.success) {
throw new Error('Biometric authentication failed');
}
// Step 5: Sign challenge
const challengeBytes = this.base64ToBytes(challenge);
const signature = await sign(keyPair.privateKey, challengeBytes);
const signatureBase64 = this.bytesToBase64(signature);
// Step 6: Complete registration
const verifyResponse = await this.apiClient.post('/devices/register/verify', {
sessionId,
signedChallenge: signatureBase64
}, {
headers: { Authorization: `Bearer ${accessToken}` }
});
return verifyResponse.data.data;
} catch (error) {
console.error('Device registration failed:', error);
throw error;
}
}
async createDeviceFingerprint() {
const deviceInfo = await DeviceInfo.getDeviceInfo();
return `${deviceInfo.platform}-${deviceInfo.version}-${deviceInfo.processor}-${deviceInfo.biometryType}-${deviceInfo.uniqueId}`;
}
}Biometric Login Flow
export class BiometricLoginService {
constructor(apiClient) {
this.apiClient = apiClient;
}
async loginWithBiometric(deviceFingerprint) {
try {
// Step 1: Request challenge
const challengeResponse = await this.apiClient.post('/mobile/challenge', {
deviceFingerprint
});
const { challenge, sessionId } = challengeResponse.data.data;
// Step 2: Authenticate with biometric
const biometricResult = await BiometricAuth.authenticate({
promptMessage: 'Sign in with your biometric',
fallbackPromptMessage: 'Use your device passcode'
});
if (!biometricResult.success) {
throw new Error('Biometric authentication cancelled');
}
// Step 3: Sign challenge with stored private key
const keyPair = await this.getStoredKeyPair();
const challengeBytes = this.base64ToBytes(challenge);
const signature = await sign(keyPair.privateKey, challengeBytes);
// Step 4: Complete login
const loginResponse = await this.apiClient.post('/mobile/biometric', {
sessionId,
signedChallenge: this.bytesToBase64(signature),
rememberMe: true
});
const tokens = loginResponse.data.data.tokens;
// Store tokens securely
await this.storeTokensSecurely(tokens);
return tokens;
} catch (error) {
console.error('Biometric login failed:', error);
throw error;
}
}
}Web Application Integration
Action Confirmation Flow
export class ActionConfirmationService {
constructor(apiClient, notificationService) {
this.apiClient = apiClient;
this.notificationService = notificationService;
}
async initiateConfirmation(actionType, actionPayload, accessToken) {
try {
// Step 1: Initiate confirmation
const response = await this.apiClient.post('/confirmation/initiate', {
actionType,
actionPayload
}, {
headers: { Authorization: `Bearer ${accessToken}` }
});
const confirmation = response.data.data;
// Step 2: Show confirmation dialog to user
this.showConfirmationDialog(confirmation, actionType, actionPayload);
// Step 3: Start polling for status
return this.pollConfirmationStatus(confirmation.confirmationId, accessToken);
} catch (error) {
console.error('Failed to initiate confirmation:', error);
throw error;
}
}
async pollConfirmationStatus(confirmationId, accessToken, maxAttempts = 60) {
for (let attempt = 0; attempt < maxAttempts; attempt++) {
try {
const response = await this.apiClient.get(`/confirmation/${confirmationId}/status`, {
headers: { Authorization: `Bearer ${accessToken}` }
});
const status = response.data.data.status;
if (status === 'approved') {
return { success: true, status: 'approved' };
} else if (status === 'rejected') {
return { success: false, status: 'rejected' };
} else if (status === 'expired') {
return { success: false, status: 'expired' };
}
// Continue polling if still pending
await new Promise(resolve => setTimeout(resolve, 5000)); // 5 second interval
} catch (error) {
console.error('Error polling confirmation status:', error);
if (attempt === maxAttempts - 1) {
throw error;
}
}
}
// Timeout
return { success: false, status: 'timeout' };
}
showConfirmationDialog(confirmation, actionType, actionPayload) {
const dialog = document.createElement('div');
dialog.className = 'confirmation-dialog';
dialog.innerHTML = `
<div class="confirmation-content">
<h3>Biometric Confirmation Required</h3>
<p>A confirmation request has been sent to your registered mobile device.</p>
<div class="action-details">
<strong>Action:</strong> ${this.formatActionType(actionType)}<br>
<strong>Details:</strong> ${this.formatActionPayload(actionPayload)}
</div>
<div class="confirmation-status">
<div class="spinner"></div>
<span>Waiting for confirmation...</span>
</div>
<button onclick="this.closest('.confirmation-dialog').remove()">Cancel</button>
</div>
`;
document.body.appendChild(dialog);
return dialog;
}
}Rate Limiting & Security
Rate Limiting Rules
| Endpoint | Limit | Window | Scope |
|---|---|---|---|
/mobile/challenge |
10 requests | 1 minute | per device fingerprint |
/devices/register/challenge |
5 requests | 5 minutes | per user |
/confirmation/initiate |
20 requests | 1 hour | per user |
/mobile/biometric |
3 requests | 1 minute | per session |
| All endpoints | 1000 requests | 1 hour | per IP address |
Security Headers
All API responses include security headers:
Strict-Transport-Security: max-age=31536000; includeSubDomains
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
X-XSS-Protection: 1; mode=block
Content-Security-Policy: default-src 'self'Audit Logging
All biometric operations are logged with:
- User ID and device ID
- IP address and User-Agent
- Timestamp and operation result
- Error details for failed operations
- Challenge and signature metadata (hashed)
This comprehensive documentation provides everything needed to integrate with and test the PVPipe biometric authentication API. The examples cover all major flows and edge cases for robust production integration.