How to Architect a Scalable OAuth Token Management System for B2B SaaS Integrations
Architect OAuth token management for Brex, AI agents, and B2B SaaS: proactive refresh, per-account mutex locks, encryption, and BYO-OAuth portability.
If you have ever built a third-party integration in a weekend, you know the feeling of triumph when that first 200 OK comes back. You store the access token in your database, maybe save the refresh token alongside it, and push the feature to production.
Three months later, your error logs light up.
Tokens expire mid-sync. Background jobs hit race conditions because two worker threads tried to refresh the same token simultaneously. A customer changes their password, revoking all active sessions, and your application blindly hammers the provider's API with an invalid token for hours before anyone notices. If you are building B2B SaaS integrations, your OAuth token management system is either already broken or about to be.
The initial OAuth handshake is the easy part. The hard part is everything that happens afterward. And the stakes are not theoretical. In 2025, 22% of breaches began with stolen or compromised credentials according to Verizon, the highest of any vector. Breaches involving stolen credentials are costly - averaging $4.8M per incident. The Salesloft Drift breach in August 2025 proved this applies directly to OAuth tokens: Salesloft experienced a supply chain breach through its Drift chatbot integration that impacted more than 700 organizations. Threat actors stole OAuth authentication tokens that allowed them to impersonate the trusted Drift application and gain unauthorized access to customer environments.
Managing OAuth token lifecycles is not a database storage problem. It is a distributed systems problem with real security implications. This guide breaks down exactly how to architect a scalable, concurrent-safe OAuth token management system for enterprise integrations.
The Hidden Complexity of OAuth Token Management in B2B SaaS
OAuth token management is the continuous process of acquiring, storing, refreshing, and revoking OAuth tokens for customer-connected third-party accounts. In B2B SaaS, this means managing tokens for every customer's Salesforce org, HubSpot portal, Workday tenant, and dozens of other platforms - simultaneously.
The OAuth 2.0 spec gives you a framework. Every provider then implements it with their own creative interpretation:
- Token lifetimes vary wildly. HubSpot access tokens expire in 30 minutes. Salesforce tokens last longer but can be revoked at any time by an admin. Some providers do not return an
expires_infield at all, leaving you to guess. - Refresh token rotation is inconsistent. Some providers (like Microsoft Entra ID) issue a new refresh token with every refresh request, invalidating the old one. Others keep the refresh token stable. Many APIs issue a new refresh token with each refresh. In this case, a race condition could lead to the loss of your valid refresh token, making future refreshes impossible.
- Revocation is silent. A customer changes their password, an admin removes your app from their org, or the provider decides to expire refresh tokens after 90 days of inactivity. You find out when your next API call returns a
401.
Many legacy enterprise APIs make things worse by returning 403 Forbidden, 500 Internal Server Error, or even a 200 OK with an error message buried deep inside the JSON payload when a token expires. Writing custom interceptors for 50 different API error formats is an unsustainable engineering burden.
Why Reactive Token Refresh Fails at Scale
Reactive Token Refresh: A pattern where an application waits for an API request to fail with an HTTP 401 Unauthorized error before attempting to use a refresh token to obtain a new access token.
Most engineering teams default to the reactive approach because it feels logical. You use a token until the provider tells you it is invalid. The execution flow looks like this:
- The client sends a request to the third-party API.
- The provider returns a
401 Unauthorizedstatus. - The client intercepts the error, pauses the original request, and sends a
POST /oauth/tokenrequest. - The provider validates the refresh token and returns a new access token.
- The client updates the database and retries the original request.
At low volumes, this works. At scale, it collapses under its own weight.
Latency injection on every expired token. If a standard API request takes 200 milliseconds, a reactive refresh forces the user to wait through a 401 rejection (200ms), a token exchange handshake (500ms), and a subsequent retry (200ms). You have just quadrupled the latency on the critical path. For batch operations processing thousands of records, this pause cascades across every worker that hits the same expired token.
Disrupted long-running operations. Data sync jobs that run for minutes or hours will inevitably encounter token expiration mid-execution. A reactive approach means the job fails partway through, requiring retry logic, idempotency guarantees, and partial-state recovery - all because a token expired predictably.
Partial state corruption. If the network drops exactly after the provider issues the new token but before your database commits the update, your system state is corrupted. The provider has rotated the refresh token, but your application is still holding the old one. The next refresh attempt hits an invalid_grant error. Your application is permanently locked out, and the end user must manually re-authenticate. Look for warning signs like API requests failing with "access token invalid" error messages, or access token refreshes failing with "revoked refresh token" or invalid_grant messages. While refresh tokens can be revoked for many reasons, frequent revocation errors might indicate race conditions.
You are debugging timing, not logic. The nastiest part of reactive refresh is that it works perfectly in development, where you have a single process and low traffic. These issues often appear under load or in production environments where multiple processes are running simultaneously. They're much harder to detect in development or testing environments with single-threaded execution.
The fix is straightforward in principle: do not wait for tokens to expire.
Solving OAuth Token Refresh Concurrency and Race Conditions
Token Refresh Race Condition: A concurrency failure where multiple parallel processes attempt to refresh the same expired OAuth token simultaneously, leading to provider rate limits, token invalidation, or corrupted database state.
The most dangerous architectural flaw in token management is the "thundering herd" problem.
Imagine your application runs a nightly background sync job that pulls 50,000 contact records from a customer's Salesforce instance. To speed up the process, your worker spins up 20 concurrent threads to fetch paginated data. Midway through the sync, the access token expires. All 20 threads hit a 401 Unauthorized error at the exact same millisecond.
Strict OAuth providers enforce refresh token rotation: every time you use a refresh token, the provider issues a brand new one and immediately invalidates the old one. Here is what happens when 20 threads race to refresh:
sequenceDiagram
participant W1 as Worker 1
participant W2 as Worker 2
participant DB as Token Store
participant P as OAuth Provider
W1->>DB: Read token (expired)
W2->>DB: Read token (expired)
W1->>P: POST /token (refresh_token_v1)
W2->>P: POST /token (refresh_token_v1)
P-->>W1: new access_token + refresh_token_v2
P-->>W2: 400 invalid_grant (token already used)
W1->>DB: Store refresh_token_v2
W2->>DB: ❌ Fails - writes stale data or crashesThe provider processes the first request and issues a new token pair. When the second request arrives milliseconds later using the now-invalidated refresh token, the provider's security model assumes the token has been stolen and is being replayed by an attacker. To protect the account, it revokes the entire token family. All active tokens are instantly destroyed. Your sync job crashes, and the customer receives an alarming email from their IT department about a suspected security breach.
This is not a hypothetical edge case. It happens constantly in production systems. A recent GitHub issue against the Claude Code CLI documented the exact failure mode: when multiple CLI processes run concurrently, they race on refreshing the single-use OAuth refresh token. The loser of the race gets a 404 and loses authentication with no automatic recovery. A similar issue hit OpenAI Codex users running parallel sessions, where 18 agents on openai-codex with shared OAuth profile, every ~12h when the access token expires, a burst of agents all try to refresh simultaneously.
How to Prevent Refresh Token Race Conditions
The solution is serialized refresh with request deduplication: ensure only one refresh operation runs per connected account at any given time, and have all other callers wait for the result. You cannot solve this with optimistic locking or simple SQL UPDATE statements. Optimistic locking relies on version numbers and causes massive retry storms when conflicts occur, which will quickly exhaust your API rate limits.
There are several implementation strategies, each with trade-offs:
| Strategy | Best For | Trade-offs |
|---|---|---|
| In-memory mutex | Single-instance apps | Does not work across multiple servers |
| Database advisory locks | Multi-instance, single-region | Adds DB load; lock timeout complexity |
| Redis distributed lock | Multi-instance, multi-region | Requires Redis infrastructure; SETNX expiry tuning |
| Actor / per-tenant serializer | Edge-native or serverless | Platform-specific; higher per-request cost |
The pattern in pseudocode:
async function getValidToken(accountId: string): Promise<string> {
const token = await tokenStore.get(accountId);
// Check with a 30-second safety buffer
if (!isExpiringSoon(token, bufferSeconds: 30)) {
return token.accessToken;
}
// Acquire a lock scoped to this specific account
return await lock.acquire(accountId, async () => {
// Re-read token - another caller may have already refreshed it
const freshToken = await tokenStore.get(accountId);
if (!isExpiringSoon(freshToken, bufferSeconds: 30)) {
return freshToken.accessToken;
}
// Perform the actual refresh
const newToken = await oauthProvider.refresh(freshToken.refreshToken);
await tokenStore.save(accountId, newToken);
return newToken.accessToken;
});
}Two things matter here. First, the lock is scoped per account, not global. Refreshes for different customer accounts should run in parallel. Only refreshes for the same account need serialization. Second, the double-check pattern inside the lock is essential - the first caller refreshes, and subsequent callers that were waiting re-read the already-refreshed token without making a duplicate provider request.
The resolved flow looks like this:
sequenceDiagram
participant W1 as Worker Thread 1
participant W2 as Worker Thread 2
participant Lock as Mutex Lock
participant DB as Database
participant API as Provider API
Note over W1, W2: Distributed Mutex Pattern
W1->>Lock: Acquire Lock (tenant_123)
Lock-->>W1: Lock Granted
W2->>Lock: Acquire Lock (tenant_123)
Lock-->>W2: Promise Pending (Wait)
W1->>API: POST /oauth/token
API-->>W1: 200 OK (New Token Pair)
W1->>DB: Save Encrypted Tokens
W1->>Lock: Release Lock / Resolve Promise
Lock-->>W2: Promise Resolved
W2->>DB: Read Fresh Token
W2->>API: Proceed with API RequestExactly one refresh request hits the provider, completely eliminating the race condition. For a deeper dive into the distributed systems concepts behind this, read our guide on OAuth at Scale: The Architecture of Reliable Token Refreshes.
Proactive Refresh Architecture: Renewing Tokens Before They Expire
Proactive Refresh: An automated background process that schedules and executes a token renewal before the current access token expires, guaranteeing that valid credentials are always available for in-flight requests.
Once you have concurrency under control, the next step is eliminating reactive refreshes entirely. When you complete an OAuth token exchange, the provider returns a JSON payload containing an expires_in field:
{
"access_token": "eyJhbGciOiJIUzI1NiIs...",
"refresh_token": "def50200234a...",
"token_type": "Bearer",
"expires_in": 3600
}Instead of waiting for the token to die, calculate the absolute expires_at timestamp and store it in your database. Then, schedule a background worker or distributed alarm to trigger a refresh before that timestamp.
The architecture involves three components:
flowchart LR
A[Token Acquired<br>via OAuth] --> B[Schedule Alarm<br>60-180s before expiry]
B --> C{Alarm Fires}
C --> D[Acquire Mutex Lock]
D --> E[Refresh Token<br>with Provider]
E --> F{Success?}
F -- Yes --> G[Store New Token<br>Schedule Next Alarm]
F -- No, Retryable --> H[Schedule Retry<br>in 3 hours]
F -- No, Fatal --> I[Mark needs_reauth<br>Notify Customer]Implementing Jitter and Buffer Times
If you onboarded 1,000 enterprise users at 9:00 AM, and all their tokens expire in exactly one hour, a naive cron job will attempt to refresh 1,000 tokens at exactly 9:59 AM. This spikes your infrastructure load and likely triggers abuse filters on the provider's side.
Introduce randomized jitter into your scheduling. If the token expires at 10:00 AM, schedule the refresh alarm to fire at a random interval between 9:57 AM and 9:59 AM. This spreads the network load evenly across your workers.
Distinguish Retryable from Fatal Errors
An HTTP 500 from the provider's token endpoint is transient - schedule a retry. An invalid_grant or HTTP 401 means the refresh token itself is dead. No amount of retrying will fix it. Mark the account as requiring re-authentication and stop the alarm. Ship a webhook to the customer so they know immediately.
On-Demand Safety Buffer
You must also implement a strict buffer time for on-demand checks. Before executing any API request, check the token's expiration timestamp. If the token will expire within the next 30 seconds, treat it as already expired and force a refresh. This safety margin ensures that long-running API requests or large file uploads do not fail mid-flight because the token expired while the payload was in transit.
This two-pronged approach - proactive alarms plus on-demand refresh as a fallback before each API call - means tokens are almost always fresh when a request arrives. The on-demand path only activates if the alarm system fails or if a token was just created and does not yet have a scheduled refresh.
Securing Token Storage: Encryption and Zero Exposure
A stolen refresh token is a persistent backdoor. Unlike access tokens that expire in minutes, refresh tokens can last days or weeks. Unlike access tokens that expire quickly, refresh tokens persist for days or weeks, operating independently of your SSO and MFA controls. They're the bridges attackers walk across to move laterally between your SaaS applications.
The Salesloft Drift breach illustrated this at scale. The actor systematically exported large volumes of data from numerous corporate Salesforce instances. GTIG assesses the primary intent of the threat actor is to harvest credentials. Storing OAuth tokens in plain text is the equivalent of leaving your house keys under the welcome mat.
Token Encryption at Rest
All sensitive fields - including access tokens, refresh tokens, API keys, and client secrets - must be encrypted at rest. Use AES-256-GCM (Advanced Encryption Standard with Galois/Counter Mode), which provides both confidentiality and data authenticity. The GCM algorithm provides data authenticity, integrity and confidentiality and belongs to the class of authenticated encryption with associated data (AEAD) methods. This means it does not just encrypt the data - it also detects tampering.
Generate a cryptographically secure, random 12-byte Initialization Vector (IV) for every single encryption operation, and store the IV alongside the ciphertext.
import { createCipheriv, createDecipheriv, randomBytes } from 'crypto';
function encryptToken(plaintext: string, encryptionKey: Buffer): string {
const iv = randomBytes(12); // 96-bit IV, unique per encryption
const cipher = createCipheriv('aes-256-gcm', encryptionKey, iv);
const encrypted = Buffer.concat([
cipher.update(plaintext, 'utf8'),
cipher.final(),
]);
const tag = cipher.getAuthTag();
// Store IV + tag + ciphertext together
return `${iv.toString('base64')}::${Buffer.concat([tag, encrypted]).toString('base64')}`;
}This must be unique for every encryption operation carried out with a given key. Put another way: never reuse an IV with the same key. The AES-GCM specification recommends that the IV should be 96 bits long. If you reuse an IV, the entire security of GCM collapses.
Securing the OAuth Initiation Flow
The vulnerability surface begins before the user even authorizes your application. When initiating the OAuth flow, many applications pass raw tenant IDs or environment variables in the state parameter of the authorization URL. This exposes internal system identifiers and invites Cross-Site Request Forgery (CSRF) attacks.
Instead of passing raw data, generate a secure, time-bound Link Token. Hash this token using HMAC and store the digest in a fast key-value store with a strict 7-day Time-To-Live (TTL). Pass this opaque identifier in the state parameter. For a comprehensive guide on securing the initial handshake, including PKCE, review our breakdown on Beyond Bearer Tokens: Architecting Secure OAuth Lifecycles & CSRF Protection.
Principle of Least Exposure
Beyond encryption, limit when tokens are ever decrypted:
- List endpoints should mask sensitive fields. When returning a list of connected accounts to your dashboard, show only metadata. Never include raw tokens in API responses.
- Decrypt only at the point of use. The only code path that needs the raw access token is the one making the outgoing API call to the third-party provider.
- Audit access. Log every decryption event. If your decryption rate suddenly spikes, something is wrong.
For server-side applications that store tokens for many users, encrypt them at rest and ensure that your data store is not publicly accessible to the Internet.
Building a Self-Healing Token State Machine
Your connected accounts need a clear state model that handles failure gracefully and recovers automatically when possible.
stateDiagram-v2
[*] --> connecting : OAuth initiated
connecting --> active : Token acquired
active --> active : Proactive refresh succeeds
active --> needs_reauth : Refresh fails (invalid_grant)
needs_reauth --> active : Customer re-authenticates
needs_reauth --> needs_reauth : API calls blockedThe key behaviors:
- Idempotent status transitions. If an account is already marked
needs_reauth, a second failure should not send a duplicate notification to the customer. Check current state before updating. - Automatic reactivation. If a customer re-authenticates (or the provider starts accepting the refresh token again after a transient issue), detect the successful refresh and flip the account back to
activeautomatically. Fire a webhook so the customer's system knows the integration is healthy again. - Webhook notifications for auth failures. When an account enters
needs_reauth, immediately fire a webhook event likeintegrated_account:authentication_error. This lets your customers build alerting and self-service re-authentication flows. Do not force them to check a dashboard manually.
For a detailed breakdown of handling specific refresh failure scenarios, see our post on Handling OAuth Token Refresh Failures in Production.
Case Study: Connecting an AI Agent to Brex Expense Data
Corporate spend platforms are a natural target for AI agents: expense categorization, anomaly detection, month-end close automation, and receipt reconciliation. Brex exposes a wide REST API surface across transactions, expenses, budgets, cards, and payments, and also publishes an official Model Context Protocol server at https://api.brex.com/mcp for MCP-compatible AI clients. If you are building an AI agent that reads Brex expense data - or a B2B SaaS product syncing Brex transactions into an accounting stack - the token lifecycle rules below are the operational contract you have to follow.
This section consolidates every Brex token-lifecycle detail scattered across our other Brex guides into a single authoritative reference.
Brex Token Types and Lifetimes
Brex exposes three distinct authentication artifacts, and mixing them up is the fastest way to break a production integration:
| Token type | Grant | Lifetime | Refresh behavior | Best for |
|---|---|---|---|---|
| User token | Dashboard-generated | Expires after 30 days of inactivity; does not expire when used regularly | No refresh - regenerate manually if expired | Single-tenant tools, admin scripts |
| Partner OAuth token | Authorization Code Grant | Access token expires in 3600 seconds (1 hour) and must be refreshed | Refresh token rotates on every refresh | Multi-tenant B2B SaaS |
| Onboarding partner | Client Credentials Grant, no user associated with API access | Provider-issued, short | Re-issued via client credentials | Partner-only onboarding endpoints |
| MCP session | OAuth via MCP discovery | Session-bound; ends on inactivity | Reconnect to re-authorize | AI agents (Claude, Cursor, ChatGPT connectors) |
Two facts drive every design decision downstream:
- Partner OAuth access tokens live for exactly one hour. Any long-running integration or persistent AI agent must implement refresh well before that window closes.
- User tokens survive as long as you use them. User tokens expire if they are not used to make an API call for 30 days. When used regularly, they will not expire. For always-on agents, a cheap weekly no-op call keeps a user token alive indefinitely.
The API version split matters too: the Transactions API lives at /v2/transactions/card/primary while the Expenses API lives at /v1/expenses/card. Monitor them as independent signals - one path can degrade without the other.
Reconciling Inconsistencies in Brex's Own Docs
Brex's public documentation uses "user token" and "OAuth token" almost interchangeably in places, but they are not the same artifact. The FAQ is explicit: The user token is used to authenticate your own Brex account. The OAuth token is used when building partner applications to authenticate other Brex accounts. If you are building anything multi-tenant, ignore the user-token path entirely and use partner OAuth.
Similarly, the MCP documentation lets you authenticate with either a Brex API token or OAuth, but Brex recommends OAuth for AI clients: Prefer OAuth over API keys. OAuth scopes access to what you authorize, and sessions can be revoked from the Dashboard. For an AI agent that acts on behalf of an end user, OAuth is the only defensible choice.
Scope to Endpoint Mapping
Brex uses fine-grained scopes that map directly to endpoint families. Scopes define which endpoints your app has access to. You will specify your scopes when generating your user token. Requesting the wrong scope produces a 403 insufficient_scope error at call time - not at consent time - so it is easy to miss until an end user hits the broken path.
| Endpoint family | Read scope | Write scope | Notes |
|---|---|---|---|
Transactions (/v2/transactions/*) |
transactions.card.readonly |
- | Card and cash transaction history |
Expenses (/v1/expenses/card) |
expenses.card.readonly |
expenses.card |
Receipt upload, memo edits, category assignments |
Team (/v1/users, /v1/departments) |
users.readonly |
users |
Also needed for virtual card issuance |
Cards (/v2/cards) |
cards.readonly |
cards |
Request the readonly version if you are just listing cards |
| Budgets / Spend Limits | budgets.readonly |
budgets |
/v1/budgets targets spend limits and will soon be deprecated in favor of /v2/spend_limits |
Payments (/v1/transfers, /v1/vendors) |
payments.readonly |
payments |
Idempotency keys required on writes |
Accounting (/v1/accounting/*) |
accounting.readonly |
accounting |
Bookkeepers may access the accounting API |
For an AI agent that reads expense data, the minimum viable scope set is expenses.card.readonly plus transactions.card.readonly. Add users.readonly if the agent needs to attribute spend to specific employees. As a general security practice, you should request the minimum set of scopes required for whatever action the user is performing.
Two edge cases worth designing for up front:
- Insufficient-scope errors surface at call time. Scopes define which API endpoints your integration can access, but permissions are based on the customer's role type. The role type defines which actions a customer can perform on the Brex dashboard and, consequently, the Brex APIs. So even a correctly-scoped token can fail if the underlying user's role forbids the action.
- Incremental scope upgrades. If you later want to add new functionality that requires fetching user information, or make updates to cards, you can request those scopes (users, cards) which will send the user through the authentication flow again and add those scopes to their previously consented scopes.
Refresh Best Practices and Secure Storage for Brex
Brex partner integrations require refresh token rotation. Partner integrations should always rotate the refresh_token. Whenever your integration exchanges a refresh token for a new access token, it should retain both the new access token and the refresh token (which is returned in the same response). If you do not persist the new refresh token, the next refresh will fail with invalid_grant and the customer has to re-authenticate.
Combine that with the concurrency patterns earlier in this guide, and the operational contract for Brex looks like this:
- Schedule refresh at ~55 minutes. Access tokens live for 3600 seconds. Schedule proactive refresh at
expires_at - 300s, with 60-180 seconds of jitter to spread load across many customer accounts. - Serialize refreshes per integrated account. Because Brex refresh tokens rotate on every use, a single-flight mutex per account is mandatory. Two concurrent refreshes will race, one wins the rotation, and the loser corrupts your stored refresh token.
- Persist the new refresh token atomically. In the same transaction that stores the new access token, overwrite the refresh token from the refresh response. Never assume it is stable.
- Encrypt every credential at rest. AES-256-GCM with a unique 96-bit IV per token, as covered earlier. Client secrets belong in the same encrypted store - use environment variables to replace secrets in your source code. Namely, do not expose any client_ids and client_secrets and ensure they are not checked into any version control.
- Keep user tokens warm with continuous usage. For long-lived user tokens driving internal agents, schedule a lightweight call (for example
GET /v2/users/me) every 20-25 days to reset the 30-day inactivity clock. - Serialize AI-agent workloads. Agents with multiple concurrent worker processes are the exact workload that produces refresh-token race conditions. Funnel all Brex calls through a single credential proxy that owns the mutex per account.
- CSRF-protect the authorization redirect. To mitigate cross-site request forgery, partner integrations should always use the optional state parameter when initiating the OAuth 2.0 Authorization Code Grant. The value of your state parameter must be a random string, at least 9 characters long.
Decision Matrix: Which Brex Auth Should You Use?
| Scenario | Recommended auth | Why |
|---|---|---|
| Internal analytics dashboard for one Brex account | User token | No refresh plumbing; 30-day inactivity keeps it alive if used regularly |
| AI agent for your own finance team | Brex MCP + OAuth | MCP handles auth discovery; OAuth grants are revocable per session |
| B2B SaaS syncing many customers' Brex data | Partner OAuth (Auth Code) | Only multi-tenant option; refresh rotation required |
| MCP-compatible AI assistant for customers (Claude, Cursor) | Brex MCP + OAuth | Any employee can authorize via OAuth and access tools based on their Brex capabilities |
| Onboarding-only partner integration | Client Credentials | No user is associated with this API access |
Checklist: Avoid Common Brex Auth Failures
Before shipping a Brex integration, verify each of these:
- Partner OAuth access tokens are refreshed before the 3600-second expiry (target 55 minutes with jitter).
- Every refresh response's new
refresh_tokenis written atomically alongside the new access token. - Refresh calls for the same integrated account are serialized through a mutex - no concurrent refreshes.
- The
stateparameter in the authorization URL is a random string of at least 9 characters and is validated on callback. - Client secrets, access tokens, and refresh tokens are AES-256-GCM encrypted at rest with per-value IVs.
- Scopes requested are the minimum required;
403 insufficient_scoperesponses are handled with actionable UI that names the missing scope. - Role-based permission errors are distinguished from scope errors in your logging.
- User tokens driving long-lived agents have a keep-alive job that fires an inexpensive call at least every 25 days.
- Random idempotency keys are attached to writes on endpoints where erroneous duplicate processing would be especially bad, such as Create transfer and Create card.
- Rate-limit
429responses honor theRetry-Afterheader with exponential backoff. - Both
/v1/expenses/cardand/v2/transactions/card/primaryare monitored independently for latency and error rate. - Webhook events for
authentication_errorandreactivatedare wired into your customer notification system. - For AI-agent deployments, per-account concurrency is capped so a single tenant cannot fan out and self-DoS Brex.
How Truto Automates the Entire OAuth Token Lifecycle
Building distributed locking systems, proactive alarm schedulers, encryption pipelines, and self-healing state machines requires months of dedicated engineering time. Maintaining that infrastructure as you scale to millions of API requests per day requires a dedicated platform team. Everything described above is infrastructure that has nothing to do with your core product.
Truto handles this entire OAuth lifecycle automatically for every connected account across 200+ integrations:
- Zero race conditions. Each integrated account gets strictly serialized refresh—concurrent API calls, sync jobs, and scheduled refreshes funnel through one flight per account while different accounts stay parallel. If a refresh is already in progress, callers await the same operation and share the result without duplicate provider requests. No Redis lock tuning on your side.
- Proactive + on-demand refresh. Scheduled refresh runs 60 to 180 seconds before expiry (randomized to spread load). Every API request also checks token freshness as a fallback. Tokens are valid when your request hits the provider.
- Automatic reauth detection. When a refresh fails with a non-retryable error like
invalid_grant, the account is markedneeds_reauthand a webhook (integrated_account:authentication_error) fires immediately. When the customer re-authenticates and the next refresh succeeds, the account reactivates automatically. - Encryption by default. All tokens, API keys, client secrets, and sensitive credential fields are AES-GCM encrypted at rest with per-value random IVs.
- Zero integration-specific code. There is no
if (provider === 'salesforce')logic in the token management engine. Integration behavior is defined entirely as declarative JSON configuration. The same execution pipeline handles OAuth 2.0 Authorization Code flows, Client Credentials flows, and custom API key injection. When refresh scheduling or locking behavior improves, every single one of 200+ supported integrations benefits instantly.
The honest trade-off: using Truto (or any managed integration platform) means you are delegating credential management to a third party. That is a real trust decision. We address it with SOC 2 Type II compliance, zero-storage architecture options, and the ability to deploy in your own infrastructure. But it is a trade-off worth evaluating explicitly. For more on how to evaluate this, see our post on passing enterprise security reviews with API aggregators.
Avoiding Vendor Lock-In: A Dual-Layer Architecture for API Portability
When you adopt any integration platform, you are trusting a third party with your customers' credentials and data flows. In 2026, as systems rely more heavily on third-party APIs, vendor lock-in has become a strategic risk - not just a technical inconvenience. API vendor lock-in occurs when your system becomes deeply dependent on a single API provider in a way that makes switching difficult, expensive, or risky.
The architectural defense is a dual-layer API pattern that separates your normalized data flows from your raw provider access, while keeping credentials portable across both.
The Dual-Layer Pattern: Unified API + Proxy Escape Hatch
Most integration platforms offer either a unified (normalized) API or a raw proxy. The strongest architecture uses both layers simultaneously, backed by a shared credential store:
flowchart TD
App[Your Application] --> Decision{Need a common<br>data model?}
Decision -- Yes --> Unified[Unified API Layer<br>Normalized schema across providers]
Decision -- No --> Proxy[Proxy API Layer<br>Raw provider-native access]
Unified --> Creds[Shared Credential Store<br>Encrypted tokens + proactive refresh]
Proxy --> Creds
Creds --> Provider[Third-Party Provider APIs<br>Salesforce, HubSpot, Workday, etc.]
style Creds fill:#f0f4ff,stroke:#4a6fa5,stroke-width:2pxThe unified layer handles 80-90% of your integration needs: listing contacts, creating tickets, syncing employees. When you need provider-specific features - a custom Salesforce SOQL query, a HubSpot workflow trigger, or a proprietary endpoint that no common data model covers - you drop down to the proxy layer. Both layers share the same authenticated connection and the same token management pipeline.
This gives you two forms of portability:
- Provider portability. Your core integration logic talks to the unified API. Swapping a customer from HubSpot to Salesforce requires zero code changes on your side because both produce the same normalized response.
- Platform portability. If you ever need to move off your integration platform, the proxy layer means your application already knows how to consume raw provider responses. You are not locked into a proprietary data model with no escape hatch.
Credential Portability: Bring-Your-Own-OAuth
The deepest form of vendor lock-in in integration platforms is credential ownership. If the platform's OAuth app is the one authorized to access your customers' data, leaving that platform means every customer must re-authenticate. For enterprise customers with complex approval chains, that could take weeks.
The solution is Bring-Your-Own-OAuth (BYO-OAuth): register your own OAuth application credentials with the integration platform, so the authorization grant belongs to your app, not the platform's.
sequenceDiagram
participant App as Your Application
participant Platform as Integration Platform
participant Provider as OAuth Provider
Note over App,Platform: One-time setup
App->>Platform: Register your OAuth client_id + client_secret
Platform-->>App: Credentials stored (encrypted)
Note over App,Provider: Customer connects
App->>Platform: Initiate OAuth for customer
Platform->>Provider: Authorization redirect (YOUR client_id)
Provider-->>Platform: Auth code callback
Platform->>Provider: Exchange code for tokens (YOUR credentials)
Provider-->>Platform: Access token + refresh token
Platform-->>App: Connection active
Note over App,Provider: Ongoing API usage
App->>Platform: GET /unified/crm/contacts
Platform->>Platform: Decrypt token, refresh if needed
Platform->>Provider: API call with Bearer token
Provider-->>Platform: Response data
Platform-->>App: Normalized response
Note over App: If you leave the platform
App->>Provider: Tokens still valid (issued to YOUR OAuth app)With BYO-OAuth, the authorization grant is between your OAuth application and the provider. The integration platform manages the token lifecycle - proactive refresh, encryption, concurrency control - but the credentials are portable. If you leave the platform, existing tokens remain valid because they were issued to your OAuth application, not the platform's.
Here is how to register your own OAuth credentials at the environment level in Truto:
// Register your own Salesforce OAuth app for a specific environment
const response = await fetch(
'https://api.truto.one/environment-integration',
{
method: 'PATCH',
headers: {
'Authorization': 'Bearer YOUR_TRUTO_API_TOKEN',
'Content-Type': 'application/json',
},
body: JSON.stringify({
integration_name: 'salesforce',
environment_id: 'env_your_environment_id',
credentials: {
oauth2: {
config: {
client_id: 'YOUR_SALESFORCE_CONNECTED_APP_CLIENT_ID',
client_secret: 'YOUR_SALESFORCE_CONNECTED_APP_SECRET',
scope: 'api refresh_token offline_access',
},
},
},
}),
}
);Once registered, every new OAuth connection for that integration in that environment uses your OAuth app. The platform's credential resolution merges configuration across three levels - platform defaults, environment overrides, and per-account overrides - with the most specific level taking priority. Your environment-level credentials override the platform's defaults without affecting other integrations or environments.
Calling Provider-Native Endpoints via Proxy
The proxy layer gives you raw access to any endpoint the provider exposes, using the same managed credentials as the unified API. No schema transformation, no field mapping - you send what the provider expects and get back exactly what the provider returns.
// Query a custom Salesforce object via SOQL - not covered by any unified model
const salesforceResponse = await fetch(
'https://api.truto.one/proxy/query?' +
new URLSearchParams({
integrated_account_id: 'ia_customer_abc',
q: 'SELECT Id, Risk_Score__c, Compliance_Status__c FROM Custom_Audit__c WHERE Risk_Score__c > 80',
}),
{ headers: { Authorization: 'Bearer YOUR_TRUTO_API_TOKEN' } }
);
const data = await salesforceResponse.json();
// Returns raw Salesforce response - no transformation applied
// {
// "result": {
// "totalSize": 12,
// "done": true,
// "records": [
// { "Id": "a01xx000003GYb1", "Risk_Score__c": 92, "Compliance_Status__c": "Review" }
// ]
// }
// }// Trigger a HubSpot workflow enrollment - provider-specific, no unified equivalent
const hubspotResponse = await fetch(
'https://api.truto.one/proxy/workflows-enrollments?' +
new URLSearchParams({
integrated_account_id: 'ia_customer_xyz',
}),
{
method: 'POST',
headers: {
'Authorization': 'Bearer YOUR_TRUTO_API_TOKEN',
'Content-Type': 'application/json',
},
body: JSON.stringify({
inputs: [{ email: 'prospect@example.com' }],
}),
}
);The proxy handles authentication, token refresh, rate limiting, and pagination identically to the unified layer. Your application gets managed credentials without being constrained to a predefined data model. Start with the proxy to ship fast, then layer unified models on top as common patterns emerge across your integrations.
Operational Runbook: Monitoring, Rotation, and Export
A multi-provider API strategy is only as strong as your operational visibility into token health across every connected account.
Token health monitoring. Subscribe to webhook events that surface credential issues in real time:
// Webhook payload when a token refresh fails permanently
{
"event_type": "integrated_account:authentication_error",
"payload": {
"id": "ia_customer_abc",
"integration_name": "salesforce",
"status": "needs_reauth",
"last_error": "invalid_grant: expired refresh token"
}
}
// Webhook payload when a customer re-authenticates successfully
{
"event_type": "integrated_account:reactivated",
"payload": {
"id": "ia_customer_abc",
"integration_name": "salesforce",
"status": "active"
}
}Build dashboards that track refresh success rates and needs_reauth counts per provider. Spikes in auth failures for a specific provider often signal an upstream issue - a provider-side token policy change, a revoked scope, or an API deprecation - before the provider's own status page reflects it.
Credential rotation. When rotating OAuth client secrets (which most providers now recommend at least annually):
- Generate a new client secret in the provider's developer console.
- Update the environment-level credentials via the integration platform's API.
- Existing refresh tokens typically remain valid - most OAuth providers do not invalidate tokens when the client secret changes.
- New token exchanges and refreshes will use the updated secret automatically.
Portability checklist. Before you commit to any integration platform, verify these exit capabilities:
| Capability | What to verify |
|---|---|
| BYO-OAuth support | Can you register your own OAuth app credentials so authorization grants belong to you? |
| Proxy / raw access | Can you call provider-native endpoints through the platform without schema transformation? |
| Webhook parity | Does the platform expose auth lifecycle events (failures, reactivations) so you can monitor externally? |
| Zero-storage option | Does the platform offer a pass-through mode that does not persist your customer data at rest? |
| Override hierarchy | Can you customize field mappings, query translations, and endpoint routing per-environment or per-account without forking? |
Avoiding vendor lock-in requires you to think over architectural decisions and plan strategically from day one. Lock-in is an architectural problem, not a procurement mistake. The time to verify these capabilities is during evaluation, not during an emergency migration.
What to Build Next
If you are designing your OAuth token management system from scratch, here is the priority order:
- Start with proactive refresh. This eliminates the majority of token-related failures immediately. Even a simple cron job that refreshes tokens 5 minutes before expiry is better than reactive-only.
- Add per-account locking. Use whatever locking primitive your stack already has - database advisory locks, Redis SETNX, or in-memory mutexes for single-instance apps. Upgrade to distributed locks when you scale.
- Encrypt tokens at rest. AES-256-GCM with per-value random IVs. This is non-negotiable after the Salesloft Drift breach demonstrated what happens when OAuth tokens are compromised at scale.
- Build the state machine. Track account health explicitly. Fire webhooks on auth failures so customers can self-serve re-authentication. Auto-reactivate when refreshes succeed again.
- Instrument everything. Track refresh success rates, latency, and failure reasons per provider. You will discover that Provider X randomly returns 500s every Tuesday at 2am, and you will be glad you have the data to prove it.
Or skip the infrastructure work entirely and let Truto handle it.
Pro Tip: Stop treating integration errors as purely technical faults. When an OAuth token fails permanently, it is a customer success issue. Automate the communication layer so your users know exactly which integration needs to be reconnected before their data syncs fall behind.
FAQ
- How do you prevent OAuth token refresh race conditions in distributed systems?
- Use a per-account mutex lock (via Redis distributed locks, database advisory locks, or another single-flight primitive per tenant) to serialize refresh requests. Concurrent callers wait for the in-progress refresh and receive the same result, preventing duplicate provider requests that can invalidate rotating refresh tokens.
- What is proactive token refresh and why is it better than reactive refresh?
- Proactive refresh schedules token renewal 60-180 seconds before expiration using background alarms, so tokens are always fresh when API requests arrive. Reactive refresh waits for a 401 error, adding latency spikes and risking permanent invalid_grant lockouts when refresh tokens rotate during network failures.
- How should OAuth tokens be encrypted at rest?
- Use AES-256-GCM encryption with a unique random 12-byte IV per encryption operation. Store the IV alongside the ciphertext. GCM provides both confidentiality and integrity verification, detecting tampering in addition to preventing unauthorized reads. Never reuse an IV with the same key.
- What happens when an OAuth refresh token is permanently invalidated?
- Mark the connected account as needing re-authentication, stop retry attempts immediately (retrying invalid_grant errors is pointless), and fire a webhook notification so the customer can re-authorize. Automatically reactivate the account if a subsequent re-authentication succeeds.
- How do third-party API providers handle concurrent refresh requests?
- Strict providers enforce refresh token rotation. If they receive multiple concurrent requests using the same refresh token, they process the first and invalidate the old token. Subsequent requests are treated as a replay attack, and the provider may revoke the entire token family, permanently locking out your application.