On this page
Connecting an application to another service requires delegating access without collecting the user's password for that service. OAuth provides that delegation framework. Its security depends on the entire redirect and token lifecycle, not simply exchanging a code for a token after the consent screen appears.
Identify the actors and trust boundaries
Your application is the client. The authorization server authenticates the user and issues credentials. The resource server exposes the protected API. A provider may operate both servers, but their responsibilities remain distinct. Decide which backend owns the callback and where long-lived credentials will be stored.
Inspect a synthetic token response in the JSON Formatter to separate access-token data from account information. Never paste real authorization codes, refresh tokens, or client secrets into debugging tools. OAuth is delegation, not automatically an identity protocol; use OpenID Connect when the application needs standardized login identity.
For a conventional backend integration, use the authorization code flow with PKCE. The backend can also authenticate as a confidential client where the provider requires it. PKCE and client authentication solve different problems and are not interchangeable. Follow the provider's current profile and OAuth security best practices.
Create a short-lived authorization transaction
When a user starts connecting an account, create a transaction bound to that user's current session. Store the provider, intended action, a randomly generated state value, the PKCE verifier, and an expiration time. Do not store an arbitrary return URL without validating it against a local allowlist.
Derive the challenge using the S256 method. The verifier must satisfy the syntax and entropy requirements in RFC 7636. Node's cryptographic primitives can create a suitable value without relying on application timestamps or ordinary random-number functions.
import { randomBytes, createHash } from 'node:crypto';
const verifier = randomBytes(32).toString('base64url');
const challenge = createHash('sha256')
.update(verifier)
.digest('base64url');
const state = randomBytes(32).toString('base64url');
const params = new URLSearchParams({
response_type: 'code',
client_id: configuredClientId,
redirect_uri: configuredCallback,
scope: 'read:projects',
state,
code_challenge: challenge,
code_challenge_method: 'S256'
});The variables for client and callback configuration are trusted application settings, not request parameters copied unchecked from the browser. Save the transaction before redirecting. Avoid putting the verifier in a public URL; its purpose is to remain unavailable to someone who intercepts only the authorization code.
Validate the callback before exchanging the code
The callback may contain a provider error instead of an authorization code. Handle cancellation as a normal outcome while still checking the transaction context. For success, validate state, session binding, expiration, and expected provider before attempting the token exchange. Consume the transaction once so the callback cannot be reused.
Use a fixed, registered redirect URI. A permissive callback redirect can leak codes or turn your application into an open redirector. For multi-provider integrations, prevent authorization-server mix-ups by retaining the provider identity in the server-side transaction and applying the provider's issuer validation requirements.
Exchange the code from the backend over HTTPS using the saved verifier and exact callback URI expected by the authorization server. Authenticate the client using the provider's configured method when applicable. Do not send a confidential client secret to browser JavaScript or include it in the authorization URL.
A failed exchange does not mean you should keep retrying a one-time code indefinitely. Bound network timeouts, handle provider-specific error responses, and restart the authorization flow when required. Avoid logging callback query strings because authorization codes and state values may otherwise enter proxy or application logs.
Store tokens and preserve account boundaries
An access token authorizes calls to the resource server. Its exact format may be opaque; do not assume every access token is a JWT you should decode for identity. Use the provider's supported endpoints and metadata to learn what it represents.
Store refresh tokens in a protected server-side store with access restricted to the integration components that need them. Encrypt sensitive credentials at rest according to your platform's key-management practices. Keep tokens out of analytics events, exception reports, and browser storage when the backend can make the resource requests itself.
Bind each integration to the correct local account and external account identity. Account linking is a security-sensitive operation: require the current user to have permission to modify that integration, and consider recent reauthentication for high-risk changes. A valid external authorization does not grant permission to overwrite another customer's connection.
If the feature is login, validate an OpenID Connect ID token with a maintained library, including signature, issuer, audience, expiry, and nonce where required. The OpenID Connect Core specification distinguishes these identity assertions from OAuth access tokens.
Operate refresh, disconnect, and testing flows
Refresh before access expires according to the provider's documented behavior, allowing a small operational margin rather than assuming exact clock agreement. Coordinate concurrent refresh attempts when tokens rotate: two workers using one old refresh token can interfere with each other. Persist the new credential set atomically when rotation succeeds.
Implement disconnect semantics. Revoke tokens through the provider's supported endpoint where available, remove local credentials, and stop pending jobs that might otherwise continue using cached access. Explain whether disconnecting only stops future access or also deletes already imported data.
Test denied consent, mismatched state, an expired transaction, callback replay, wrong verifier, and provider outages. Include users opening multiple connect tabs; a single session field that overwrites the previous transaction can create confusing failures. A bounded collection of transaction records works better.
Use Unix Timestamp Converter with synthetic expiry examples to document refresh timing. Keep an integration checklist in your Backend & APIs reference covering scopes, storage, key rotation, and support procedures. Request only the scopes the feature actually needs; smaller delegated privileges reduce the impact of implementation mistakes.
Keep concurrent connection attempts independent
A user may open two provider consent tabs before either callback returns. Storing one verifier and one state value directly on the session means the second attempt can overwrite the first. Instead, store separate short-lived transactions indexed by an unpredictable identifier and associate each with the same authenticated session. Each callback must select and consume only its matching transaction.
Account linking also needs an explicit confirmation policy. If the user intended to connect a work account but consented with a personal provider account, show the provider identity returned through supported APIs before treating the integration as ready. Do not silently replace an existing connection solely because the local browser session matches.
Test two tabs completing in reverse order, one cancelled attempt, an expired transaction, and a callback after local sign-out. None should attach tokens to the wrong local account or revive an expired session. Cleanup abandoned transactions regularly, while retaining only redacted operational events needed to diagnose failures without preserving codes or verifiers.
Key takeaways
- Bind each authorization attempt to a short-lived, single-use server-side transaction.
- Use PKCE alongside any required confidential-client authentication.
- Separate delegated API access from OpenID Connect login identity.
- Protect token storage, account linking, refresh rotation, and disconnect behavior.
Reach designers & developers at work
Put your product next to the tools people use every day. One sponsor per placement, no tracking pixels.
Become a sponsor