Watercolor of a child on a swing by Winslow Homer
← Back to blog

Security patterns

How to Store OAuth Tokens Securely (Without Leaking Secrets)

OAuth access and refresh tokens are bearer credentials: anyone who has them can act as your user. Storing them safely is one of the most important parts of building secure integrations.

Paulina XuNovember 23, 202510 min
SecurityOAuthArchitecture

This guide covers concrete patterns for web backends, SPAs, and mobile apps—without diving into any vendor-specific infrastructure.

Know What You’re Storing

Most providers give you some combination of:

  • Access tokens – short-lived, used to call APIs.
  • Refresh tokens – long-lived, used to get new access tokens.
  • Client secrets – identify your app to the OAuth server.

All three are secrets and should be handled like passwords. OWASP’s secrets management guidance explicitly treats API tokens as sensitive data that must be stored and transmitted securely.

Backend: The Safest Place for Tokens

Whenever possible, keep tokens on the server, not in the browser.

Store tokens in a database, encrypted at rest

Common pattern:

  • Store access_token, refresh_token, expiry, provider, and user ID in a tokens table.
  • Use:
  • Database encryption at rest.
  • Application-level encryption for the token columns with keys managed by KMS or a secrets manager.

Cloud providers recommend using dedicated secret stores for highly sensitive values and keeping encryption keys separate from the data.

CREATE TABLE oauth_tokens (
  id            SERIAL PRIMARY KEY,
  user_id       UUID NOT NULL,
  provider      TEXT NOT NULL,
  access_token  BYTEA NOT NULL,   -- encrypted
  refresh_token BYTEA,
  expires_at    TIMESTAMPTZ,
  UNIQUE (user_id, provider)
);

Never log tokens

  • Scrub tokens from logs, traces, and error messages.
  • Use log filters/redaction in your logging pipeline.

OWASP explicitly warns against exposing secrets in logs and monitoring systems.

Browser Apps: What Not to Do (and What to Do Instead)

For SPAs and other “public clients”, there is no truly secure storage in the browser. Any data readable by JavaScript can be stolen via XSS. OWASP and others warn against keeping long-lived tokens in localStorage for this reason.

Avoid localStorage for tokens

  • localStorage is readable by any script on the page (including injected scripts).
  • Tokens stored there persist until cleared—great for attackers, bad for you.

Better pattern: backend-issued cookies + in-memory access tokens

A commonly recommended pattern for SPAs is:

  1. Keep access tokens in memory (JavaScript variables, React state, etc.).
  2. Store refresh tokens (or a session identifier) in an HTTP-only, Secure, SameSite cookie.
  3. The SPA calls your backend; the backend reads the cookie, refreshes tokens if needed, and talks to the third-party API.

Example cookie flags from the server side:

Set-Cookie: rt_session=abc123;
  HttpOnly;
  Secure;
  SameSite=Strict;
  Path=/api/

Because the cookie is HttpOnly, JavaScript can’t read it, which reduces exposure from XSS.

Mobile & Desktop: Use OS Secure Storage

On native platforms, store tokens in OS-level secure stores:

  • iOS: Keychain
  • Android: Keystore / EncryptedSharedPreferences
  • Desktop: platform-specific keychains or encrypted credential stores

These systems are designed specifically for long-lived secrets and are recommended over flat files or standard app storage.

Operational Best Practices

Use short-lived access tokens + refresh tokens

Short-lived access tokens limit blast radius if leaked; refresh tokens support long-lived sessions without constant logins. This model is recommended in OAuth 2.0 guidance and by major IdPs like Auth0 and Google.

Rotate and revoke

  • Implement refresh token rotation if supported: every use of a refresh token returns a new one and invalidates the old.
  • Provide a way to revoke tokens when:
  • Users disconnect your app.
  • You detect suspicious activity.
  • You rotate client secrets.

Keep secrets out of source control and CI logs

OWASP’s secrets cheat sheet and multiple cloud security guides stress: never hard-code tokens in source code or commit them to Git; use secret managers or encrypted config instead.

Quick Checklist

Before shipping, check that:

  • Access & refresh tokens live only on the backend or in OS secure storage.
  • Browser apps use in-memory tokens + HTTP-only cookies, not localStorage.
  • Tokens are encrypted at rest and never logged.
  • Access tokens are short-lived; refresh tokens are rotated and revocable.
  • Client secrets and signing keys are managed via a proper secrets manager.

If you follow these patterns, you dramatically reduce the chance that a leaked token turns into a serious breach—without making your OAuth integration painful to use.

Read More