Guides

Multi-tenant OAuth architecture: token storage, isolation, and key rotation

How to architect OAuth token storage for many tenants: pool vs. silo vs. bridge isolation, token record schema, envelope encryption, RFC 9700.

Garrett Scott
,
Head of Marketing

Multi-tenant OAuth architecture: token storage, isolation, and key rotation

Paragon runs SOC 2 Type II–audited, per-tenant-isolated OAuth infrastructure for third-party integrations — the layer underneath how multi-tenant auth fits together end to end. Building it yourself means three architecture decisions: a spec-compliant authorization flow, an isolation model that holds a real boundary between tenants under load, and an encrypted storage layer that survives a key rotation without a maintenance window — get any one wrong and the other two don't save you. A spec-correct flow on top of a leaky isolation model is still a breach waiting to happen; a well-isolated store holding unencrypted tokens is a compromised Redis instance away from the same outcome. This page works through all three in the order a build decision needs them: the flow and the spec that governs it (RFC 9700), the isolation model that decides how far one tenant's compromise reaches, the token record schema that makes that isolation model enforceable in a database, and the encryption mechanics — envelope encryption, per-tenant keys, key rotation — that keep a compromised store from also being a readable one.

How do you build and manage OAuth flows for many third-party SaaS integrations?

Paragon's answer is SOC 2 Type II–audited, per-tenant-isolated token storage underneath a spec-compliant OAuth flow — the same three-layer model most integration platforms in this category converge on, whether or not they publish the mechanics. The rest of this page works through each layer: what RFC 9700 actually requires for the flow; the pool, silo, and bridge isolation models and where each breaks; the fields a token record needs to hold up that isolation model; and the DEK/KEK mechanics that let you rotate a key without touching a single stored token value.

The flow and the spec: PKCE, RFC 9700, and what "MUST" actually means

The default flow for a multi-tenant integration platform is the authorization code grant with PKCE. RFC 6749 defines the authorization code grant, but it never mentions PKCE — the term doesn't appear anywhere in the document. PKCE is a separate specification, RFC 7636, published just under three years later, in September 2015. A page that cites RFC 6749 for PKCE is citing the wrong document.

RFC 9700 — the OAuth 2.0 Security Best Current Practice, published as BCP 240 in January 2025 — is what actually pins down when PKCE is required. Per §2.1.1, public clients face a hard MUST; confidential clients only get a RECOMMENDED — not a universal mandate either way. Authorization servers themselves must support PKCE regardless of client type. The confidential-client recommendation exists because PKCE also defends against authorization-code injection and misuse, not only the interception attack it was originally designed for.

The implicit grant gets similarly overstated. RFC 9700 §2.1.2 says clients SHOULD NOT use it — a strong discouragement, not a prohibition. "Effectively dead in practice" is accurate; "banned" overstates the normative language.

OAuth 2.1 gets treated as a ratified standard in a lot of copy on this topic. It isn't. As of August 2026, it's still an Internet-Draft — currently draft-ietf-oauth-v2-1-15, dated March 2026 — not an RFC. It folds RFC 6749 and RFC 9700's recommendations into one document (PKCE built into the authorization code grant, implicit and Resource Owner Password Credentials grants dropped), but it's a dated snapshot, re-issued periodically. Treat it as "the OAuth 2.1 direction," not the current standard.

The state parameter's downgrade, and what carries CSRF protection now

Under RFC 6749 §4.1.1, state is RECOMMENDED, not required — but §10.12 separately says the client MUST implement CSRF protection for its redirection URI, and state has historically been the typical mechanism for that. OAuth 2.1 draft-15 §7.9 downgrades state itself to OPTIONAL where PKCE's code_verifier or an OIDC nonce provides equivalent protection — not unconditionally. RFC 9700 is explicit that a client can rely on PKCE alone for this only after confirming the authorization server actually supports it; without that confirmation, state or nonce still has to carry the CSRF defense. Under OAuth 2.1 that obligation moves onto whichever mechanism the client can actually verify is in place.

Skipping or mis-implementing state under the still-widely-deployed RFC 6749 model isn't theoretical. A missing or predictable state value lets an attacker bind their own authorization code or access token into a victim's session — a login-CSRF pattern where the victim ends up authenticated as, or linked to, the attacker's account. RFC 9700 formalizes this as an active attack class in §4.5 (authorization code injection) and §4.7 (CSRF), which is why a spec-compliant flow still treats state as load-bearing today, even under the OAuth 2.1 draft's conditional downgrade.

What happens after the token is issued — how long it lives, when it rotates, what a provider does on reuse detection — is a separate set of mechanics, covered in refresh token rotation and expiry mechanics at scale.

Three isolation models for per-tenant OAuth tokens

Once the flow issues a token, the next decision is how it's stored relative to every other tenant's tokens. There's no single standardized name for this spectrum, but a common way to describe it — grounded in AWS's own multi-tenancy isolation guidance rather than a formal, universal taxonomy — is pool, silo, and bridge.

Pool stores every tenant's tokens in a shared table under a shared key, with tenant boundaries enforced only at the application and row-query level — no cryptographic barrier, just a WHERE tenant_id = ? clause. Silo is the opposite extreme: a fully separate store and a fully separate key per tenant, the strong-isolation end of the spectrum AWS documents for Neptune multi-tenancy. Bridge sits between the two — shared storage infrastructure with a per-tenant key or cryptographic boundary layered on top, so tenants share the database without sharing the thing that decrypts their tokens.


Pool

Silo

Bridge

Isolation strength

Logical only — no cryptographic tenant boundary

Cryptographic and physical — separate store and key per tenant

Cryptographic — shared store, per-tenant key boundary

Operational overhead

Lowest — one store, one key to manage

Highest — store and key count scale linearly with tenant count

Moderate — one store, but key management scales with tenant count

Where it breaks

A single row-level bug or a leaked shared key exposes every tenant at once

Key and vault sprawl hits provider-side limits (request-rate, role-assignment ceilings) as tenant count grows

Silently collapses back into pool if the per-tenant key boundary is enforced only in application code, not at the IAM/encryption-context level

Pool's failure mode is direct: with no cryptographic wall between tenants, one leaked shared key or compromised row-level check exposes every tenant sharing that store. Bridge's failure mode is subtler — it only works if the per-tenant key boundary is enforced by the platform (an IAM policy, a KMS grant, an encryption-context check), not by application code that's supposed to remember which key to use. A bug in that check doesn't announce itself; it quietly turns a bridge deployment back into a pool deployment with extra steps.

The token record: what to store, what never to store

The fields below are a recommended model, not an industry standard — no vendor publishes a literal "OAuth token record schema." This is a synthesis of patterns with first-party sources: AWS Secrets Manager's approach to storing secret values, and OWASP's Secrets Management Cheat Sheet guidance on logging a secret's lifecycle.

  • tenant_id / customer_id — the row-level and cryptographic isolation key, required regardless of which model above you pick.

  • provider_id / integration_id — which third-party OAuth provider and connection type this record belongs to.

  • provider_account_id — the provider's own identifier for the connected account, distinct from provider_id. Without it, a reconnected account or a support ticket referencing "the Salesforce connection" can't be matched back to the same upstream account a refresh error is failing against.

  • encrypted_access_token — ciphertext only, never plaintext, mirroring how Secrets Manager stores a secret value: never the plaintext, only the encrypted form plus a reference to the key that wrapped it.

  • encrypted_refresh_token — the same ciphertext-only treatment, handled at least as strictly given how much longer it typically lives.

  • wrapped_dek — the record's DEK, itself encrypted by the tenant's KEK, stored beside the ciphertext it protects. Without it there's nothing to unwrap on the next read — the plaintext DEK is discarded from memory right after encryption.

  • encryption_context — the tenant and record identifiers bound directly into the KMS wrap/unwrap call, mirroring Secrets Manager's practice of binding SecretARN/SecretVersionId to every GenerateDataKey/Decrypt call. Without it, a wrapped DEK copied into the wrong tenant or record context still decrypts for any caller that holds KMS permission; with it, the decrypt call fails unless the same tenant and record context is supplied, instead of depending on an application-layer check a bug can skip.

  • expires_at — needed for refresh logic; low sensitivity, fine to leave unencrypted and queryable, consistent with Secrets Manager leaving rotation metadata unencrypted while encrypting only the secret value.

  • scope — the granted OAuth scope(s); low sensitivity, typically unencrypted and queryable.

  • key_id / key_version — which KEK and version wrapped this record's DEK. This is what makes key rotation possible without re-encrypting every row — more below.

  • refresh_error — the last refresh failure's error code and timestamp. Without it, a revoked token and one that hit a transient provider outage look identical from the outside — meaning either needless re-prompts or a missed revocation until a customer notices.

  • Audit fieldscreated_at, last_rotated_at, last_used_at, and created_by / rotated_by, following OWASP's §2.11 guidance to record when a secret was created, consumed, rotated, or deleted, and by whom.

  • Revocation metadatarevoked_at, revoked_by, and revocation_reason (user-disconnected, tenant-offboarded, provider-force-revoked, admin-initiated). The reason is what determines whether the right response is prompting a reconnect, deleting the row, or alerting the tenant's admin that the provider revoked access on its own.

  • status — active, revoked, or expired; not directly sourced, but a natural complement to the audit trail.

[Image: token record schema — field-by-field diagram]

Encryption mechanics: envelope encryption, per-tenant keys, and crypto-shredding

The key_id/key_version field above is most useful with a specific pattern: envelope encryption, using a key-encryption key (KEK) to wrap a data-encryption key (DEK). Key identifiers work fine with direct encryption too — envelope encryption is the scalable pattern, not the only one where key metadata does anything. NIST's glossary defines a key-wrapping key as a symmetric key used to protect the confidentiality and integrity of other keys — the formal name for what AWS KMS, Azure Key Vault, and Google Cloud KMS all implement. Microsoft states the mechanic cleanly in its own explainer: a KEK that never leaves the key vault encrypts a DEK, so decrypting the DEK requires calling back to the vault, and disabling the KEK cryptographically erases every DEK — and every secret — it ever wrapped.

Applied to token storage, the granularity that scales is a unique DEK per token record, wrapped by a per-tenant KEK — not one DEK shared across a tenant's whole record set. A fresh symmetric DEK encrypts each token locally, outside the KMS boundary, for performance; the tenant's KEK — never leaving the HSM in plaintext — wraps that DEK via an API call, and the plaintext DEK is discarded from memory once used. Reading the token back means calling the KMS to unwrap the DEK, then decrypting locally. The alternative — one DEK reused across every record a tenant owns — saves a little KMS call volume at a much larger blast radius: compromise that single DEK and every token the tenant has is exposed at once, not one record at a time.

AWS's current published guidance on the per-tenant version of this pattern, from its 2025 Architecture Blog post on multi-tenant KMS strategy, is one KMS key per tenant, centrally managed in a dedicated account — not one key per tenant per service. The reasoning: per-key cost and key proliferation multiply badly once tenant count crosses service count, and a per-tenant (not per-tenant-per-service) boundary collapses offboarding to a single key. AWS's post stops at recommending the one-key-per-tenant shape; the consequence worth planning for is that disabling or scheduling deletion of that one key renders every piece of ciphertext it ever wrapped, across every service, permanently unrecoverable.

Key rotation splits into two mechanics depending on what's rotating. AWS KMS automatic key rotation (opt-in per key, annual by default) keeps the key ID stable and the old backing key material available internally — callers never rewrap anything, because AWS transparently decrypts old ciphertext with the retained material. Rewrapping is separate, needed only for a genuinely different key: a new tenant KEK, or an application-managed key-version bump. That's what key_id/key_version exists for: unwrap each affected DEK with the old KEK, re-wrap it with the new one, and update the record's key_id/key_version — on the next read, or via a background sweep. The token ciphertext itself is never touched; only the wrapped DEK and its version pointer change. Old and new key versions coexist in the same table until the sweep catches up.

Microsoft's MSAL distributed token cache — recommended for web apps, web APIs, and multi-tenant daemon services on Redis, SQL Server, or Cosmos DB — is not encrypted at rest by default. Per Microsoft's MSAL.NET token cache serialization documentation, Microsoft's distributed-cache sample sets Encrypt = false; encryption at rest is opt-in, configured separately through ASP.NET Core Data Protection. MSAL's desktop cache adds a second data point: it uses OS-native protection where available, but documents a plain-text fallback mode that stores tokens unencrypted in an ACL-restricted file when encryption at rest fails for environment-related reasons.

If a distributed cache running with Encrypt = false is compromised — a memory dump, or an exposed or misconfigured shared Redis instance — every cached token in that partition comes back as plaintext, not ciphertext requiring a KMS call. No KEK/DEK step stands between the compromise and a readable token, which is exactly the gap AWS's one-key-per-tenant guidance above is designed to close.

What a platform absorbs vs. what you'd still build

OAuth token management — issuing, storing, refreshing, and revoking third-party credentials for many tenants — is the discipline all of the above adds up to, and per-tenant token isolation is close to table stakes among well-funded platforms now, not a standalone differentiator. As of August 2026, Auth0/Okta's Token Vault with Organizations Support (GA May 28, 2026) stores and auto-refreshes third-party OAuth tokens scoped per organization, marketed as architectural rather than policy-based isolation. WorkOS Pipes runs a close parallel: OAuth flow, auto-refresh, and encrypted storage retrievable per user and organization, with a growing provider list. Both matter for build-vs-buy on this problem specifically: isolation by itself is now a shared baseline.

Automatic token refresh sits in the same place — standard across most funded platforms now, so a blanket "most platforms don't auto-refresh" claim is inaccurate. The one sourced exception is Tray: Tray Embedded sends an expiry-warning webhook seven to ten days out and requires the partner to manually re-prompt the end user instead of refreshing automatically — a Tray-specific gap, not a category-wide pattern.

What token management doesn't cover on its own is everything built on top of the credential store — the connector catalog, the actions layer, and managed sync running against those same isolated tokens. That combination, not the isolation primitive alone, is the real build-vs-buy question once the mechanics above are priced out. (Current as of August 2026 — both Auth0/Okta and WorkOS have shipped meaningful changes here within the last two quarters and are worth re-checking before relying on this section.)

How Paragon handles token storage and isolation

Paragon's published position on the storage layer specifically: credentials encrypted at rest in an isolated vault, keys and encrypted values stored separately, Paragon-managed, with per-tenant isolation holding across Paragon's cloud and VPC deployments — table stakes on its own now, per the previous section. What it's paired with is the rest of the managed OAuth layer: connector breadth, the actions layer, and managed sync running against that same isolated credential store, so the isolation decision doesn't have to be made twice.

If you're weighing build vs. buy

Everything in this page — the flow, the isolation model, the token record, the encryption mechanics — is buildable in-house; none of it requires a vendor. What it requires is getting all four pieces right and keeping them right through every key rotation and offboarding event. Paragon's documentation on how third-party credentials are stored covers where the mechanics above land in a running system.

FAQ

What is PKCE and does RFC 9700 require it for every OAuth client? PKCE is MUST for public clients and RECOMMENDED for confidential clients under RFC 9700 §2.1.1 — not a universal MUST for every client type.

Is the state parameter still required under OAuth 2.1? RFC 6749 made state RECOMMENDED; the OAuth 2.1 draft downgrades it further to OPTIONAL, but only where a PKCE code_verifier or an OIDC nonce actually supplies that CSRF protection. RFC 9700 ties this to confirming the authorization server supports PKCE first — absent that confirmation, state or nonce is still required. OAuth 2.1 is still a moving Internet-Draft, so treat this as the current direction, not a settled rule.

What's the difference between pool, silo, and bridge isolation models for multi-tenant OAuth tokens? Pool shares one store and key across tenants with only logical separation; silo gives each tenant a fully separate store and key; bridge shares infrastructure but enforces a per-tenant cryptographic key boundary on top of it.

What fields belong in a multi-tenant OAuth token record? A recommended model, not an industry standard, is tenant and provider identifiers, ciphertext-only access and refresh token fields, a wrapped DEK and encryption context, an expiry timestamp, scope, a key ID and key version, a refresh-error field, audit and revocation fields, and a status field. Paragon's own connected-account credentials are Paragon-managed and encrypted at rest, with keys held apart from the values they protect.

Is Microsoft's MSAL token cache encrypted at rest by default? No. Per Microsoft's own MSAL.NET documentation, the distributed token cache ships with Encrypt = false in its sample configuration — encryption at rest has to be turned on separately, through ASP.NET Core Data Protection; MSAL doesn't apply it automatically.

How do you rotate an encryption key without re-encrypting every stored OAuth token? For an application-managed key change — not routine automatic KMS rotation, which needs no rewrap at all — a key_id/key_version field on each record lets you rotate the key-encryption key and re-wrap the affected data-encryption keys lazily, on next access or via a background sweep, leaving the stored token ciphertext untouched, with no downtime and no bulk re-encryption job.

Implementation checklist

  • Choose an isolation model — pool, silo, or bridge — before designing the token record around it.

  • Design the token record: tenant and provider identifiers, ciphertext-only token fields, wrapped_dek, encryption_context, key_id/key_version, refresh_error, audit and revocation fields, and status.

  • Set up envelope encryption: a KEK that never leaves the KMS/HSM, wrapping a DEK per token record.

  • Plan key rotation and offboarding up front — key_id/key_version makes an application-managed key change lazy (routine automatic KMS rotation needs no rewrap at all); a per-tenant KEK turns offboarding into a crypto-shred instead of a bulk-delete job.

  • Implement the authorization flow against RFC 9700: PKCE MUST for public clients, RECOMMENDED for confidential clients, and the state parameter's narrowing role under the OAuth 2.1 direction.

  • Decide build vs. buy only after pricing out the first four steps — not before.

Related

TABLE OF CONTENTS
    Table of contents will appear here.
Ship native integrations 7x faster with Paragon

Ready to get started?

Join hundreds of SaaS companies that are scaling their integration roadmaps with Paragon

Ready to get started?

Join hundreds of SaaS companies that are scaling their integration roadmaps with Paragon

Ready to get started?

Join hundreds of SaaS companies that are scaling their integration roadmaps with Paragon

Ready to get started?

Join hundreds of SaaS companies that are scaling their integration roadmaps with Paragon