CM03

Consent & Connection

Journey: Customer Mobile
Duration: ~1 minute
Protocol: OAuth 2.0
Providers: TrueLayer, Xero, Companies House
1

User Interface Layer

Consent management and data source toggles

🔐 Data Source Cards

  • Open Banking (TrueLayer): Toggle switch, 24-month transaction access, FCA-regulated badge
  • Accounting Software (Xero): Toggle switch, P&L and balance sheet access, OAuth security
  • Companies House: Toggle switch, public company info, no login required
  • Each Card Shows: Provider name, data accessed, purpose, security level, expandable "Why?" section

🎛️ Toggle Controls

  • Three-State Toggle: Ready (gray) → Connecting (blue) → Connected (green)
  • Progress Bars: Animated 0-100% fill during connection
  • Status Badges: "Ready to connect", "Connecting...", "✓ Connected"
  • Disable Logic: Must select at least one source to proceed

💬 AI Guidance Message

  • Opening Statement: "To assess your loan, I'll need to connect to your financial data sources"
  • Reassurance: "This is secure, fast, and you maintain full control"
  • Flexibility Note: "You can toggle any source on or off"

📊 Connection Summary

  • Success State: Shows after all connections complete
  • Data Retrieved: "✓ 1,847 transactions, 36 months P&L, Company verified"
  • Next Step: "Continue to Identity Verification" button enabled

🎨 UI Events Captured

  • onToggleSource(provider) → Enable/disable connection for specific provider
  • onExpandInfo(provider) → Show "Why do you need this?" explanation
  • onConnectAll() → Initiates OAuth flows for all selected sources
  • onContinue() → Validates all connections and navigates to CM04
2

API / Backend for Frontend (BFF)

OAuth initiation and token management endpoints

🔌 OAuth Initiation Endpoint

  • Method: POST
  • URL: /api/v1/applications/{application_id}/oauth/init
  • Purpose: Generate OAuth authorization URLs for customer redirect
Request
POST /api/v1/applications/app_20251222_143023_usr123/oauth/init
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Content-Type: application/json

{
  "providers": ["truelayer", "xero", "companies_house"],
  "redirect_uri": "https://app.aina.co.uk/oauth/callback",
  "state": "app_20251222_143023_usr123"
}
Response (200 OK)
{
  "request_id": "req_cm03_20251222_143345",
  "status": "success",
  "oauth_urls": {
    "truelayer": {
      "authorization_url": "https://auth.truelayer.com/connect?...",
      "state": "state_truelayer_12345",
      "expires_in": 600
    },
    "xero": {
      "authorization_url": "https://login.xero.com/identity/connect/authorize?...",
      "state": "state_xero_67890",
      "expires_in": 600
    },
    "companies_house": {
      "direct_lookup": true,
      "company_number": "12345678",
      // No OAuth needed - public API
    }
  },
  "timestamp": "2025-12-22T14:33:45Z"
}

🔄 OAuth Callback Endpoint

  • Method: GET
  • URL: /api/v1/oauth/callback
  • Purpose: Receive authorization codes and exchange for access tokens
Callback URL (from provider)
GET /api/v1/oauth/callback
  ?code=auth_code_abc123xyz
  &state=state_truelayer_12345
  &scope=transactions balance

// Server exchanges code for access token
POST https://auth.truelayer.com/connect/token
{
  "grant_type": "authorization_code",
  "client_id": "{AINA_CLIENT_ID}",
  "client_secret": "{AINA_CLIENT_SECRET}",
  "code": "auth_code_abc123xyz",
  "redirect_uri": "https://app.aina.co.uk/oauth/callback"
}

// Response: access_token, refresh_token, expires_in

💾 Token Storage Endpoint

  • Method: POST
  • URL: /api/v1/applications/{application_id}/tokens
  • Purpose: Securely store encrypted access and refresh tokens
Token Storage
{
  "application_id": "app_20251222_143023_usr123",
  "provider": "truelayer",
  "access_token": "{ENCRYPTED}",
  "refresh_token": "{ENCRYPTED}",
  "expires_at": "2025-12-22T15:33:45Z",
  "scope": "transactions balance",
  "created_at": "2025-12-22T14:33:45Z"
}

🔒 Security Measures

  • State Parameter: CSRF protection - validate state matches on callback
  • Token Encryption: AES-256 encryption for all stored tokens
  • HTTPS Only: All OAuth flows use TLS 1.3
  • Token Rotation: Automatic refresh before expiry

🔐 OAuth 2.0 Authorization Flow (3-Legged)

1

Customer Selects Data Sources

Customer toggles on TrueLayer and Xero. Companies House requires no OAuth (public API).

Action: Customer clicks "Connect Data Sources" button
UI State: Button becomes disabled, status changes to "Connecting..."
2

AINA Initiates OAuth

AINA backend generates authorization URLs with state parameters and redirects customer.

API Call: POST /api/v1/applications/{id}/oauth/init
Response: Authorization URLs for TrueLayer and Xero
Security: Unique state parameter per provider for CSRF protection
3

Redirect to Provider (TrueLayer)

Customer redirected to TrueLayer's authorization page in new tab/window.

URL: https://auth.truelayer.com/connect?client_id=...&redirect_uri=...&state=...
Customer Action: Selects bank, logs in, grants consent
Duration: 15-30 seconds
4

Provider Returns Authorization Code

TrueLayer redirects back to AINA with authorization code in query parameters.

Callback URL: /oauth/callback?code=abc123&state=state_truelayer_12345
Validation: Server validates state parameter matches
Security: Authorization code is single-use and expires in 10 minutes
5

AINA Exchanges Code for Tokens

AINA backend makes server-to-server call to exchange authorization code for access token.

Request: POST to TrueLayer token endpoint with client_secret
Response: access_token, refresh_token, expires_in (3600s)
Storage: Tokens encrypted with AES-256 and stored in PostgreSQL
6

Repeat for Xero

Same OAuth flow executed for Xero accounting platform.

Xero Specifics: Requires tenant_id selection (organization)
Token Expiry: Xero tokens expire in 30 minutes
Refresh Token: Valid for 60 days, auto-renewed
7

Companies House Direct Lookup

No OAuth needed - AINA uses company number to query public API with API key.

API Call: GET /company/12345678
Auth: Basic authentication with API key
Response Time: 400-600ms
8

Connection Summary Displayed

UI updates to show all connections successful with data retrieved counts.

TrueLayer: ✓ 1,847 transactions retrieved
Xero: ✓ 36 months P&L retrieved
Companies House: ✓ Company verified (Active status)
Next: "Continue to Identity Verification" button enabled

Total Duration: ~45-60 seconds (depends on customer login speed at bank)

3

Business Logic & Orchestration

OAuth coordinator and consent management

🔧 OAuth Coordinator Service

  • Multi-Provider Management: Handles OAuth flows for TrueLayer, Xero simultaneously
  • State Management: Generates cryptographically secure state parameters
  • Callback Routing: Routes OAuth callbacks to correct application
  • Token Lifecycle: Manages token expiry, refresh, and revocation
OAuth Coordinator Logic
// OAuth Flow Orchestration

function initiateOAuthFlow(applicationId, providers) {
  const oauthUrls = {};
  
  for (const provider of providers) {
    // 1. Generate unique state parameter
    const state = generateSecureState(applicationId, provider);
    
    // 2. Store state in Redis (10-minute TTL)
    redis.set(`oauth_state:${state}`, {
      applicationId,
      provider,
      createdAt: Date.now()
    }, 600);
    
    // 3. Build authorization URL
    const authUrl = buildAuthorizationUrl(provider, state);
    oauthUrls[provider] = { authUrl, state };
  }
  
  return oauthUrls;
}

function handleOAuthCallback(code, state) {
  // 1. Validate state parameter
  const stateData = redis.get(`oauth_state:${state}`);
  if (!stateData) throw 'Invalid or expired state';
  
  // 2. Exchange authorization code for tokens
  const tokens = await exchangeCodeForTokens(
    stateData.provider,
    code
  );
  
  // 3. Encrypt and store tokens
  await storeEncryptedTokens(
    stateData.applicationId,
    stateData.provider,
    tokens
  );
  
  // 4. Trigger data sync
  await triggerDataSync(stateData.applicationId, stateData.provider);
  
  // 5. Delete state (prevent reuse)
  redis.del(`oauth_state:${state}`);
}

🔐 Consent Management

  • Granular Permissions: Track exactly what data customer consented to
  • Consent Audit Trail: Log timestamp, scope, IP address, user agent
  • Revocation Support: Customer can revoke consent anytime from settings
  • Expiry Tracking: Alert customer when consent nears expiration

♻️ Token Refresh Strategy

  • Proactive Refresh: Refresh tokens 5 minutes before expiry
  • Background Job: Cron job checks expiring tokens every 5 minutes
  • Failure Handling: If refresh fails, notify customer to re-authorize
  • Token Rotation: New refresh token issued with each refresh
Token Refresh Logic
// Background job runs every 5 minutes

async function refreshExpiringTokens() {
  // Find tokens expiring in next 10 minutes
  const expiring = await db.query(`
    SELECT * FROM oauth_tokens
    WHERE expires_at < NOW() + INTERVAL '10 minutes'
    AND refresh_token IS NOT NULL
  `);
  
  for (const token of expiring) {
    try {
      // Call provider's token refresh endpoint
      const newTokens = await refreshToken(
        token.provider,
        token.refresh_token
      );
      
      // Update database with new tokens
      await db.update('oauth_tokens', {
        access_token: encrypt(newTokens.access_token),
        refresh_token: encrypt(newTokens.refresh_token),
        expires_at: Date.now() + newTokens.expires_in,
        updated_at: Date.now()
      });
      
    } catch (error) {
      // Notify customer to re-authorize
      await sendNotification(token.user_id, {
        type: 'REAUTH_REQUIRED',
        provider: token.provider
      });
    }
  }
}

✅ Connection Validation

  • Test API Call: Make test request to verify token works
  • Scope Verification: Ensure granted scopes match requested scopes
  • Data Availability: Confirm minimum data requirements met
  • Quality Check: Validate data format and completeness
4

Integration & Middleware Layer

State management and secure token storage

💾 Redis State Storage

  • Purpose: Store OAuth state parameters temporarily
  • TTL: 10 minutes (state expires if customer doesn't complete flow)
  • Key Format: oauth_state:{state_parameter}
  • Value: JSON with application_id, provider, timestamp
Redis State Structure
KEY: oauth_state:state_truelayer_12345abc
VALUE: {
  "application_id": "app_20251222_143023_usr123",
  "provider": "truelayer",
  "user_id": "usr_olivia_thompson",
  "created_at": 1703258425000,
  "redirect_uri": "https://app.aina.co.uk/oauth/callback"
}
TTL: 600 seconds (10 min)

🔐 Token Encryption Service

  • Algorithm: AES-256-GCM (Galois/Counter Mode)
  • Key Management: AWS KMS for encryption key rotation
  • Encryption-at-Rest: All tokens encrypted before PostgreSQL storage
  • Encryption-in-Transit: TLS 1.3 for all API calls
Token Encryption
function encryptToken(plaintext) {
  // 1. Get encryption key from AWS KMS
  const dataKey = await kms.generateDataKey({
    KeyId: process.env.KMS_KEY_ID
  });
  
  // 2. Generate random IV (initialization vector)
  const iv = crypto.randomBytes(16);
  
  // 3. Encrypt with AES-256-GCM
  const cipher = crypto.createCipheriv(
    'aes-256-gcm',
    dataKey.Plaintext,
    iv
  );
  
  const encrypted = Buffer.concat([
    cipher.update(plaintext, 'utf8'),
    cipher.final()
  ]);
  
  const authTag = cipher.getAuthTag();
  
  // 4. Return encrypted data + metadata
  return {
    ciphertext: encrypted.toString('base64'),
    iv: iv.toString('base64'),
    authTag: authTag.toString('base64'),
    encryptedDataKey: dataKey.CiphertextBlob.toString('base64')
  };
}

🌐 API Gateway Routing

  • OAuth Init: Routes to OAuth Coordinator service
  • OAuth Callback: Validates origin, routes to token exchange handler
  • Token Storage: Routes to secure token vault service
  • Rate Limiting: Max 10 OAuth initiations per minute per user

📊 Connection Monitoring

  • Health Checks: Test OAuth providers every 5 minutes
  • Connection Status: Track success rate per provider
  • Alerting: Notify ops team if success rate drops below 95%
  • Metrics: Average connection time, failure reasons, retry counts
5

External Systems Integration

OAuth provider specifications

🏦
TrueLayer OAuth
  • Authorization URL: https://auth.truelayer.com/connect
  • Token URL: https://auth.truelayer.com/connect/token
  • Scopes: transactions, balance, accounts, info
  • Grant Type: authorization_code
  • Token Expiry: 3600 seconds (1 hour)
  • Refresh Token: Valid for 90 days
  • Response Format: JSON with access_token, refresh_token
  • // Authorization URL Structure
    https://auth.truelayer.com/connect
      ?client_id={AINA_CLIENT_ID}
      &scope=transactions balance
      &redirect_uri={CALLBACK_URL}
      &state={SECURE_STATE}
      &response_type=code
      &enable_mock=false
    📊
    Xero OAuth
  • Authorization URL: https://login.xero.com/identity/connect/authorize
  • Token URL: https://identity.xero.com/connect/token
  • Scopes: accounting.reports.read, accounting.transactions.read
  • Grant Type: authorization_code
  • Token Expiry: 1800 seconds (30 minutes)
  • Refresh Token: Valid for 60 days
  • Additional: Requires tenant_id selection after authorization
  • // Authorization URL Structure
    https://login.xero.com/identity/connect/authorize
      ?client_id={AINA_CLIENT_ID}
      &scope=accounting.reports.read offline_access
      &redirect_uri={CALLBACK_URL}
      &state={SECURE_STATE}
      &response_type=code
    🏢
    Companies House API
  • Base URL: https://api.company-information.service.gov.uk
  • Authentication: Basic Auth with API key (no OAuth)
  • Endpoint: GET /company/{company_number}
  • Rate Limit: 600 requests per 5 minutes
  • Data Access: Public data (no customer consent required)
  • Response Time: 400-600ms average
  • Cost: Free tier (no cost per request)
  • // Direct API Call (No OAuth)
    GET /company/12345678
    Authorization: Basic {base64(api_key:)}
    Accept: application/json
    
    // Response includes:
    // - company_name, company_status
    // - incorporation_date, company_type
    // - registered_office_address, SIC codes

    🔄 OAuth Provider Comparison

    Feature TrueLayer Xero Companies House
    OAuth Required ✅ Yes ✅ Yes ❌ No (API Key)
    Token Expiry 60 minutes 30 minutes N/A
    Refresh Token 90 days 60 days N/A
    Connection Time 20-40 seconds 15-30 seconds 1-2 seconds
    Consent Required Explicit Explicit None (public)

    🛡️ Security & Compliance Features

    🔐

    End-to-End Encryption

    All tokens encrypted with AES-256-GCM. Encryption keys managed by AWS KMS with automatic rotation. TLS 1.3 for all data in transit.

    🛡️

    CSRF Protection

    Cryptographically secure state parameters prevent cross-site request forgery. State validated on callback and immediately deleted after use.

    📋

    Consent Audit Trail

    Every consent action logged with timestamp, IP address, user agent, and scope. Immutable audit logs stored for 7 years for compliance.

    ♻️

    Token Rotation

    Automatic token refresh before expiry. New refresh tokens issued with each refresh. Failed refreshes trigger customer re-authorization notification.

    🚫

    Revocation Support

    Customers can revoke consent anytime. Revocation immediately invalidates tokens with provider and deletes from database.

    FCA Compliance

    Open Banking via TrueLayer is FCA-regulated. Read-only access, 90-day consent, full customer control. Meets PSD2 and GDPR requirements.

    6

    Data Persistence Layer

    OAuth token and consent storage

    💾 OAuth Tokens Table

    • Database: PostgreSQL
    • Table: oauth_tokens
    • Encryption: All tokens encrypted at application layer before storage
    • Indexing: Indexed on application_id and provider for fast lookups
    SQL Schema