We use analytics to understand how our website is used. No personal data is collected.

April 20, 2026 · Piyush Ranjan Mishra

OAuth Integration Patterns: Lessons from Connecting 50 SaaS Platforms

OAuthIntegrationsSaaSArchitecture
One OAuth interface, fifty provider implementations behind it
One OAuth interface, fifty provider implementations behind it

I built a reusable OAuth login library that ended up covering 50 SaaS platforms — Salesforce, HubSpot, Outreach, Salesloft, Gmail, Google Calendar, and more. Once the shared layer existed, adding a new platform took roughly 60% less time than the first handful had. The first five integrations were easy and taught me nothing. Integrations 6 through 50 are where the actual patterns emerged, because that’s when the “every OAuth provider is basically the same” assumption started breaking.

OAuth 2.0 is a spec with a hundred dialects

Every provider implements the core authorization-code flow, but the details diverge constantly:

  • Token refresh behavior — some providers rotate refresh tokens on every use (meaning you must persist the new one immediately or lose access), others issue a long-lived refresh token that never changes. Treating refresh-token rotation as the default assumption, and persisting immediately after every refresh, avoided an entire category of “why did this integration silently stop working three days later” bugs.
  • Scope granularity — Salesforce’s scope model and Google’s scope model don’t map onto each other conceptually, let alone syntactically. A generic “requested scopes” abstraction has to be provider-aware under the hood; you can’t fully normalize this away.
  • State parameter handling — technically part of the spec, inconsistently enforced. Some providers will silently drop or mangle a state value with certain characters. I stopped trusting any provider to round-trip state faithfully and instead store the pending-auth context server-side, keyed by a short opaque token I generate myself.

The abstraction that actually held up

A single OAuthProvider interface with per-provider implementations of exactly four methods:

interface OAuthProvider {
  getAuthorizationUrl(state: string): string;
  exchangeCodeForTokens(code: string): Promise<TokenSet>;
  refreshTokens(refreshToken: string): Promise<TokenSet>;
  revokeTokens(tokens: TokenSet): Promise<void>;
}

Everything provider-specific (endpoint URLs, scope formatting, token response parsing) lives inside the implementation. Everything else — persisting tokens, scheduling refresh before expiry, retrying a failed API call once after a refresh — is shared, generic code that doesn’t know or care which provider it’s talking to. This is the boring, obvious abstraction, and it’s the right one. The temptation is to get clever and find a “universal OAuth client” library that promises to handle every provider generically — in my experience that promise breaks down around provider #10, because the dialects above aren’t edge cases, they’re the norm.

Token storage and refresh: the part nobody demos

The demo-friendly version of OAuth is “redirect, callback, done.” The production version is “now keep this working, silently, for months, across token expiry, revocation, and provider-side policy changes.” What mattered in practice:

  1. Refresh proactively, not reactively. Don’t wait for a 401 to trigger a refresh — track expiry time and refresh a few minutes before it, so the user-facing API call never has to eat a retry round-trip.
  2. Handle revocation gracefully. A user can revoke your app’s access from the provider’s side at any time, with zero notice to you. The refresh call will fail, and your system needs a clear “this connection is dead, prompt for re-auth” state rather than silently retrying a doomed refresh forever.
  3. One failed API call gets exactly one retry after a forced refresh. Not zero (transient token issues are common), not unlimited (you’ll hammer a provider that’s rate-limiting you for a different reason).

The scaling test: what actually breaks past platform #20

Rate limits become a real constraint, not a theoretical one — different providers, different windows, different backoff expectations, and your system needs per-provider rate-limit awareness rather than one global retry policy. And support burden becomes real: users connecting the wrong Google account, revoking access accidentally, or hitting provider-side outages all look identical from your system’s perspective (“the integration is broken”) unless you’re logging enough detail per failure to tell them apart quickly.

If you’re about to build your third OAuth integration

Stop copy-pasting the second one. Extract the interface now, before the sixth integration makes the copy-paste approach actively painful. The four-method interface above is a reasonable starting point — the real work is admitting that “generic OAuth” is a per-provider implementation detail wearing a shared trench coat, not an actual abstraction you get for free.