Consent management and data source toggles
onToggleSource(provider) → Enable/disable connection for specific provideronExpandInfo(provider) → Show "Why do you need this?" explanationonConnectAll() → Initiates OAuth flows for all selected sourcesonContinue() → Validates all connections and navigates to CM04OAuth initiation and token management endpoints
/api/v1/applications/{application_id}/oauth/initPOST /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" }
{
"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"
}
/api/v1/oauth/callbackGET /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
/api/v1/applications/{application_id}/tokens{
"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"
}
Customer toggles on TrueLayer and Xero. Companies House requires no OAuth (public API).
AINA backend generates authorization URLs with state parameters and redirects customer.
Customer redirected to TrueLayer's authorization page in new tab/window.
TrueLayer redirects back to AINA with authorization code in query parameters.
AINA backend makes server-to-server call to exchange authorization code for access token.
Same OAuth flow executed for Xero accounting platform.
No OAuth needed - AINA uses company number to query public API with API key.
UI updates to show all connections successful with data retrieved counts.
Total Duration: ~45-60 seconds (depends on customer login speed at bank)
OAuth coordinator and consent management
// 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}`); }
// 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 }); } } }
State management and secure token storage
oauth_state:{state_parameter}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)
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') }; }
OAuth provider specifications
https://auth.truelayer.com/connecthttps://auth.truelayer.com/connect/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
https://login.xero.com/identity/connect/authorizehttps://identity.xero.com/connect/token// 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
https://api.company-information.service.gov.ukGET /company/{company_number}// 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
| 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) |
All tokens encrypted with AES-256-GCM. Encryption keys managed by AWS KMS with automatic rotation. TLS 1.3 for all data in transit.
Cryptographically secure state parameters prevent cross-site request forgery. State validated on callback and immediately deleted after use.
Every consent action logged with timestamp, IP address, user agent, and scope. Immutable audit logs stored for 7 years for compliance.
Automatic token refresh before expiry. New refresh tokens issued with each refresh. Failed refreshes trigger customer re-authorization notification.
Customers can revoke consent anytime. Revocation immediately invalidates tokens with provider and deletes from database.
Open Banking via TrueLayer is FCA-regulated. Read-only access, 90-day consent, full customer control. Meets PSD2 and GDPR requirements.
OAuth token and consent storage