Skip to content

What is OAuth Token Management? The B2B SaaS Guide

OAuth token management for AI agents connecting to Salesforce and HubSpot. Covers PKCE flows, token refresh lifecycles, scopes, concurrency control, and error handling at scale for B2B SaaS.

Sidharth Verma Sidharth Verma · · 20 min read
What is OAuth Token Management? The B2B SaaS Guide

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.

The initial OAuth handshake is the easy part. Everything that happens afterward — keeping tokens fresh, handling concurrent refreshes safely, detecting revocations, securing credentials at rest — is where integrations silently fail. And the stakes are not theoretical. According to the 2025 Verizon DBIR, 22% of breaches began with credential abuse and 16% began with phishing. A staggering 88% of attacks against basic web applications involved the use of stolen credentials. The average cost of a U.S. breach hit $10.22 million according to IBM's 2025 Cost of a Data Breach Report.

If you are building B2B SaaS integrations, your OAuth token management system is either already broken or about to be. This guide covers the full lifecycle, the security stakes, the concurrency traps, and practical architectural patterns for handling OAuth tokens at scale.

What is OAuth Token Management?

OAuth token management is the continuous process of acquiring, securely storing, refreshing, encrypting, and revoking OAuth tokens for customer-connected third-party accounts.

When a user connects their third-party account (like Salesforce or HubSpot) to your application, the OAuth 2.0 framework issues an access token and a refresh token. Managing these tokens at enterprise scale goes well beyond that initial handshake:

  • Token acquisition — handling the authorization code exchange, enforcing Proof Key for Code Exchange (PKCE), and resolving dynamic scopes without exposing tenant identifiers in plaintext URLs
  • Secure storage — encrypting tokens at rest using AES-GCM or equivalent
  • Proactive refresh — renewing access tokens before they expire
  • Concurrency control — preventing race conditions when multiple processes try to refresh the same token
  • Failure handling — detecting revoked tokens, marking accounts for re-authentication, and notifying downstream systems
  • Revocation — cleaning up tokens when a customer disconnects

Most teams ship the first bullet point in a weekend. The remaining five are what keep your integration alive in production for months and years. For a deeper dive into the architectural patterns required, see our guide on how to architect a scalable OAuth token management system.

Connecting AI Agents to Salesforce and HubSpot via OAuth

If you are building an AI agent that reads and writes data in Salesforce or HubSpot - syncing CRM contacts, logging meeting notes, enriching leads, or automating pipeline updates - your first real engineering challenge is OAuth. The agent needs long-lived, automatically refreshed access to your customer's CRM org, and each provider handles authentication differently.

This section walks through the OAuth flows, scopes, token lifecycles, and error patterns you need to get right for Salesforce and HubSpot agent integrations.

Choosing the Right OAuth Flow for Your Agent

Not every OAuth flow fits every agent architecture. The right choice depends on whether your agent acts on behalf of a specific user or as a headless service.

Flow When to Use Salesforce HubSpot
Authorization Code + PKCE Agent acts on behalf of a user who grants consent Recommended for all user-facing apps Only supported OAuth flow
Client Credentials Headless service-to-service, no user context Supported (runs as a pre-configured user) Not supported
JWT Bearer Server-to-server with certificate-based auth Supported (no refresh token returned) Not supported

Authorization Code with PKCE should be used when your app provides a user interface and requires the end user to explicitly authorize the app to act on behalf of that user. This is the right default for most AI agent use cases where you need access to a specific customer's data.

Client Credentials should be used for machine-to-machine integrations where the app acts on its own behalf, not on behalf of any user. For Salesforce, in the OAuth Policy section, you choose a Salesforce user that the integration will "run as," which determines which permissions and data the app can access.

The JWT Bearer Flow allows server-to-server integration by using a certificate to sign a JSON Web Token request. The client application proves its identity by appending a signature to the JWT and specifies the user in a JWT or SAML-format assertion. This flow is ideal for situations where no UI is involved. No refresh token is returned in this flow, so if the access token expires you send a request to generate an access token again.

For most B2B SaaS agents, Authorization Code with PKCE is the right starting point. It gives you delegated user-level access, returns a refresh token for long-lived sessions, and both Salesforce and HubSpot support it.

Step-by-Step: PKCE Authorization Code Flow

PKCE (Proof Key for Code Exchange) prevents authorization code interception attacks. PKCE is an enhancement to the Authorization Code Flow that prevents interception of the authorization code. Here is the full flow for connecting an agent to a customer's CRM:

Step 1: Generate a PKCE code verifier and challenge

import secrets
import hashlib
import base64
 
# Generate a cryptographically random code verifier (43-128 chars)
code_verifier = secrets.token_urlsafe(64)
 
# Create the code challenge using S256
digest = hashlib.sha256(code_verifier.encode('ascii')).digest()
code_challenge = base64.urlsafe_b64encode(digest).rstrip(b'=').decode('ascii')

Store the code_verifier securely in your session state - you will need it during the token exchange. Not storing the original code verifier is a common mistake - it is required for token exchange.

Step 2: Build the authorization URL

For Salesforce:

https://login.salesforce.com/services/oauth2/authorize?
  response_type=code
  &client_id=YOUR_CONSUMER_KEY
  &redirect_uri=https://yourapp.com/callback
  &scope=api refresh_token
  &code_challenge=YOUR_CODE_CHALLENGE
  &code_challenge_method=S256
  &state=RANDOM_CSRF_TOKEN

For HubSpot:

https://app.hubspot.com/oauth/authorize?
  client_id=YOUR_CLIENT_ID
  &redirect_uri=https://yourapp.com/callback
  &scope=crm.objects.contacts.read crm.objects.contacts.write
  &state=RANDOM_CSRF_TOKEN

Note that HubSpot's OAuth flow does not use PKCE - it relies on the client secret during token exchange instead. Salesforce requires code_challenge_method=S256 specifically. Only S256 is supported in Salesforce.

Step 3: Exchange the authorization code for tokens

After the user authorizes your app, the provider redirects to your callback URL with a code parameter. Exchange it immediately - authorization codes expire quickly.

import httpx
 
# Salesforce token exchange
response = httpx.post(
    "https://login.salesforce.com/services/oauth2/token",
    data={
        "grant_type": "authorization_code",
        "code": authorization_code,
        "client_id": SALESFORCE_CONSUMER_KEY,
        "client_secret": SALESFORCE_CONSUMER_SECRET,
        "redirect_uri": "https://yourapp.com/callback",
        "code_verifier": code_verifier,  # PKCE proof
    },
)
token_data = response.json()
# token_data includes: access_token, refresh_token, instance_url, id
# HubSpot token exchange
response = httpx.post(
    "https://api.hubapi.com/oauth/v1/token",
    data={
        "grant_type": "authorization_code",
        "code": authorization_code,
        "client_id": HUBSPOT_CLIENT_ID,
        "client_secret": HUBSPOT_CLIENT_SECRET,
        "redirect_uri": "https://yourapp.com/callback",
    },
)
token_data = response.json()
# token_data includes: access_token, refresh_token, expires_in (1800)

A critical difference: Salesforce Access Tokens typically expire in 2 hours, but this value is not guaranteed to be static. While Salesforce does not include an expires_in parameter, they do have a special token introspection endpoint. You need to call the introspection endpoint at https://login.salesforce.com/services/oauth2/introspect or configure a tokenExpiryDuration override. HubSpot's access token expires after the number of seconds given in the expires_in field of the response, currently 30 minutes.

Also note that Salesforce returns an instance_url in the token response (e.g., https://na1.salesforce.com). All subsequent API calls must use this instance URL, not the generic login.salesforce.com. Failing to store and use the correct instance URL is a common source of 404 errors in Salesforce integrations.

Request only the scopes your agent actually needs. Broad scopes expand the blast radius if tokens are compromised.

Salesforce - Minimal scopes for an agent that reads and writes CRM data:

Scope Purpose
api Access Salesforce REST and SOAP APIs
refresh_token Obtain a refresh token for long-lived access

For most apps, refresh_token, web, and api are sufficient. The api scope grants access to all REST API endpoints that the user's profile allows, so CRM object-level permissions are controlled by the Salesforce user's profile and permission sets rather than OAuth scopes.

HubSpot - Granular scopes for CRM agent operations:

Scope Purpose
crm.objects.contacts.read Read contacts
crm.objects.contacts.write Create/update contacts
crm.objects.companies.read Read companies
crm.objects.companies.write Create/update companies
crm.objects.deals.read Read deals
crm.objects.deals.write Create/update deals

HubSpot's OAuth requires you to set scopes for your app. Each scope provides access to a set of HubSpot API endpoints and allows users to grant your app access to specific tools in their HubSpot account. If your app can work with multiple types of HubSpot accounts, you can use the optional_scope parameter to include any tiered scopes. This way, customers using HubSpot free accounts can still authorize your app, even if they can't access all of its scopes. Your app must check for and handle any scopes that it doesn't get authorized for.

Refresh Token Lifecycle: Salesforce vs. HubSpot

The refresh token lifecycle differs significantly between these two providers, and your agent's token management strategy must account for both.

Behavior Salesforce HubSpot
Access token TTL ~2 hours (not guaranteed, no expires_in returned) 30 minutes (expires_in: 1800)
Refresh token expiry Configurable via Connected App policy (default: valid until revoked) Never expires unless app is uninstalled
Refresh token rotation No (same refresh token remains valid) No (same refresh token remains valid)
expires_in returned? No (use introspection endpoint) Yes
Instance URL required? Yes (returned in token response) No (always api.hubapi.com)

HubSpot refresh tokens don't actually expire. The only exception is when somebody uninstalls an app from HubSpot, at which point HubSpot will expire the refresh token.

For Salesforce, the refresh token policy is set on the Connected App under OAuth Policies. The admin can configure it to expire after a set period of inactivity or remain valid until explicitly revoked. For AI agent use cases, set this to "Refresh token is valid until revoked" to avoid unexpected disconnections.

Here is how refresh logic should work for both providers:

from datetime import datetime, timedelta
 
def get_valid_token(account):
    token = account.oauth_token
    buffer_seconds = 30  # safety margin for in-flight requests
 
    if account.provider == "salesforce":
        # Salesforce doesn't return expires_in - use stored expiry
        # or assume ~2 hours if introspection isn't configured
        if not token.expires_at or token.expires_at <= datetime.utcnow() + timedelta(seconds=buffer_seconds):
            return refresh_token(account)
    elif account.provider == "hubspot":
        # HubSpot always returns expires_in (1800 seconds)
        if token.expires_at <= datetime.utcnow() + timedelta(seconds=buffer_seconds):
            return refresh_token(account)
 
    return token
 
def refresh_token(account):
    # Acquire a distributed lock per account before refreshing
    # to prevent the thundering herd problem (see section below)
    with distributed_lock(f"token_refresh:{account.id}"):
        # Re-check after acquiring lock - another process may have refreshed
        account.reload()
        if not account.oauth_token.is_expired(buffer_seconds=30):
            return account.oauth_token
 
        response = httpx.post(
            account.token_url,
            data={
                "grant_type": "refresh_token",
                "client_id": account.client_id,
                "client_secret": account.client_secret,
                "refresh_token": account.oauth_token.refresh_token,
            },
        )
        new_token = response.json()
        account.update_token(new_token)  # encrypt and persist
        return new_token

Account-Scoped Auth and Token Rotation

In a B2B SaaS application, you manage OAuth tokens for hundreds or thousands of connected accounts simultaneously. Each customer's Salesforce org or HubSpot portal is a separate "integrated account" with its own token pair, its own refresh cycle, and its own failure state.

The right architecture scopes all token operations to the individual account:

  • One lock per account. Your distributed mutex should be keyed to the integrated account ID, not a global lock. Two refreshes for the same account are serialized. Two refreshes for different accounts run in parallel.
  • One alarm per account. Proactive refresh schedules should be per-account, with randomized jitter so that thousands of accounts don't all refresh at the same wall-clock moment.
  • Isolated failure. If one customer's Salesforce org revokes access, only that account transitions to a needs_reauth state. All other accounts continue operating.

Token rotation adds another layer of complexity. While neither Salesforce nor HubSpot currently rotates refresh tokens on use, other providers (like Xero and Zoom) do. If your agent integrates with multiple CRM platforms, your storage layer must handle atomic token updates - persist the new refresh token before discarding the old one, or risk permanent lockout if a write fails mid-rotation.

Encrypt all stored tokens at the application layer before they reach your database. The minimum set of fields to encrypt:

  • access_token
  • refresh_token
  • client_secret
  • Any provider-specific API keys or session tokens

Use AES-GCM for authenticated encryption. This prevents both unauthorized reads and ciphertext tampering, even if an attacker gains direct database access.

Common OAuth Errors and Remediation

These are the errors you will hit in production when connecting agents to Salesforce and HubSpot. Knowing which are retryable and which are terminal saves you from hammering a provider with doomed requests.

Error Provider Cause Action
invalid_grant Both Refresh token revoked, user changed password, or admin removed app access Terminal. Stop retrying. Mark account as needs_reauth. Notify the customer to reconnect.
refresh_token_reused Varies Two processes used the same one-time refresh token concurrently Terminal for that token. Indicates a race condition - implement distributed locking (see Concurrency section).
INVALID_SESSION_ID Salesforce Access token expired or was revoked Retryable. Refresh the token and retry the request.
The OAuth token used to make this call expired X minutes ago HubSpot Access token past its 30-minute TTL Retryable. Refresh the token and retry. Indicates your proactive refresh isn't running frequently enough.
insufficient_scope HubSpot Requested API endpoint requires a scope your app didn't request Non-retryable. Update your app's scope configuration and have the customer re-authorize.
API_DISABLED_FOR_ORG Salesforce The Salesforce org doesn't have API access enabled (common on some editions) Non-retryable. The customer needs to upgrade their Salesforce edition or enable API access.
HTTP 500/502/503 from token endpoint Both Provider-side outage Retryable with backoff. Schedule a retry in 3-5 minutes. Do not burn the refresh token.

The single most important distinction: invalid_grant means stop. No amount of retrying will fix a revoked refresh token. Your system should immediately transition the account to needs_reauth, fire a webhook event so your application can alert the customer, and halt background sync jobs for that account to avoid burning through API rate limits.

For Salesforce specifically, always check the instance_url you stored during initial authorization. If a customer migrates their Salesforce org to a different instance (or you are using login.salesforce.com instead of the instance-specific URL), API calls will fail with confusing redirect errors.

The Hidden Complexity of Token Lifecycles

The OAuth 2.0 specification provides a framework, but every SaaS provider implements it with their own chaotic flair.

Token lifetimes vary wildly. When issued, Microsoft Entra ID access tokens have a default lifetime assigned as a random value ranging between 60-90 minutes. Salesforce tokens can last much longer. Some providers never return an expires_in field at all, leaving you to guess. For sensitive APIs, some providers set access token expirations as short as 5-15 minutes, while general-purpose APIs typically use durations of 30-60 minutes.

Refresh token rotation is a landmine. Many providers issue a new refresh token every time you use the old one. If your application successfully requests a new token but fails to persist the new refresh token — due to a network blip, a process crash, or a database write failure during a network partition — you lose it permanently. The old one is already invalidated. The integration is broken, and the customer must manually re-authenticate from scratch.

Silent revocation happens constantly. A customer changes their password. An admin revokes app access from their security console. A provider rotates signing keys. Your token is now invalid, and nobody told you. You find out when your sync job starts throwing invalid_grant errors at 3 AM.

Here is what that diversity looks like across real providers:

Provider Behavior Example Risk
Short-lived tokens (5-15 min) Google Workspace High refresh frequency, more chances for race conditions
Rotating refresh tokens Xero, Zoom One missed write = permanent lockout
No expires_in returned Salesforce Must use introspection endpoint or hardcode expiry
Long-lived tokens (24h+) Some HRIS platforms Stale tokens persist longer than expected after revocation
Client Credentials (no refresh) ServiceNow (M2M) Must re-acquire a brand new token each time

Concurrency and the "Thundering Herd" Problem

The most common cause of permanent OAuth lockouts in production is the thundering herd problem, where multiple concurrent processes attempt to refresh the same expired token simultaneously.

Picture this: you have a background sync job running every five minutes, a webhook handler, and two user-facing API requests — all running against the same customer's CRM org. The access token expires. All four processes detect the expiry at the same moment and independently race to refresh it.

sequenceDiagram
    participant W1 as Background Worker
    participant W2 as Web Server
    participant DB as Database
    participant API as Provider API
    W1->>DB: Read token (Expired)
    W2->>DB: Read token (Expired)
    W1->>API: POST /oauth/token (Refresh request)
    W2->>API: POST /oauth/token (Refresh request)
    API-->>W1: 200 OK (Returns New Token A)
    API-->>W2: 400 Bad Request (Replay Attack Detected)
    Note over API: Provider revokes ALL tokens<br>due to suspected abuse

When the provider receives two simultaneous requests using the same one-time-use refresh token, its security systems flag the behavior as a replay attack. To protect the end user, the provider revokes the entire grant chain. Both the old token and the newly issued token are destroyed. Your application is locked out.

This is not a theoretical risk. It shows up in real production systems regularly. 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. In OpenAI's Codex, concurrent token refresh attempts cause a race condition where the first refresh succeeds, but subsequent attempts fail with a refresh_token_reused error. In Claude Code, 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.

The Solution: Distributed Mutex Locks

To safely handle OAuth token refreshes at scale, you must serialize refresh operations per account. This requires implementing a distributed lock (a mutex) keyed to the specific integrated account ID.

sequenceDiagram
    participant P1 as Process 1
    participant P2 as Process 2
    participant Lock as Distributed Mutex<br>(per account)
    participant Provider as OAuth Provider

    P1->>Lock: acquire(account_id)
    P2->>Lock: acquire(account_id)
    Lock-->>P1: lock granted
    Lock-->>P2: wait (lock held)
    P1->>Provider: POST /oauth/token (refresh)
    Provider-->>P1: new access_token + refresh_token
    P1->>Lock: release + store result
    Lock-->>P2: return cached result
    Note over P2: Uses same fresh token,<br>no duplicate refresh

When multiple callers attempt to refresh a token, the architecture enforces this pattern:

  1. Acquisition: The first caller acquires the lock, creates an operation promise, and sets a strict timeout (e.g., 30 seconds) to prevent deadlocks.
  2. Awaiting: Subsequent callers check the shared state, see that a refresh is already in progress, and await the same promise rather than triggering a duplicate refresh.
  3. Execution: The first caller executes the HTTP request to the provider, updates the database with the new encrypted tokens, and resolves the promise.
  4. Release: All awaiting callers receive the newly refreshed token simultaneously without making duplicate network requests. The lock is cleared.

This pattern ensures two refreshes for the same account are strictly serialized, while refreshes for different accounts run entirely in parallel.

Proactive vs. On-Demand Token Refresh

Even with a perfect distributed lock, there are two strategies for keeping tokens fresh — and a production system needs both.

On-Demand Refresh

On-demand refresh occurs when an application waits for a token to expire, intercepts the API call, pauses the operation, negotiates a new token, and resumes the original request. Before every API call, check if the token is expired with a buffer:

def get_valid_token(account):
    token = account.oauth_token
    # 30-second buffer prevents in-flight request failures
    if token.expires_at <= now() + timedelta(seconds=30):
        token = refresh_token(account)
    return token

This works, but it injects 500ms to 2000ms of latency directly into the first request after expiry. If that request is a synchronous API call from your user's dashboard, they feel it. If the provider's auth server is degraded, the user's request fails entirely.

Proactive (Background) Refresh

Proactive refresh is the enterprise standard. Instead of waiting for expiry, the platform schedules a background task to renew the token before it expires. The system reads the expires_in value returned during the initial OAuth handshake and schedules a renewal 60 to 180 seconds before that exact timestamp.

flowchart LR
    A[Token issued<br>expires_at = T] --> B[Schedule refresh<br>at T minus 60-180s]
    B --> C{Token still valid?}
    C -->|Yes| D[Refresh token<br>via OAuth provider]
    C -->|No, already expired| E[On-demand refresh<br>on next API call]
    D --> F[Store new token<br>+ reschedule alarm]
    D -->|Refresh fails| G[Mark account<br>needs_reauth]
    G --> H[Fire webhook to<br>notify customer]

Adding randomized jitter within that 60-to-180-second window is critical. If 10,000 customers all authenticate at 9:00 AM, a hard 60-minute expiry would cause 10,000 refresh requests to hit your infrastructure at exactly 10:00 AM. Jitter spreads this load evenly, preventing self-inflicted denial-of-service attacks.

The combination of proactive and on-demand refresh gives you defense in depth. The proactive path keeps tokens warm for the vast majority of API calls. The on-demand path catches anything that slips through — a missed alarm, a token revoked between refresh cycles, a brand-new account that hasn't had its first alarm scheduled yet.

Security best practices reinforce this pattern: use the shortest reasonable lifespan for access tokens — often 30 to 60 minutes — to shrink the attack surface if a token is compromised. Proactive refresh makes short-lived tokens feasible at scale without punishing your users with latency.

The Security Stakes: Why Token Management Matters

OAuth tokens are not session cookies. They are bearer credentials that grant direct API access to your customers' most sensitive systems — their CRM data, employee records, financial accounts. They bypass multi-factor authentication and provide programmatic access that attackers prize above almost anything else.

In August 2025, 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. Over a ten-day period, the attackers systematically queried and exported large volumes of records from more than 700 organizations, including Cloudflare, Google, PagerDuty, Palo Alto Networks, Proofpoint, and Zscaler.

The stolen OAuth tokens allowed attackers to access platforms integrated with Salesloft, including Salesforce, Slack, Google Workspace, Amazon S3, Microsoft Azure, and OpenAI.

The Drift breach is a case study in what happens when token lifecycle management fails at a platform level. Treating an access token like a standard string in a database column is a massive liability. A secure token management system must implement defense-in-depth measures:

  1. Encryption at Rest: All sensitive fields — access_token, refresh_token, client_secret, and custom API keys — must be encrypted at the application layer before touching the database. Using AES-GCM ensures authenticated encryption, preventing attackers from tampering with the ciphertext even if they gain direct database access.
  2. Least-Privilege Scopes: Request only the OAuth scopes you actually need. Broad scopes expand the blast radius of any compromise — as the Drift breach demonstrated across Salesforce, Slack, Google Workspace, and more.
  3. Secure Link Tokens: The OAuth authorization flow should never expose internal tenant IDs or environment variables in plain-text query parameters. Applications should generate time-bound, hashed link tokens to initiate the flow, mitigating enumeration and CSRF attacks. See our guide on architecting secure OAuth lifecycles and CSRF protection for implementation details.
  4. Zero Data Retention: When proxying API requests, the token management layer should inject the decrypted token directly into the HTTP headers in memory, ensuring the plaintext token never touches application logs or caching layers.

State Machines and Graceful Failure

Tokens will inevitably fail. Users change their passwords, administrators revoke third-party app access from their IT dashboards, providers experience outages. A resilient OAuth system uses a self-healing state machine to manage these failures gracefully.

When an API request fails, the system must inspect the error response to determine the appropriate transition:

  • Transient errors (HTTP 500, network timeouts during refresh): Leave the account in an active state. Schedule a retry with exponential backoff. Do not burn the refresh token.
  • Terminal auth errors (HTTP 401 with invalid_grant): The refresh token has been permanently revoked. Retrying will not fix it and just wastes cycles while risking rate limits. The state machine must immediately transition the account to needs_reauth.

Once marked as needs_reauth, the system should:

  • Halt all background sync jobs for that specific account to prevent API rate limit penalties.
  • Fire an asynchronous webhook event (e.g., integrated_account:authentication_error) to your application.
  • Surface an alert in your UI prompting the specific user to re-authenticate.

If the user completes a fresh OAuth flow, the state machine transitions back to active, fires a reactivated webhook, and background jobs resume where they left off. This auto-healing loop is critical — it keeps integrations healthy without manual intervention and ensures downstream systems always know the current state of every connection.

For a detailed breakdown of handling these specific HTTP errors, see our guide on handling OAuth token refresh failures in production.

Build vs. Buy: Handling OAuth at Scale

Let's be honest about the engineering cost. Building a production-grade OAuth token management system means building:

  1. A distributed lock per connected account to prevent concurrent refresh race conditions
  2. A background scheduling system for proactive token renewal with randomized timing
  3. AES-GCM encryption at rest for all stored tokens, with key rotation support
  4. A state machine for account lifecycle (activeneeds_reauthactive) with idempotent transitions
  5. Webhook infrastructure to notify your application when accounts need re-authentication
  6. Provider-specific handling for the dozens of OAuth quirks across different APIs — custom scope separators, non-standard refresh parameters, providers that never return expires_in

That is not a week of work. It is several engineering-months, and it requires ongoing maintenance as providers change their OAuth implementations.

Factor Build In-House Use an Integration Platform
Time to first integration 4-8 weeks Days
Concurrency handling You build the distributed lock Handled for you
Provider quirks You discover each one manually Already catalogued across 100+ providers
Token encryption You implement and audit Pre-built with SOC 2 compliance
Ongoing maintenance Your team absorbs every API change Platform absorbs it
Control and customization Full control Depends on platform flexibility
Vendor dependency None You depend on the platform

There is no universally right answer. If you are integrating with one or two providers and have a strong platform team, building in-house can make sense. If you are connecting to ten or more SaaS platforms and your team is shipping product features rather than infrastructure, the math changes fast.

A mature unified API platform handles the entire token lifecycle natively — distributed locks to prevent race conditions, background schedulers to eliminate API latency, AES-GCM encryption to secure credentials, and automatic re-auth detection with webhook notifications — all exposed through a clean, normalized API. Using any third-party platform introduces a dependency, but for most teams the alternative is building and maintaining a distributed systems project that has nothing to do with their core product.

What to Do Next

If you are evaluating your token management posture, here is a practical checklist:

  • Audit your token storage. Are access tokens and refresh tokens encrypted at rest? Would a database breach expose them in plaintext?
  • Check for race conditions. Do you have a lock mechanism preventing concurrent refreshes for the same account? Run a load test.
  • Implement proactive refresh. Waiting for tokens to expire mid-request is an avoidable failure mode.
  • Classify your error handling. Distinguish retryable errors (5xx) from terminal errors (invalid_grant). Stop retrying dead tokens.
  • Set up auth health monitoring. Track the percentage of connected accounts in a needs_reauth state. Alert if it spikes.
  • Review your scopes. The Drift breach demonstrated that over-permissioned OAuth tokens dramatically expand the blast radius of any compromise.

OAuth token management is the kind of infrastructure that is invisible when it works and catastrophic when it does not. Whether you build it yourself or use a platform like Truto, the underlying principles are the same: encrypt everything, refresh proactively, lock against concurrency, and fail loudly when tokens die.

FAQ

How do I connect an AI agent to read and write data in Salesforce?
Use the OAuth 2.0 Authorization Code flow with PKCE. Create a Connected App in Salesforce, request the 'api' and 'refresh_token' scopes, and exchange the authorization code for tokens. Store the instance_url from the token response - all API calls must use it. Salesforce access tokens last about 2 hours but don't return an expires_in field, so use the introspection endpoint or configure a fixed expiry duration.
How do I authenticate an AI agent with HubSpot's API?
HubSpot only supports the Authorization Code OAuth flow (no PKCE). Create a public app in HubSpot's developer dashboard, request granular CRM scopes like crm.objects.contacts.read and crm.objects.contacts.write, and exchange the code for tokens at api.hubapi.com/oauth/v1/token. Access tokens expire every 30 minutes, but refresh tokens never expire unless the app is uninstalled.
What is OAuth token management?
OAuth token management is the continuous process of acquiring, securely storing, refreshing, encrypting, and revoking OAuth tokens for customer-connected third-party accounts. It goes beyond the initial OAuth handshake to include proactive refresh scheduling, concurrency control with distributed locks, failure detection, and encrypted storage.
How do you prevent race conditions when refreshing OAuth tokens?
Use a distributed mutex lock keyed to each integrated account ID. The first process to detect an expired token acquires the lock, refreshes the token, and stores the result. Concurrent processes wait for the in-progress refresh and receive the same new token without making duplicate requests to the provider.
What does the invalid_grant OAuth error mean and how should I handle it?
The invalid_grant error means the refresh token has been permanently revoked - typically because the user changed their password, an admin removed app access, or the provider invalidated the token. Stop retrying immediately, mark the account as needs_reauth, fire a webhook to notify your application, and prompt the user to reconnect.

More from our Blog

What is a Unified API?
Engineering

What is a Unified API?

Learn how a unified API normalizes data across SaaS platforms, abstracts away authentication, and accelerates your product's integration roadmap.

Uday Gajavalli Uday Gajavalli · · 14 min read