IBExWalletAPI
Integration

Passkey Integration

Passkeys and Wallets Integration Guide

This guide explains how to integrate your application with the IBEx Fi API for passkey authentication and wallet management.

Table of Contents

  1. General Architecture
  2. Domain Configuration
  3. rpId Logic
  4. Integration Examples
  5. Authentication Flow
  6. Error Handling
  7. Best Practices

General Architecture

Components

  • IBEx Fi API: Central server managing passkeys and wallets
  • Local Client: Your development application
  • CNAME: DNS redirection to the API
  • Database: Storage of authorized domains

Request Flow

Local Client → CNAME → IBEx Fi API → Database

Domain Configuration

1. Local Development

For local development with WebAuthn, you must use domains ending with .localhost:

// ✅ Correct - WebAuthn works over HTTP on .localhost
const API_URL = 'http://ibexwallet.org.localhost:3004';
 
// ❌ Incorrect - WebAuthn does not work over HTTP on other domains
const API_URL = 'http://ibexwallet.org:3004';

2. CNAME Configuration

Create a CNAME pointing to the staging API:

passkeys-localhost.ibexwallet.org → passkeys-staging.ibex.fi

3. Domain Registration

The domain must be registered in the API database. Contact the IBEx team to add your domain.

rpId Logic

Automatic Calculation

The API automatically calculates the rpId based on the request hostname:

// Calculation example
const hostname = "passkeys-localhost.ibexwallet.org";
const EXTERNAL_DOMAIN_PREFIX = "passkeys-";
const ENV = "staging";
 
if (hostname.startsWith(`${EXTERNAL_DOMAIN_PREFIX}${ENV}`)) {
    // passkeys-staging.ibexwallet.org → ibexwallet.org
    rpId = hostname.substring("passkeys-staging".length + 1);
} else if (hostname.startsWith(`${EXTERNAL_DOMAIN_PREFIX}localhost`)) {
    // passkeys-localhost.ibexwallet.org → ibexwallet.org.localhost
    rpId = hostname.substring("passkeys-localhost".length + 1) + ".localhost";
}

Matching Examples

Received HostnameCalculated rpIdClient Domain
passkeys-staging.ibexwallet.orgibexwallet.orghttps://ibexwallet.org
passkeys-localhost.ibexwallet.orgibexwallet.org.localhosthttp://ibexwallet.org.localhost:3004

Integration Examples

Example 1: Local Development

// Client configuration
const API_BASE_URL = 'http://ibexwallet.org.localhost:3004';
const CNAME_URL = 'https://passkeys-localhost.ibexwallet.org';
 
// 1. Passkey Registration
async function registerPasskey() {
    // Fetch registration options
    const response = await fetch(`${CNAME_URL}/v1.2/auth/sign-up`, {
        method: 'GET',
        headers: {
            'Content-Type': 'application/json'
        }
    });
    
    const { credentialRequestOptions } = await response.json();
    
    // Create passkey client-side
    const credential = await navigator.credentials.create({
        publicKey: credentialRequestOptions
    });
    
    // Send passkey to the API
    const signUpResponse = await fetch(`${CNAME_URL}/v1.2/auth/sign-in`, {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json'
        },
        body: JSON.stringify({
            credential: credential
        })
    });
    
    const { access_token, refresh_token } = await signUpResponse.json();
    return { access_token, refresh_token };
}
 
// 2. Passkey Authentication
async function authenticatePasskey() {
    // Fetch authentication options
    const response = await fetch(`${CNAME_URL}/v1.2/auth/sign-in`, {
        method: 'GET',
        headers: {
            'Content-Type': 'application/json'
        }
    });
    
    const { credentialRequestOptions } = await response.json();
    
    // Authenticate with the passkey
    const credential = await navigator.credentials.get({
        publicKey: credentialRequestOptions
    });
    
    // Send authentication to the API
    const authResponse = await fetch(`${CNAME_URL}/v1.2/auth/sign-in`, {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json'
        },
        body: JSON.stringify({
            credential: credential
        })
    });
    
    const { access_token, refresh_token } = await authResponse.json();
    return { access_token, refresh_token };
}

Example 2: Production

// Production configuration
const API_BASE_URL = 'https://passkeys-staging.ibexwallet.org';
 
// Same code as development, but with the production URL
async function registerPasskey() {
    const response = await fetch(`${API_BASE_URL}/v1.2/auth/sign-up`, {
        method: 'GET',
        headers: {
            'Content-Type': 'application/json'
        }
    });
    
    // ... remainder of the code is identical
}

Example 3: Wallet Management

// Fetch wallet information
async function getWalletInfo(accessToken) {
    const response = await fetch(`${API_BASE_URL}/v1.2/users/me`, {
        method: 'GET',
        headers: {
            'Authorization': `Bearer ${accessToken}`,
            'Content-Type': 'application/json'
        }
    });
    
    return await response.json();
}
 
// Fetch user safes
async function getUserSafes(accessToken) {
    const response = await fetch(`${API_BASE_URL}/v1.2/safes`, {
        method: 'GET',
        headers: {
            'Authorization': `Bearer ${accessToken}`,
            'Content-Type': 'application/json'
        }
    });
    
    return await response.json();
}

Authentication Flow

1. Sign Up

sequenceDiagram
    participant Client
    participant API
    participant Browser
    
    Client->>API: GET /v1.2/auth/sign-up
    API->>Client: credentialRequestOptions
    Client->>Browser: navigator.credentials.create()
    Browser->>Client: credential
    Client->>API: POST /v1.2/auth/sign-in (credential)
    API->>Client: { access_token, refresh_token }

2. Sign In

sequenceDiagram
    participant Client
    participant API
    participant Browser
    
    Client->>API: GET /v1.2/auth/sign-in
    API->>Client: credentialRequestOptions
    Client->>Browser: navigator.credentials.get()
    Browser->>Client: credential
    Client->>API: POST /v1.2/auth/sign-in (credential)
    API->>Client: { access_token, refresh_token }

Error Handling

Common Errors

// 1. Unauthorized Domain
if (error.message === "unable to extract rpId from request hostname") {
    // The domain is not configured in the API
    console.error("Unauthorized domain. Contact the IBEx team.");
}
 
// 2. Unrecognized Passkey
if (error.message === "Unexpected RP ID hash") {
    // The rpId does not match between registration and authentication
    console.error("rpId configuration issue.");
}
 
// 3. Expired Token
if (response.status === 401) {
    // Use the refresh token
    const refreshResponse = await fetch(`${API_BASE_URL}/v1.2/auth/refresh`, {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json'
        },
        body: JSON.stringify({
            refresh_token: refreshToken
        })
    });
}

Token Management

class TokenManager {
    constructor() {
        this.accessToken = null;
        this.refreshToken = null;
    }
    
    setTokens(accessToken, refreshToken) {
        this.accessToken = accessToken;
        this.refreshToken = refreshToken;
        localStorage.setItem('access_token', accessToken);
        localStorage.setItem('refresh_token', refreshToken);
    }
    
    async refreshTokens() {
        const response = await fetch(`${API_BASE_URL}/v1.2/auth/refresh`, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json'
            },
            body: JSON.stringify({
                refresh_token: this.refreshToken
            })
        });
        
        if (response.ok) {
            const { access_token, refresh_token } = await response.json();
            this.setTokens(access_token, refresh_token);
            return access_token;
        } else {
            throw new Error('Token refresh failed');
        }
    }
    
    async authenticatedRequest(url, options = {}) {
        const headers = {
            'Authorization': `Bearer ${this.accessToken}`,
            'Content-Type': 'application/json',
            ...options.headers
        };
        
        const response = await fetch(url, { ...options, headers });
        
        if (response.status === 401) {
            // Expired token, attempt to refresh it
            await this.refreshTokens();
            headers.Authorization = `Bearer ${this.accessToken}`;
            return fetch(url, { ...options, headers });
        }
        
        return response;
    }
}

Best Practices

1. Configuration

// Centralized configuration
const CONFIG = {
    development: {
        apiUrl: 'https://passkeys-localhost.ibexwallet.org',
        clientUrl: 'http://ibexwallet.org.localhost:3004'
    },
    staging: {
        apiUrl: 'https://passkeys-staging.ibexwallet.org',
        clientUrl: 'https://ibexwallet.org'
    },
    production: {
        apiUrl: 'https://passkeys.ibexwallet.org',
        clientUrl: 'https://ibexwallet.org'
    }
};
 
const currentConfig = CONFIG[process.env.NODE_ENV || 'development'];

2. Error Handling

class IBExAPIError extends Error {
    constructor(message, status, data) {
        super(message);
        this.name = 'IBExAPIError';
        this.status = status;
        this.data = data;
    }
}
 
async function handleAPIResponse(response) {
    if (!response.ok) {
        const errorData = await response.json().catch(() => ({}));
        throw new IBExAPIError(
            errorData.message || 'API request failed',
            response.status,
            errorData
        );
    }
    return response.json();
}

3. Domain Validation

function validateDomain(domain) {
    // Check that the domain is authorized
    const allowedDomains = [
        'ibexwallet.org.localhost',
        'ibexwallet.org'
    ];
    
    if (!allowedDomains.includes(domain)) {
        throw new Error(`Domain ${domain} is not authorized`);
    }
    
    return domain;
}

4. Tests

// Connectivity test
async function testConnection() {
    try {
        const response = await fetch(`${API_BASE_URL}/health`);
        if (response.ok) {
            console.log('✅ API connection successful');
            return true;
        }
    } catch (error) {
        console.error('❌ API connection failed:', error);
        return false;
    }
}
 
// Passkey test
async function testPasskeyFlow() {
    try {
        // Registration test
        const signUpResult = await registerPasskey();
        console.log('✅ Passkey registration successful');
        
        // Authentication test
        const signInResult = await authenticatePasskey();
        console.log('✅ Passkey authentication successful');
        
        return true;
    } catch (error) {
        console.error('❌ Passkey flow failed:', error);
        return false;
    }
}

Support

For any questions or integration issues:

  1. Verify that your domain is registered in the API
  2. Check the API logs for detailed errors
  3. Contact the IBEx team with the error details

Note: This documentation is based on the staging environment. For production, replace the URLs with production URLs.