Guides

Challenges Building an Authentication Layer for Integrations

The real challenges of building auth for your SaaS product's native integrations: OAuth variability, multi-tenant scale, and OIDC confusion.

Garrett Scott
,
Head of Marketing

Challenges Building an Authentication Layer for Integrations

Paragon Auth already provides fully managed authentication across hundreds of integrations, and for any integration you build yourself through the Custom Integration Builder, so your team doesn't have to solve any of what follows from scratch.

Building native product integrations with popular apps such as Salesforce, Slack, or Jira into an application always seems simple at the beginning. All you'd need to do is build an authentication mechanism, make a few API calls, and deploy it to your customers, right? Unfortunately, many engineering teams spend weeks stuck just at step 1, authentication.

During the process of building that authentication infrastructure, the one that now provides the hundreds of integrations and Custom Integration Builder access mentioned above, we uncovered and overcame challenges that even our own team could never have anticipated when we set out to build Paragon years ago.

We're sharing those learnings here in case you still want to build your product's native integrations in-house, even though Paragon solves this problem out-of-the-box and at scale for your team.

How do I authenticate to my users' integrations?

Paragon is the layer most teams reach for once they've hit a few of the problems below: fully managed authentication across hundreds of integrations, plus anything you build yourself through the Custom Integration Builder, so your team owns the product experience instead of the OAuth edge cases underneath it. Authenticating to your users' integrations means running, for every third-party app you support, an authorization flow, a credential store, and a refresh loop that has to survive years of provider changes without breaking a single customer's connection. The rest of this article is a field guide to what's actually hard about that, and why.

Auth <> Security

We can't talk about authentication without discussing security.

When handling auth for native integrations, whether it be through API Keys, OAuth, or even username and passwords (in rare cases), you'll have to store multiple credentials for each of your users. Depending on your integration use case, you may be storing credentials/tokens that give you access to sensitive data in their accounting systems, to private Slack/Teams messages, to employee data in their HR platform — you get the idea.

Your auth services need to hold up under real security scrutiny — tokens function like passwords in most cases, and should be protected the same way.

Sure, it's easy enough to store these credential values as plaintext in some database table that you can grab whenever you need to make a request on behalf of your users. But that approach leaves you exposed to data breaches that can leak access across dozens of your customers' other connected apps.

As an example, back in April 2022, Heroku was compromised and their users' GitHub integration OAuth access tokens were stolen, causing several private GitHub repositories to be breached and cloned. Even npm had their data harvested, an event that was immediately covered by dozens of publications, including Forbes.

Paragon's Approach to Securely Storing Credentials

At Paragon, we wanted to ensure that even in the worst-case scenario in which a database is compromised, the attacker cannot obtain decrypted credentials.

1. Encryption & Storage

We ensure that your customers' integration credentials are symmetrically encrypted before they are stored.

Encryption keys are stored independently in a separate database, and whenever we need to access the decrypted credentials to make an API call, our Workflow service will fetch the encrypted value and the associated encryption key and decrypt it locally in memory.

2. Penetration tests

We regularly pen test our infrastructure to ensure that it's equipped to prevent attackers from getting unauthorized access to both our customers' and their users' credentials.

Challenges with OAuth

While many services do use API Keys to authenticate requests, most of the top SaaS companies use OAuth 2.0 to authorize requests to their API instead.

Generally, implementing OAuth based authorization flows for integrations involves setting up services to handle:

  • The initial authorization request to get the access and refresh tokens

  • Storage of the access and refresh tokens

  • Authenticating requests to the 3rd party API with the access tokens

  • Using the refresh tokens to get new access tokens

Many teams initially think that since OAuth 2.0 is a 'standard', it would be trivial to implement across dozens of integrations.

However, there are many hidden challenges that need to be overcome in order to build an auth layer for your integration roadmap — challenges that even our own team didn't anticipate when we initially set out to build Paragon a few years ago, and spent years of dedicated engineering to solve.

Let's talk about some of those challenges.

An Unstandardized Standard

The main issue with OAuth is that it isn't really a protocol. Rather, it's a skeleton of a protocol and everything is dependent on how the app developer decides to implement it.

Every app has its own interpretation of the OAuth standard (just look at each 3rd party application's API documentation), which introduces significant variations and inconsistencies when it comes to how you need to address it.

Here are just a few of the many examples we've run into:

  • The state parameter in the OAuth authorization request should support URL-encoded values, but developers have reported Twitter stripping characters from it.

  • While you have to specify the scope for most apps, Mailchimp and Notion don't use them.

  • Every app can have very different refresh token policies. Google's classic offline-access refresh tokens don't rotate on each use, but they are neither unlimited nor eternal: apps whose consent screen is still in Testing status get refresh tokens that expire in seven days, and Google caps them at 100 per account per OAuth client ID. But for Salesforce, each refresh token expires on a user-configurable basis and each user gets only five approvals per connected app.

  • Some apps require a Proof of Key for Code Exchange (PKCE), which adds additional requirements and steps in the Authorization Code Flow, while others do not.

The list goes on, but the greatest challenge is handling token refresh.

Complexities with Refreshing Tokens

Under OAuth, access tokens typically have a time-to-live or TTL (the expires_in parameter of a token response) before expiring and becoming invalid.

When it expires, new access tokens can be obtained using the provided refresh token (as shown in the diagram earlier).

To prevent your users' access tokens from expiring (which will break the connection, cause requests to fail, and inconveniently require your user to authenticate again), your authentication service needs to refresh them periodically in the background. But how?

Approach #1: Refreshing before every Request

The easiest implementation is to get a new access token using the refresh token each time when an API call is made.

But as you can imagine, this scales poorly because you would have to double the number of requests your integration services need to make, which can easily lead to rate-limiting and load balancing issues.

Additionally, with integration use cases that don't run jobs in the background (such as user-triggered workflows), longer durations of inactivity can lead to even refresh tokens expiring.

Approach #2: Refreshing Periodically in the Background

So instead of refreshing before every request, we landed on a much more reliable, if more complex, approach. Instead of refreshing tokens before making a request, Paragon runs a background job that refreshed all our users' tokens periodically by sending requests to sample endpoints to determine if an access token was still valid or not.

This approach resolves the two issues we outlined earlier — running into rate limits and tokens expiring because of inactivity.

However, implementing this into our auth infrastructure was significantly more complex than the first approach, as it led to us having to handle many complications and edge cases, including:

  • Differing refresh policies

  • Preventing race conditions

  • Forced De-authorization

  • Ambiguous errors

Differing Refresh Policies

To start, each app you want to integrate with may have implemented the token refresh flow differently.

Some apps let you keep your existing refresh token indefinitely.

  • ie. Google, where a single refresh token keeps working until it is revoked or hits a cap

Some apps rotate your refresh token out from under you on their own schedule.

  • ie. QuickBooks, which rotates in a new refresh token roughly once every 24 hours of use rather than keeping one static

Some apps limit how many refresh tokens you can generate per organization

  • ie. Salesforce allows five approvals per user per connected app — issue a sixth and the oldest is revoked

Some apps have expiring refresh tokens (expiry completely up to the app developer).

  • ie. NetSuite's refresh tokens are one-time-use and expire after two days by default (configurable up to 30 days on the integration record), while Microsoft Entra — which fronts Outlook — sets no fixed expiry at all, only a rolling inactivity window.

Some apps have different Inactivity and Absolute Expirations

  • ie. Jira's rotating refresh tokens expire after 90 days of inactivity (each use resets the clock), while QuickBooks layers a flat five-year absolute maximum on top of rotation — hit it and your users re-auth no matter what

While each of these adhere to the general OAuth standard, you can't reuse the same approach to handle every integration's auth. Not accounting for all the edge cases can lead to many production-level challenges with your integration after going live.

That's why our integrations engineering team had to become OAuth experts in order to build the unified layer for auth that all our customers rely on for their products' native integrations.

On the bright side, this led to Paragon releasing its Custom Integration Builder which enables customers to rely on our authentication service for any native integration, beyond our hundreds of pre-built connectors.

Preventing Race Conditions

If you're able to comprehensively handle the complications with the varying refresh policies, next comes the challenge of preventing race conditions when refreshing tokens. Never fun to deal with when it comes to distributed systems.

Just as one example, if a token is being refreshed, but a concurrent request is made to the 3rd party API, what do you do?

Under rotation schemes with reuse detection, accidentally using a stale token doesn't just fail that one request — the provider can treat the reuse as possible theft and invalidate the whole token family, including the one mid-refresh.

To prevent race conditions, we introduced a token mutex as a locking mechanism.

This means that if a refresh job obtained the token mutex, all requests to that specific 3rd party service would be paused until it completes.

Handling Forced De-authorization

If that wasn't enough, you have to also deal with forced de-authorization from the app's side (which is more common than we expected). For example, we've seen vigilant Salesforce and Google Workspace admins manually revoke several connected apps at once. Since the app is deauthorized, it's not always reliable to depend on the access token TTL to check its validity.

We hinted at this earlier, but since we can't rely on the TTL, we use sample endpoints for each app to test if a token was still valid.

For example, [.inline-code-highlight]GET /rest/api/3/mypreferences/locale[.inline-code-highlight] for all Atlassian applications — if a 200 Authorized response is returned, our service will know that the token is still valid — but otherwise it will use the refresh token to get a new access/refresh token.

Ambiguous Authentication Errors

Finally, debugging auth errors. There are very few services that we've built integrations for where we felt that they provided sufficient explanations as to why an error occurred, and in most cases the 3rd party app's API docs completely lack details on auth errors, or provide very generic and unhelpful resources.

To make our OAuth client reliable and make debugging easier, we needed to be able to identify which errors are recoverable and which are not.

While OAuth outlines standardized errors, which are invalid_request, invalid_client, invalid_grant, unauthorized_client, unsupported_grant_type, and invalid_scope, due to all the different policies listed earlier, it is incredibly difficult to debug, especially across dozens of services.

This led us to creating a repository of error responses such that our auth service can identify which errors are recoverable and which ones aren't, which has taken years to compile and is constantly being updated as changes are made to the 3rd party app's API and authentication flow.

Third-party OAuth flows vary more than the spec suggests

The unstandardized-standard problem above is really about variability: third-party OAuth flows differ not just in scopes and consent-screen quirks, but in how and when providers change the rules on their own schedule. Salesforce's move toward mandatory Refresh Token Rotation, Google's seven-day refresh-token expiry for apps still in "Testing" status, and Microsoft Entra's fixed, non-configurable token lifetimes are three current examples: the flow you built against a provider's documentation six months ago may not be the flow that provider runs today.

That's a different challenge from handling a rotation or an expiry once you already know about it, our deep dive on OAuth token refresh and expiry covers that mechanics layer. This page's point is narrower: the flows themselves differ and change underneath you, often with no changelog entry you'd notice until a customer's connection breaks.

Multi-tenant authentication multiplies every one of these problems

Everything above gets harder once you're authenticating more than one customer to more than one provider. Multi-tenant authentication turns a manageable N×M problem, N customers times M third-party providers, into N×M separately live credentials, each with its own refresh clock, forced-de-authorization risk, and history of ambiguous errors. A race condition that shows up once in a million refreshes for one customer starts showing up daily once you have a few thousand customers on a few dozen providers, and by then it isn't an edge case, it's Tuesday. The failure modes above stop being things you debug occasionally and start being things you monitor continuously, because at that scale something is always mid-refresh, mid-race, or mid-de-authorization. For the architecture that isolates one customer's credentials from another's at that scale, see how to build multi-tenant auth for customer-facing integrations.

Customer-facing integrations raise the stakes on every failure mode above

An internal, system-to-system integration fails quietly: your own team notices and fixes it. Customer-facing integrations don't get that grace period, a broken connection surfaces as a support ticket before your monitoring even catches it. That has two consequences. One OAuth app registration serves every customer on a given provider, not one per customer, so a single provider policy change affects all of them at once, not one at a time. And customers expect visibility into their own connection status, and expect a broken connection to be your product's problem to surface, not something they discover only when a workflow silently stops running, building that status visibility, and the support tooling behind it, is its own project on top of everything else in this article.

OAuth and OIDC solve different problems, and conflating them is a common, costly mistake

OAuth 2.0 and OIDC get treated as interchangeable more often than they should be. They aren't. OAuth is an authorization framework: it tells you what an app can do on a user's behalf. OIDC is, in the specification's own words, "a simple identity layer on top of the OAuth 2.0 protocol," adding the pieces OAuth alone never provides: a signed ID Token asserting who authenticated, a UserInfo endpoint for profile claims, and a standard set of claims every provider returns the same way.

A plain OAuth access token proves what an app can access, not who the person is. Skipping ID-token verification, or assuming a valid access token proves identity, is a specific, common, and avoidable authentication bug. If your integration needs to know who is connecting, not just what they've authorized you to touch, OIDC is the layer that answers that question. OAuth alone was never designed to.

Where this actually breaks

Challenge

Why It's Hard

What Breaks in Practice

What a Managed Layer Absorbs

Every provider implements OAuth differently

The spec leaves scopes, PKCE, and refresh policy open to each provider's own interpretation

Your team maintains a different auth flow per provider instead of one flow reused everywhere

Provider differences normalized behind one integration point

Refresh policies differ and change without notice

Providers change expiry, rotation, and caps on their own schedule, not yours

An integration that worked in staging breaks silently in a customer's account after a provider policy update

Continuous provider-change monitoring, absorbed centrally instead of per integration

Multi-tenant authentication multiplies every failure

One broken refresh path isn't one incident, it's one incident per affected customer

Support tickets scale with customer count instead of staying flat as you grow

Per-tenant credential isolation that contains a failure to the one credential it touches

Customer-facing integrations need visible connection health

A broken connection becomes the customer's problem the moment they notice it, not yours

Customers discover a broken connection before your monitoring does

Centralized status and failure logging your team, and eventually your customer, can check

OAuth gets mistaken for identity

An access token proves access, not identity

Apps trust a token as proof of "who," skip ID-token verification, and inherit an identity gap

A clear boundary between the authorization layer and any identity layer built on top of it

Closing Thoughts

Although auth is just the first step in building any native integration for your application, it is incredibly complex to get right and is the pre-requisite to any of your integrations functioning properly.

That's why it mattered to our customers that Paragon provides fully managed authentication — we took on the burden of auth so your team can focus on challenges unique to your product and business.

To see how Paragon handles auth across hundreds of integrations, book time with our team.

But if you do decide your team should own these challenges, the sections above are a working blueprint for building your own authentication infrastructure for integrations.

FAQ

What does it take to authenticate to my users' integrations? Paragon Auth already covers this: fully managed authentication for hundreds of integrations, plus custom ones you add through the Custom Integration Builder. Doing it yourself means solving secure token storage, provider-specific refresh policies, refresh-time race conditions, forced de-authorization, and ambiguous error handling, per provider, per customer, before your first customer even connects.

Why is authenticating to customer-facing integrations harder than internal, system-to-system auth? An internal integration failing is something your own team quietly notices and fixes. A customer-facing one fails in front of the customer first, and it turns into a support conversation before it ever reaches your error logs. It also means one OAuth app registration per provider serves every customer on that provider, so a single policy change from the provider affects all of them at once.

What's the difference between OAuth and OIDC, and why does it matter for integrations? OAuth authorizes what an app can access; it was never designed to tell you who the user is. OIDC adds an identity layer on top, an ID Token, a UserInfo endpoint, and standard claims, specifically to answer that question. Treating a valid OAuth access token as proof of identity, without verifying an ID token, is a common and avoidable mistake.

Why does multi-tenant authentication get harder as you add customers? Multi-tenant authentication turns one integration definition into a separate live credential per customer per provider, an N×M problem that grows faster than your customer count alone suggests. Paragon isolates each of those credentials per user, so a failure or a compromise in one doesn't touch another's, which is the piece most teams underestimate until they've already scaled past it.

Why do third-party OAuth flows vary so much between providers? Because OAuth 2.0 is a framework, not a strict protocol, so scopes, consent-screen behavior, and refresh policy are all left to each provider's own implementation, and providers change those choices on their own schedule, often without warning.

Should you build authentication for your integrations yourself, or use a managed layer? Build it yourself if secure credential storage, per-provider refresh handling, and provider-change monitoring are genuinely core to your product's differentiation. Otherwise, Paragon Auth already covers this end to end, across hundreds of integrations and any custom one you add on top, so your team spends that engineering time on your product instead of OAuth edge cases.

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