Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Some links on this page are affiliate links: if you buy through them we may earn a commission, at no extra cost to you.

An OAuth authorization-code flow returns a short-lived authorization code to your registered redirect URI; your client then exchanges that code at the token endpoint for tokens. For current implementations, use PKCE with the S256 method: public clients must use PKCE under the IETF’s January 2025 security best current practice, and confidential clients are recommended to use it too. The example below shows the protocol steps, but endpoint URLs, client registration, and authentication details must come from your identity provider’s current documentation.

What the authorization-code flow returns

The authorization code is not an access token. It is an intermediate credential delivered through the browser redirect, then exchanged by the client at the token endpoint. The token endpoint returns the tokens when it accepts the code and validates the request. The client can then present an access token to a protected API according to that API’s requirements. This distinction is central to the authorization-code grant described in RFC 6749 and illustrated in OAuth.com’s authorization-code example.

The user authenticates and approves access at the authorization server. Your application should redirect the user there rather than asking for or handling the user’s identity-provider password directly.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Authorization-code flow with PKCE, step by step

  1. Create transaction values. Generate a cryptographically random PKCE verifier for this login and a separate unpredictable state value. Keep both associated with the initiating browser session or native-app transaction.
  2. Build the authorization request. Include the provider’s authorization endpoint, your registered client_id, exact redirect_uri, requested scope, response_type=code, state, the S256-derived code_challenge, and code_challenge_method=S256.
  3. Redirect the user. Navigate the user agent to the authorization endpoint. The provider authenticates the user and obtains any required authorization.
  4. Receive the callback. The provider redirects to the registered URI with a code and, where used, the returned state. Validate that state against the pending transaction before proceeding. Handle provider error parameters as errors, not as successful callbacks.
  5. Exchange the code. Send the authorization code, the same redirect URI, the client identifier, and the original PKCE verifier to the token endpoint. A confidential client also authenticates as required by its provider and registration. Do not put a confidential client secret in browser-delivered code.
  6. Use the response safely. If the exchange succeeds, process the returned tokens according to the provider’s response and your application’s token-storage and refresh design. Send the access token only to the intended protected resource.

The verifier never goes in the authorization request; the request contains its S256 challenge. The authorization server checks the submitted verifier against that challenge during exchange.

Runnable Node.js example for a server-side web app

This compact example uses only Node.js built-ins and demonstrates the cryptographic values, redirect, callback validation, and token exchange. It assumes an HTTPS deployment in production, a provider already configured with the exact callback URI, and a confidential server-side client using HTTP Basic client authentication. Replace the endpoint and client placeholders with the provider’s documented values. Provider-specific client authentication can differ; follow the provider’s current instructions rather than assuming Basic authentication is universal.

Save as server.mjs, set the environment variables shown, and run with a current Node.js release using node server.mjs. It listens on port 3000 by default.

import http from 'node:http';
import crypto from 'node:crypto';

const AUTHORIZATION_ENDPOINT = process.env.AUTHORIZATION_ENDPOINT;
const TOKEN_ENDPOINT = process.env.TOKEN_ENDPOINT;
const CLIENT_ID = process.env.CLIENT_ID;
const CLIENT_SECRET = process.env.CLIENT_SECRET;
const REDIRECT_URI = process.env.REDIRECT_URI || 'http://localhost:3000/callback';
const SCOPE = process.env.SCOPE || 'openid profile';
const PORT = Number(process.env.PORT || 3000);

if (!AUTHORIZATION_ENDPOINT || !TOKEN_ENDPOINT || !CLIENT_ID || !CLIENT_SECRET) {
  throw new Error('Set AUTHORIZATION_ENDPOINT, TOKEN_ENDPOINT, CLIENT_ID, and CLIENT_SECRET');
}

const pending = new Map();
const b64url = (buf) => buf.toString('base64url');
const randomValue = () => b64url(crypto.randomBytes(32));

const server = http.createServer(async (req, res) => {
  const url = new URL(req.url, `http://${req.headers.host}`);

  if (url.pathname === '/login') {
    const state = randomValue();
    const verifier = randomValue();
    const challenge = b64url(crypto.createHash('sha256').update(verifier).digest());
    // Demo-only transaction storage. Use a secure, session-bound store in production.
    pending.set(state, { verifier, createdAt: Date.now() });

    const auth = new URL(AUTHORIZATION_ENDPOINT);
    auth.search = new URLSearchParams({
      response_type: 'code',
      client_id: CLIENT_ID,
      redirect_uri: REDIRECT_URI,
      scope: SCOPE,
      state,
      code_challenge: challenge,
      code_challenge_method: 'S256'
    }).toString();
    res.writeHead(302, { Location: auth.toString() });
    return res.end();
  }

  if (url.pathname === '/callback') {
    const state = url.searchParams.get('state');
    const code = url.searchParams.get('code');
    const transaction = state && pending.get(state);
    if (!transaction || Date.now() - transaction.createdAt > 10 * 60 * 1000) {
      res.writeHead(400, { 'Content-Type': 'text/plain' });
      return res.end('Missing, invalid, or expired OAuth state');
    }
    pending.delete(state);
    if (url.searchParams.has('error')) {
      res.writeHead(400, { 'Content-Type': 'text/plain' });
      return res.end(`Authorization failed: ${url.searchParams.get('error')}`);
    }
    if (!code) {
      res.writeHead(400, { 'Content-Type': 'text/plain' });
      return res.end('Callback did not contain an authorization code');
    }

    const basic = Buffer.from(`${CLIENT_ID}:${CLIENT_SECRET}`).toString('base64');
    const tokenResponse = await fetch(TOKEN_ENDPOINT, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/x-www-form-urlencoded',
        Authorization: `Basic ${basic}`
      },
      body: new URLSearchParams({
        grant_type: 'authorization_code',
        code,
        redirect_uri: REDIRECT_URI,
        code_verifier: transaction.verifier
      })
    });
    const responseBody = await tokenResponse.text();
    res.writeHead(tokenResponse.ok ? 200 : 502, { 'Content-Type': 'application/json' });
    return res.end(responseBody);
  }

  if (url.pathname === '/') {
    res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
    return res.end('<a href="/login">Sign in</a>');
  }
  res.writeHead(404);
  res.end('Not found');
});

server.listen(PORT, () => console.log(`Listening on http://localhost:${PORT}`));

For a real deployment, replace the in-memory pending map with storage bound to the user’s initiating session, enforce HTTPS, expire and consume transaction entries, avoid returning raw tokens in an HTML response, and implement the application’s authenticated session and token lifecycle. The sample’s simple response is for demonstrating the exchange, not a production token-storage design. Register the callback URI exactly as sent in both authorization and token requests.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

PKCE mechanics and security requirements

Generate a fresh verifier for each authorization

A verifier is a high-entropy, transaction-specific secret. Its corresponding challenge is the base64url-encoded SHA-256 digest. Never hard-code a verifier or reuse one across logins. RFC 9700 says the PKCE challenge or other transaction value must be specific to the transaction and securely bound to the client and user agent.

Use S256, not a plain verifier challenge

Send code_challenge_method=S256 and the derived challenge. RFC 9700 recommends a PKCE method that does not expose the verifier in the authorization request and states, “Currently, S256 is the only such method.” See RFC 9700, Best Current Practice for OAuth 2.0 Security, published January 2025.

Enforce PKCE throughout the exchange

When an authorization request contains a valid code challenge, the authorization server must enforce the matching verifier at the token endpoint. RFC 9700 also calls for mitigating PKCE downgrade attempts. A client should not silently fall back to a less secure flow when PKCE is unavailable; confirm the provider’s supported behavior and choose a compliant configuration.

Use state to bind and validate the callback

Generate unpredictable state for the transaction, retain it where the initiating client can validate it, and reject callbacks that do not match. State handling is transaction-specific; a constant state value does not validate that a callback belongs to the login the user started. Treat provider error callbacks separately and do not exchange a code until the response passes validation.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Server-side, browser, and native client differences

Implementation Can it keep a client secret? Where to handle the verifier and tokens Redirect and authentication considerations
Server-side web application Usually yes, when the secret remains on the server. Keep transaction state and verifier in secure, session-bound server storage; protect tokens using the application’s server-side session and storage design. The web server receives the registered callback. Use the provider’s configured confidential-client authentication method at the token endpoint.
Browser-based public client No: code delivered to a user’s browser cannot keep a shared secret confidential. Use PKCE and the browser application’s appropriate transaction handling; token storage needs a deliberate design suited to the application and threat model. Use the provider’s public-client registration and documented browser redirect behavior; do not embed a client secret.
Native public client No: a secret embedded in a distributed app cannot be treated as confidential. Use PKCE; keep transaction values associated with the sign-in attempt and use platform-appropriate token protection. The app receives a provider-configured redirect, such as a supported app link or URI scheme. Exact support and configuration are provider- and platform-specific.

These are client-type distinctions, not one universal recipe. RFC 9700 requires public clients to use PKCE and recommends it for confidential clients. Microsoft Learn’s provider-specific guidance illustrates PKCE and OpenID Connect behavior for its supported app types, but its endpoints and SDK calls should not be copied as generic OAuth defaults: Microsoft Learn: OAuth 2.0 authorization code flow.

Provider and application settings to verify

  • Endpoints: Confirm the authorization and token endpoint URLs for the correct provider, tenant, realm, or environment. Do not infer them from another provider’s example.
  • Redirect URI: Register the exact URI and send the matching value in the request. Scheme, host, path, and any provider-required port or trailing-slash behavior matter.
  • Client type and authentication: Register the app as public or confidential according to where it runs. For confidential clients, use the provider-supported authentication method at the token endpoint; keep credentials server-side.
  • Scopes: Request only the scopes needed and confirm the provider’s syntax and consent model. OpenID Connect scopes and API permissions are related but not interchangeable assumptions.
  • Token lifecycle: Decide how the application protects access and refresh tokens, whether refresh tokens are issued for this client and scope, and how revocation, expiration, and rotation are handled. These rules depend on provider and application design.
  • SDK and response specifics: Check current provider documentation for exact parameter names, SDK signatures, error formats, and any OpenID Connect validation work, including ID-token handling.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Troubleshooting common failures

Redirect URI mismatch

Symptom: The provider rejects the authorization request or token exchange. Fix: Compare the registered redirect URI character-for-character with the value sent in the authorization request and token request. Use the same URI in both requests when required by the provider.

Invalid or expired authorization code

Symptom: The token endpoint rejects the code. Fix: Exchange the code promptly, only once, with the correct client and redirect URI. Authorization codes are intermediate credentials, not reusable access tokens; restart authorization if the code is no longer valid.

PKCE verification failed

Symptom: Token exchange reports a verifier or challenge mismatch. Fix: Retrieve the verifier saved for that specific login, ensure it was not regenerated or reused, and confirm the request used the base64url SHA-256 challenge with S256. Submit the verifier only to the token endpoint.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

State is missing or does not match

Symptom: The callback cannot be tied to a pending login. Fix: Check that the initiating browser session or app transaction survives the redirect and that state was stored and retrieved correctly. Reject the callback and restart sign-in rather than skipping validation.

Client authentication fails

Symptom: The authorization request succeeds but token exchange returns a client authentication error. Fix: Confirm the app’s registration type and the token endpoint authentication method required by the provider. Do not send a secret from a browser or native distributed app.

Provider or SDK parameters differ from the sample

Symptom: The provider rejects an otherwise plausible request or a library call does not match the example. Fix: Treat protocol examples as a shape, not a substitute for current provider documentation. Verify endpoint URLs, supported grant options, exact SDK signature, scope conventions, and callback configuration for the chosen provider.

Or skip the browser setup:

OAuth authorization code and PKCE solve identity authorization; they do not capture web pages. If the next task is getting clean screenshots of pages for a developer workflow, ScreenshotNeo is a website screenshot API and MCP server. One GET request takes a URL and returns an image or PDF. Use your ScreenshotNeo access key from the account setup and see the API documentation for response options and parameters:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

ScreenshotNeo accepts cookie and consent banners and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture; those steps can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits cost nothing, and responses identify the page verdict and billing status in headers. Its MCP server gives AI agents tools for screenshots, page information, and PDF capture. The free plan includes 1,000 shots per month with no card; paid plans start at $5 for 3,000 shots. Sign up free for 1,000 screenshots a month, with no card required.

Frequently Asked Questions

Does the authorization code itself let me call an API?

No. The client exchanges the authorization code at the token endpoint; the resulting access token is used with the protected resource.

Can I put an OAuth client secret in a single-page app or mobile app?

No. A secret distributed to browser or native public clients cannot be kept confidential; use the provider’s public-client configuration and PKCE.

Is PKCE needed for a confidential web client?

RFC 9700 recommends PKCE for confidential clients as well as requiring it for public clients.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.