How to Build an OAuth Integration (Without Rebuilding It Every Time)
Most "how to build an OAuth integration" guides walk you through registering an app with Google or Slack, exchanging a code for a token, and calling it done. That's the easy 20%. The other 80% — the part that actually determines whether your integration survives contact with real customers — is what happens after the token lands in your database: refresh timing, revoked-grant handling, per-tenant credential isolation, scope creep, and the fact that every provider implements "standard" OAuth slightly differently.
This guide covers both halves. If you're a developer or engineering lead building your first customer-facing OAuth connection, you'll get a correct, current implementation (OAuth 2.1-aligned, PKCE by default). If you're past that point and dealing with your fifth or fifteenth integration, you'll get the architecture and build-vs-buy framework that determines whether this scales or turns into a permanent maintenance tax.
OAuth integration means something different depending on what you're building
"OAuth integration" gets used for at least three distinct things, and conflating them is where a lot of scoping mistakes start.
Login/authentication ("Sign in with Google"). You're using OAuth (usually layered with OpenID Connect) purely to authenticate a user into your own app. You get an ID token, maybe a short-lived access token, and you rarely need long-term API access to the provider afterward.
Customer-facing data integration. A customer connects their Salesforce, QuickBooks, or Slack account to your product so your product can read or write data on their behalf, indefinitely, in the background. This is the hard case: you need long-lived refresh tokens, per-customer credential storage, and a token refresh system that runs correctly at 3 a.m. with nobody watching.
Agent/MCP authorization. Your product exposes actions to AI agents (via MCP or a similar protocol), and the agent needs delegated, scoped access to act on a user's behalf. This is architecturally closer to case two, but the client is non-human, sessions are shorter-lived, and the spec expectations (OAuth 2.1, Protected Resource Metadata) are stricter by default.
This article is primarily about case two, with a section on how case three changes the picture, because it's the one that actually breaks in production.
It also helps to place OAuth in the surrounding landscape, since these terms get blurred constantly:
Term | What it actually is |
|---|---|
Native integration | Your engineering team builds and maintains the specific connection to one provider's API, hosted in your own codebase. |
Embedded integration / embedded iPaaS | A third-party platform provides the connector, auth handling, and workflow logic, surfaced inside your product's UI as if it were yours. |
API integration | Generic term for any code that calls a provider's REST/GraphQL API — could be native or embedded. |
Unified API | A single API/schema that normalizes many providers in one category (e.g., all ATS or CRM systems) into one data model, hiding provider-specific auth and payloads. |
iPaaS | Integration Platform as a Service, typically built for internal ops teams connecting their own internal tools (think Workato, Tray.io). |
Workflow automation | Trigger-action logic connecting apps for a specific business process (e.g., Zapier-style), which may or may not involve OAuth directly. |
OAuth is the authorization layer underneath most of these categories — it's not a category itself, which is why "build an OAuth integration" is really shorthand for "build the auth layer of a data integration."
The flow you should be building in 2026: authorization code + PKCE

OAuth 2.0's original spec (RFC 6749) allowed several grant types. Most of them are now considered legacy or actively discouraged. The IETF's OAuth 2.0 Security Best Current Practice (RFC 9700, published January 2025) and the emerging OAuth 2.1 spec consolidate the guidance into one clear recommendation: use the authorization code grant with PKCE, for every client type — including confidential, server-side clients that could technically hold a secret.
What changed and why it matters if you're implementing today:
PKCE is mandatory, not just for public clients. Originally PKCE (Proof Key for Code Exchange) existed to protect mobile and single-page apps that can't keep a client secret safe. OAuth 2.1 requires it universally, because it also blocks authorization-code injection and interception attacks that affect confidential clients too. Practically: generate a
code_verifier, derive acode_challengewith S256 (plain-text challenges are no longer acceptable), and send the challenge in the authorization request.The implicit grant and Resource Owner Password Credentials grant are gone. If you find a tutorial telling you to request tokens directly from the authorization endpoint via a URL fragment, or to pass a username/password to get a token, it's describing a flow OAuth 2.1 removes outright, not merely discourages.
Redirect URIs require exact string matching. Wildcard or prefix-based redirect URI validation is a known open-redirect vector. Register the literal callback URL and validate it exactly.
Refresh tokens should rotate. Each time a refresh token is used, issue a new one and invalidate the old one, particularly for public clients. This limits the blast radius if a refresh token leaks.
Access tokens should be short-lived. Minutes to low hours, not days. Long-lived access is the refresh token's job, not the access token's.
None of this requires custom cryptography — most OAuth client libraries (Authlib, simple-oauth2, oauth4webapi, provider SDKs) implement PKCE and rotation correctly out of the box. The mistake is skipping PKCE because "we're a confidential server-side client, we don't need it" — that reasoning was valid under OAuth 2.0 and is explicitly outdated under current guidance.
Step-by-step: registering the app and running the flow
The mechanics are consistent across providers even though the screens differ.
Register your application in the provider's developer console. You'll get a client ID and client secret. Every major provider — Google, Microsoft, Salesforce, HubSpot, Slack, Atlassian — has its own console for this, and this is the step that's actually provider-specific.
Configure your OAuth consent screen. App name, logo, support email, privacy policy and terms URLs, and — critically — whether you're requesting scopes the provider classifies as sensitive or restricted. Google, for instance, requires additional app verification for sensitive/restricted Gmail or Drive scopes, and that review can take days to weeks. Budget for this in your launch timeline; it's the single most common reason integration launches slip.
Register your exact redirect URI(s). One per environment (local, staging, production) if the provider allows multiple. No wildcards.
Build the authorization request. Redirect the user to the provider's
/authorizeendpoint withresponse_type=code, yourclient_id,redirect_uri, requestedscope, astateparameter (CSRF protection — verify it on callback), and the PKCEcode_challenge+code_challenge_method=S256.Handle the callback. The provider redirects back with an authorization
code(or an error). Verifystatematches what you sent. Exchange the code at the token endpoint, including yourcode_verifier, client credentials, and redirect URI.Store the tokens — correctly. This is where the real work starts, covered below.
Use the access token, and handle 401s by refreshing. Never assume a token is valid; treat a 401 from the provider's API as "attempt one refresh, then re-authenticate if that fails."
Scope selection deserves its own callout: request the minimum scope that satisfies the feature, not the broadest scope that avoids a second consent screen later. Google's own guidance is explicit that narrower scopes (e.g., drive.file instead of full drive access) reduce both your verification burden and your customers' security exposure. Broad scopes also make enterprise security teams — who increasingly review third-party OAuth grants — more likely to block your app during procurement.
Token lifecycle management: the part that actually breaks

Getting a token is a tutorial. Keeping thousands of customers' tokens valid, secure, and correctly scoped for years is a systems problem. Four components matter:
A token record that encodes more than the token itself. Per connected account, you need: the encrypted access and refresh tokens, expiry timestamp, granted scopes, a status field (active, refresh_failed, reauth_required, revoked), and a concurrency/version marker. The status field matters because "the token expired" and "the customer revoked access in their Salesforce admin panel" require completely different handling — one is routine, the other needs to surface as an action item for the customer.
A per-account lock around refresh. If two requests for the same customer both notice an expired token and both try to refresh simultaneously, most providers will invalidate the first refresh token as soon as the second one succeeds (this is what rotation is for) — and now you have a race condition that silently locks a customer out. A Redis SETNX lock, a Postgres advisory lock, or a per-account actor/queue all solve this; the point is that refresh must be serialized per account, not per request.
Proactive refresh, not reactive. Refreshing only after a 401 means every expired token produces a failed customer-facing action first. Refresh ahead of expiry (with jitter across accounts, so you don't create a thundering herd at a fixed time of day) and reactive refresh becomes a fallback, not the primary path.
Encryption at rest, by default. Access and refresh tokens are equivalent to passwords for whatever scope was granted. Encrypt them in your database (not just at the disk/volume level — application-layer encryption so a database dump alone doesn't leak usable tokens), and treat token tables with the same access controls you'd apply to a credentials store.
Multi-tenant architecture: one app or many?

For a customer-facing integration, you're not authenticating one user against one provider account — you're managing potentially thousands of customers, each with their own provider account, own scopes, own token lifecycle, and own revocation events. Two structural decisions follow from this:
One OAuth app, many connected accounts (usually correct). Register a single OAuth application with the provider, and store a separate token record per customer who authorizes it. This is how virtually every multi-tenant SaaS integration works — it's what "OAuth 2.0 lets each user authorize your app to act on their behalf" means in provider documentation. The provider distinguishes your app from your customers' individual grants; you don't need a new app registration per customer.
Per-customer OAuth apps (rare, usually forced). Some enterprise providers or specific compliance requirements require a distinct registered app per customer tenant (common in certain private/internal integration setups, less common for public multi-tenant SaaS). If a provider forces this, your token table needs an additional foreign key to "which app registration issued this token," because your refresh logic now depends on per-customer client credentials, not just per-customer tokens.
Either way, design the token table so that "add a new provider" doesn't require a schema change — provider-specific quirks (custom expiry behavior, non-standard scope strings, refresh tokens that themselves expire after a fixed window regardless of use, like some Salesforce and Microsoft configurations) belong in a JSON metadata column or a provider-config table, not in new columns bolted on every time you add an integration.
Where teams get this wrong
Treating all providers as spec-compliant twins. OAuth 2.0/2.1 is a framework, not a rigid protocol — providers vary in how long refresh tokens live, whether they expire on inactivity, whether revocation is pushed via webhook or only discoverable on next use, and how they represent scope strings. Code that works against Google's implementation will not automatically work against Microsoft's or Salesforce's without adjustment.
No plan for silent revocation. A customer's admin can revoke your app's access from their identity provider without your product knowing until the next API call fails. If your only response to that failure is a generic error, the customer experiences it as your product breaking, not as their own action. Detect the specific revoked/invalid-grant error and route the customer straight to re-authorization.
Storing tokens unencrypted, or logging them. It's a common incident-review finding that access or refresh tokens ended up in application logs during debugging. Scrub token values in logging middleware before this becomes a security disclosure.
Requesting broad scopes to "future-proof." This increases app-review friction, increases the blast radius of a leaked token, and increases the chance a customer's security team rejects the connection during vendor review.
Refresh logic embedded per-integration instead of centralized. When token refresh, encryption, and status handling are copy-pasted into every new provider integration, the tenth integration inherits every bug the first nine had, at 10x the surface area to patch.
No reauthorization UX. Building the initial consent screen and forgetting to build the "your Salesforce connection needs attention" screen. Customers experience integration health primarily through this second screen, not the first.
A mistake-cost framework: what each failure actually costs you
Failure mode | Immediate symptom | Downstream cost |
|---|---|---|
No PKCE / legacy grant use | Passes QA, fails security review | Blocked enterprise deals, forced rework under time pressure |
Reactive-only refresh | Intermittent failed customer actions | Support tickets misattributed to "our product is broken" |
No per-account refresh lock | Random reauth-required states | Silent customer churn from a class of bug that's hard to reproduce |
Unencrypted token storage | Invisible until an audit or breach | Disclosure obligations, customer trust loss |
Per-integration refresh logic | Slower to ship integration #2 | Linear cost growth instead of amortized cost as integrations scale |
Overly broad scopes | Slower app review, more consent friction | Lower activation/conversion on the connect step |
Build vs. buy: when to write your own OAuth integration layer

Building the OAuth layer described above for one provider is a reasonable multi-day project for a competent engineer. The economics change with scale:
Factor | Build native | Use integration infrastructure (embedded iPaaS / unified API) |
|---|---|---|
1–3 integrations, stable set | Often fine — the fixed cost is manageable | Usually overkill |
10+ integrations, or customer-requested "add app X" is frequent | Refresh/token logic gets rebuilt or generalized repeatedly | Amortizes token management, scope handling, and provider quirks across every connector |
Deep, provider-specific logic (complex object mapping, high API-limit workloads) | You likely need direct control anyway | May still need a native build for that one integration, embedded for the long tail |
Small team, integrations are not the core product | Engineering time diverted from core roadmap | Frees engineers to ship product, not maintain OAuth edge cases |
Compliance requires full custody of token storage | Native gives direct control | Verify the vendor's token handling, encryption, and data residency meet your requirements first |
The realistic pattern for most SaaS companies isn't all-or-nothing. It's: build natively for the two or three integrations that are core to the product's value proposition and justify deep investment, and use embedded integration infrastructure for the long tail of "customers keep asking for app X" connections where the OAuth and maintenance overhead isn't worth dedicated engineering time. This is the gap platforms like viaSocket, Paragon, and Cyclr are built to fill — they hold the OAuth app registrations, token refresh, and provider-specific quirks for a wide connector catalog, and surface the result inside your product's UI, so your team isn't re-solving token rotation for the fifteenth time.
Implementation checklist
[ ] Authorization code grant with PKCE (S256), for every client type
[ ]
stateparameter generated, stored, and verified on callback[ ] Redirect URIs registered as exact strings, one per environment
[ ] Minimum necessary scopes requested; broader scopes gated behind a clear in-product reason
[ ] Access and refresh tokens encrypted at rest, scrubbed from logs
[ ] Token table includes status field distinguishing expired vs. revoked vs. reauth-required
[ ] Refresh serialized per connected account (lock or queue)
[ ] Proactive refresh scheduled ahead of expiry, with jitter
[ ] Reactive refresh as fallback on 401/invalid_grant
[ ] Reauthorization UX built and tested, not just the initial consent screen
[ ] Provider-specific quirks (expiry behavior, scope format, revocation signaling) documented per integration, stored as config, not hardcoded
[ ] App verification/review timeline for sensitive scopes factored into launch planning
OAuth for AI agents and MCP: what's different
If you're exposing your product's actions to AI agents rather than to a customer's browser session, the current MCP authorization specification builds directly on OAuth 2.1: PKCE is required for HTTP-based MCP servers, and clients discover authorization requirements through Protected Resource Metadata rather than hardcoded endpoints and scopes. The practical differences from the browser-based flow above:
Tokens should be short-lived and narrowly scoped by default, since an agent's blast radius from a leaked token is harder to bound than a human's.
Don't hardcode which authorization server or scopes an agent should use — publish and consume metadata so client and server can negotiate this.
Treat every tool response the agent receives as untrusted input, separate from the authorization question — this is an application-security concern layered on top of, not replacing, correct OAuth implementation.
The token-lifecycle problems described earlier (encryption, rotation, per-account locking) apply here too — an agent's session token still needs a home in a token table that behaves correctly at scale.
Frequently asked questions
Is PKCE required for server-side (confidential) OAuth clients, or just mobile and single-page apps?
Under current guidance (RFC 9700, OAuth 2.1), PKCE is required for all client types, including confidential server-side clients. It was originally aimed at public clients but is now recommended universally as defense against authorization-code injection.
How long should access tokens and refresh tokens last?
Access tokens should be short-lived — commonly minutes to a couple of hours. Refresh tokens vary by provider; some are long-lived until revoked, others expire after a fixed inactivity window regardless of use. Design your token table to store the actual expiry your provider returns rather than assuming a fixed duration.
What's the difference between an embedded iPaaS and a unified API for OAuth integrations?
A unified API normalizes data and auth across many providers in one category behind a single schema (useful when you need one consistent data model). An embedded iPaaS surfaces a broader, often cross-category connector catalog directly inside your product's UI, typically including a workflow/automation layer on top of the raw connection. Both handle OAuth on your behalf; they differ in how much of the integration experience (UI, workflow logic, data modeling) they own versus leave to you.
Do I need a separate OAuth app registration for every customer?
Usually no. One registered OAuth application per provider is standard; each customer authorizes that single app and gets their own token record. Per-customer app registrations are the exception, typically forced by specific enterprise or compliance requirements from the provider side.
What happens if a customer revokes access from their provider's admin panel?
Your app won't be notified proactively unless the provider supports revocation webhooks (few do reliably). You'll discover it on the next API call, which returns an invalid_grant or similar error. Detect this specific error class and route the customer to a re-authorization flow rather than a generic failure message.
Should I build OAuth integrations myself or use an integration platform?
It depends on how many providers you need to support and how core each one is to your product. A small number of deeply-integrated, product-critical connections often justify a native build. A long tail of "nice to have" integrations customers keep requesting is usually cheaper, in engineering time and ongoing maintenance, to handle through integration infrastructure that already manages OAuth, token refresh, and provider quirks across many connectors.
Is OAuth 2.1 backward compatible with what I built under OAuth 2.0?
Mostly. If you're already using the authorization code flow with PKCE, changes are minor. If you're using the implicit grant, the password credentials grant, or passing tokens in query strings, those paths are removed rather than deprecated, and need to be rebuilt.
Read More
Related Articles

Webhook Retries and Idempotency: Building Reliable Delivery Into Your Integration Layer
By Abhijeet Singh

Multi-Tenant Integration Architecture: How to Isolate Credentials, Routing, and Delivery Per Customer
By Abhijeet Singh