An OAuth access token is a key. Whoever holds a valid one can usually call the API it was issued for, within its scopes, until it expires. A refresh token is a key that makes more keys.
Handling them safely isn't complicated, but it does need to be deliberate. This guide covers where tokens should live, how they should move, and the mistakes that most often leak them.
Access tokens are presented to an API on each request, usually in an Authorization: Bearer header. They're often short-lived, but "short" varies by provider from minutes to hours, and some providers issue long-lived ones.
Refresh tokens are exchanged at the provider's token endpoint for new access tokens. They're typically long-lived and more valuable to an attacker, because one refresh token can generate access for weeks or months.
Client secrets identify your application to the provider. Combined with a stolen refresh token, a leaked client secret lets an attacker mint access tokens as your app.
Treat all three as secrets. Treat refresh tokens and client secrets as the most sensitive.
For a SaaS integration, tokens should be obtained, stored and used by your backend. The browser should never see a refresh token or client secret, and in most integration designs it doesn't need the access token either.
A common pattern:
The browser starts the connection and is redirected to the provider.
The provider redirects back to your backend with an authorization code.
The backend exchanges the code for tokens and stores them.
The browser only learns that the connection succeeded.
API calls to the provider are made server-side.
Avoid localStorage and sessionStorage for provider tokens. Any script running on the page, including a compromised third-party script, can read them.
Store tokens encrypted, not just in a database that happens to be on an encrypted disk. Disk encryption protects against stolen hardware; it doesn't protect against a leaked database backup or a SQL injection.
Good practice:
Encrypt token values at the application layer with a key held in a key management service or secrets manager.
Keep the encryption key separate from the database.
Use envelope encryption if you want to rotate keys without re-encrypting everything at once.
Store metadata such as expiry time, scopes and connection status in plain columns so you can query it without decrypting tokens.
Only the services that call provider APIs need to decrypt tokens. Background workers that send emails don't. Support dashboards don't.
Give token decryption its own narrowly scoped permission, and audit access to it. If engineers occasionally need to debug a connection, give them tools that test the connection without revealing the token.
Logs are the most common accidental leak. Tokens end up in logs through:
Request logging middleware that records headers.
Error messages that include the failing request.
Debug statements left in place.
URLs that carry tokens as query parameters.
Redact Authorization headers and known token fields at the logging layer, so it doesn't depend on every developer remembering. Never put tokens in URLs. Check third-party error tracking and APM tools, which often capture request details by default.
When an access token expires, use the refresh token to get a new one. A few details matter:
Store new refresh tokens immediately. Some providers rotate refresh tokens and invalidate the old one on use. If you refresh successfully but fail to save the new refresh token, the connection is broken.
Prevent concurrent refreshes. If two workers refresh the same connection at once and the provider rotates tokens, one of them ends up holding an invalidated token. Use a lock per connection, or route refreshes through a single process.
Refresh before expiry. Refreshing slightly ahead of the expiry time avoids requests failing mid-flight.
Handle refresh failure clearly. An invalid_grant error usually means the user revoked access, the refresh token expired, or the user's password or permissions changed. Mark the connection as needing reauthorization and tell the user. Retrying won't fix it.
Successful refreshes keep a connection alive. A rejected refresh needs the user, not another retry.
The damage a leaked token can do is bounded by its scopes. A token that can read calendar events is less dangerous than one that can manage the whole account. Request only what the feature uses, and add scopes later if the product grows into them.
When a user disconnects an integration or deletes their account, call the provider's revocation endpoint where one exists, then delete the stored tokens. Leaving valid tokens in your database for users who have left is unnecessary risk.
Tokens should only travel over TLS: between the user's browser and your callback, between your backend and the token endpoint, and between your backend and the API. That includes internal service-to-service traffic if tokens move between your own services.
Pathfix OAuth handles the OAuth flow, token storage, token refresh and authenticated API passthrough across 350+ providers, using your own OAuth apps and credentials. Your application makes API requests through Pathfix rather than storing and refreshing provider tokens itself, and keeps ownership of the API logic.
For integration tokens, there's rarely a reason to send them to the browser at all. Keep them server-side and give the browser only your own session cookie.