tag is parsed. Two tags cover both CORS-tagged and non-CORS fetches. -->What we learned operating auth for 900+ APIs | Nango Blog 11.4k<br>Log In Sign Up<br>Theme
Table of Contents
In May 2025, we merged a PR at Nango that had a critical bug: an if condition that never fired prevented refreshed credentials from being saved to the database. We caught the bug and reverted it in just under two hours.<br>In those two hours, our scheduled jobs had refreshed and discarded access and refresh tokens for hundreds of connections. For most APIs, that was recoverable: the old refresh token still worked, so the next scheduled refresh repaired the connection.<br>Then we learned how many providers rotate refresh tokens.<br>Many providers, including Airtable and Atlassian, invalidate the old refresh token the moment a replacement is issued. Salesforce and Slack can do the same when rotation is enabled for the OAuth app.<br>There was no way to get those tokens back. For every affected connection, our customers had to ask their users to go through the consent screen again.<br>OAuth is a standard, but OAuth providers are not interchangeable. Each API’s auth behaves a little differently, and some of those differences are undocumented and only surface in production.<br>For context, Nango is an open-source project that lets web applications and agents integrate with third-party APIs. Our customers embed it in their products. Their users connect third-party accounts, and Nango keeps those connections working.<br>Since then, we’ve grown from supporting about 400 APIs to more than 900. The incident changed how we operate auth, but it was only the first of several lessons.<br>Lesson 1: Token rotation makes ordinary bugs irreversible<br>Refresh token rotation is a security win, but a bug in the rotation path can permanently destroy a connection and force the user to reauthorize. That’s what made our incident so expensive.<br>We now treat refresh tokens like money, not cache. The new token gets persisted before anything else can fail. The persistence path has its own regression tests, and any platform-level refresh anomalies immediately page the on-call team.<br>Providers enforce refresh-token rotation differently<br>Take Airtable: a refresh request with a stale token or the wrong client credentials can return a 400 or 401, but Airtable doesn’t stop there. It revokes the user’s entire authorization for your app, and the only way back is for the user to reconnect. Sending the same refresh request twice can also cause revocation. Airtable documents a short grace period for duplicate refreshes, but retrying the stale token after that period revokes the authorization.<br>Airtable also invalidates the old access token the instant a refresh succeeds, so a long-running job still using an old token can fail mid-run.<br>Xero handles it better: if a refresh fails or the response never arrives, you can retry with the previous refresh token for up to 30 minutes.<br>Issues like these pushed us to serialize refreshes per connection behind a distributed lock.<br>*This is only the success path. Failure paths don’t fit in one diagram.<br>Even refreshing early can be wrong. For example, Exact Online’s access tokens last for 10 minutes, and its token endpoint rejects any refresh attempt made more than 30 seconds before expiry. Our “refresh 15 minutes early” rule meant every refresh fell outside that window and was rejected. We had to add support for per-provider expiration buffers.<br>Lesson 2: You can’t reliably know when access tokens expire<br>The official OAuth 2.0 spec (RFC 6749) makes expires_in optional for access tokens. For us, this has been a recurring source of silent breakage.<br>Zendesk, for example, rolled out expiring access tokens for OAuth clients, including existing ones. Its token response includes expires_in only if you request it. Without that parameter, the access token still expires under the default policy, but the response doesn’t say when. We discovered this only after the rotating refresh token lapsed and connections started dying.<br>Salesforce’s token response includes no expiry. The documented way to check it is a separate introspection endpoint, which we call before deciding whether to refresh an access token.
POST /services/oauth2/introspect HTTP/1.1<br>Host: yourInstance.my.salesforce.com<br>Authorization: Basic<br>Content-Type: application/x-www-form-urlencoded
token=&token_type_hint=access_token
Sample response:
"active": true,<br>"scope": "id api refresh_token",<br>"exp": 1549921091,<br>"iat": 1549917491,<br>"token_type": "Bearer",<br>"client_id": "xxxxxxxxxxxxxxxxxxxx"
But in June 2024, the introspection endpoint started intermittently returning unsupported_token_type for some tokens. When introspection failed, we refreshed anyway. The replacement access token could be introspected normally, though we never learned why the original token failed.<br>Refresh token lifetimes are just as inconsistent<br>NetSuite’s OAuth refresh tokens expire after 7 days, requiring full re-consent, which is why many teams use...