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

  1. API Overview
  2. Authentication & Security
  3. API Endpoints
  4. Authentication Flows
  5. Error Handling
  6. E2E API Testing Examples
  7. Integration Examples
  8. 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:

  1. JWT Bearer Authentication: Required for most endpoints
  2. Cryptographic Challenge-Response: For device registration and biometric operations
  3. Device Binding: Tokens are bound to specific registered devices
  4. 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 format
  • 401 Unauthorized: Invalid or missing JWT token
  • 409 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 transfers
  • approve_document: Document approvals
  • delete_user: User management
  • purchase_order: Purchase approvals
  • system_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 confirmation
  • approved: Confirmed by biometric signature
  • rejected: Explicitly rejected
  • expired: 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": "test@pvpipe.com",
    "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 token

Test 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 load

Test 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 limit

Integration 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.