¿Odias los anuncios? Ir Sin publicidad Hoy

OAuth 2.0 Flows Authorization Code, PKCE, and Client Credentials

Actualizado en

Una guía para desarrolladores para elegir el flujo adecuado de OAuth 2.0. Aborda el flujo de autorización (aplicaciones web), PKCE (aplicaciones de página única y móviles) y credenciales del cliente (intercambio entre servidores), con ejemplos de código funcionales y los errores que te atrapan más adelante.

OAuth 2.0 Flows: Authorization Code, PKCE, and Client Credentials 1
ANUNCIO · ¿ELIMINAR?

Three flows cover 95% of real OAuth 2.0 use cases. The spec defines more, but the rest are deprecated, edge-case, or both. Pick the right one up front and you skip a painful refactor when auth requirements change.

FlowCuándo usarloUser involved?Needs client_secret?
Authorization CodeWeb app with a backend serverYes — stays on the server
Authorization Code + PKCESPA, mobile app, any public clientNo
Client CredentialsServer-to-server (no user)NoYes — stays on the server

Authorization Code Flow

The standard flow for web apps with a backend. The access token and client secret never touch the browser — the token exchange happens server-side. That’s the whole point.

Step 1: Redirect the user

Build an authorization URL and redirect the browser to it:

GET https://accounts.example.com/oauth/authorize
  ?response_type=code
  &client_id=YOUR_CLIENT_ID
  &redirect_uri=https://yourapp.com/callback
  &scope=openid+profile+email
  &state=RANDOM_CSRF_TOKEN

El state value is your CSRF defense for the redirect. Generate a fresh random string per flow, store it in the session, and verify it when the user comes back. If you skip this check, an attacker can drive users through a flow using the attacker’s own authorization code — silently linking the user’s account to the attacker’s identity.

Step 2: Handle the callback

The authorization server redirects back to your redirect_uri with a short-lived code:

GET https://yourapp.com/callback?code=AUTH_CODE&state=SAME_STATE_YOU_SENT

Verificar state matches what you stored. Then exchange the code server-side.

Step 3: Exchange the code for tokens (server-side only)

POST https://accounts.example.com/oauth/token
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code
&code=AUTH_CODE
&redirect_uri=https://yourapp.com/callback
&client_id=YOUR_CLIENT_ID
&client_secret=YOUR_CLIENT_SECRET

The response:

{
  "access_token": "eyJhbGciOiJSUzI1NiIs...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "def50200a12b3c...",
  "scope": "openid profile email"
}

Keep both tokens server-side. When access_token expires (check expires_in), use the refresh_token to get a new one without sending the user through login again.

PKCE — For Clients That Can’t Keep Secrets

A SPA or mobile app has no safe place for a client_secret. Anyone can open DevTools and find it in your JS bundle. Anyone can decompile your APK. PKCE (Proof Key for Code Exchange, pronounced “pixy”) solves this with a one-time cryptographic challenge — no shared secret needed.

The flow is identical to Authorization Code with two additions: a code_verifier (random string you generate) and a code_challenge (SHA-256 hash of the verifier, base64url-encoded). You send the challenge upfront, then prove you hold the verifier at exchange time.

Step 1: Generate the verifier and challenge

function generateCodeVerifier() {
  const array = new Uint8Array(32);
  crypto.getRandomValues(array);
  return base64url(array);
}

async function generateCodeChallenge(verifier) {
  const data = new TextEncoder().encode(verifier);
  const digest = await crypto.subtle.digest('SHA-256', data);
  return base64url(new Uint8Array(digest));
}

function base64url(buffer) {
  return btoa(String.fromCharCode(...buffer))
    .replace(/\+/g, '-')
    .replace(/\//g, '_')
    .replace(/=/g, '');
}

// Usage
const codeVerifier = generateCodeVerifier();
const codeChallenge = await generateCodeChallenge(codeVerifier);
// Store codeVerifier in memory — NOT localStorage

Store the code_verifier in memory — a module-scoped variable, not localStorage. You’ll send it at token exchange time.

Step 2: Authorization request — add the challenge

GET https://accounts.example.com/oauth/authorize
  ?response_type=code
  &client_id=YOUR_CLIENT_ID
  &redirect_uri=https://yourapp.com/callback
  &scope=openid+profile
  &state=RANDOM_CSRF_TOKEN
  &code_challenge=BASE64URL_SHA256_OF_VERIFIER
  &code_challenge_method=S256

Step 3: Token exchange — verifier instead of client_secret

POST https://accounts.example.com/oauth/token
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code
&code=AUTH_CODE
&redirect_uri=https://yourapp.com/callback
&client_id=YOUR_CLIENT_ID
&code_verifier=ORIGINAL_VERIFIER_YOU_GENERATED

The authorization server hashes the verifier and checks it against the challenge you sent in step 2. An attacker who intercepted the auth code has no idea what the verifier is — they cannot exchange it.

Worth knowing: OAuth 2.1 (the in-progress modernization of 2.0) mandates PKCE for all flows involving redirects. If you’re writing new code, use PKCE regardless of whether your provider currently requires it.

Client Credentials — No User, No Problem

Background jobs, microservices calling other microservices, cron tasks hitting an API — none of these involve a user. Client Credentials is the right flow: the service authenticates directly using its own client ID and secret.

POST https://accounts.example.com/oauth/token
Content-Type: application/x-www-form-urlencoded

grant_type=client_credentials
&client_id=YOUR_CLIENT_ID
&client_secret=YOUR_CLIENT_SECRET
&scope=api:read api:write

Response:

{
  "access_token": "eyJhbGciOiJSUzI1NiIs...",
  "token_type": "Bearer",
  "expires_in": 86400
}

No refresh token — when it expires, just request a new one. The common mistake: hitting the token endpoint on every API call. Cache the token, check expiry before each request, only re-fetch when it’s about to expire. One token request per day (or per hour, depending on expires_in) instead of one per request:

let cachedToken = null;
let tokenExpiresAt = 0;

async function getAccessToken() {
  // Refresh 30 seconds before actual expiry
  if (cachedToken && Date.now() < tokenExpiresAt - 30_000) {
    return cachedToken;
  }

  const res = await fetch('https://accounts.example.com/oauth/token', {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: new URLSearchParams({
      grant_type: 'client_credentials',
      client_id: process.env.CLIENT_ID,
      client_secret: process.env.CLIENT_SECRET,
      scope: 'api:read api:write',
    }),
  });

  const data = await res.json();
  cachedToken = data.access_token;
  tokenExpiresAt = Date.now() + (data.expires_in * 1000);
  return cachedToken;
}

Mistakes Developers Actually Make

Storing tokens in localStorage

Any XSS vulnerability — in your own code, a dependency, a tag manager script — can read everything in localStorage. For SPAs: store the access token in memory (a module-scoped variable that disappears on refresh). Use httpOnly cookies for refresh tokens when you have a backend that can set them. JavaScript can't read httpOnly cookies.

Using the implicit flow

The implicit flow returns tokens directly in the URL fragment (#access_token=...). Those tokens end up in browser history, server access logs, and Referer headers. It was deprecated in RFC 9700. There is no reason to use implicit flow for new code. Use PKCE.

Skipping state validation

Pega el contenido de tu .github/workflows/*.yml aquí state validation on the callback, an attacker can craft a redirect URL that completes an OAuth flow using their own authorization code. The result: your user's account gets linked to the attacker's identity at the provider. Generate it fresh per flow, store it in the session, check it on callback.

Putting client_secret in frontend code

There is no such thing as a secret that lives in a browser. Minification does not hide it. Obfuscation does not protect it. If your runtime is a browser or a mobile app, you have a public client — use PKCE and omit the client_secret entirely. That's not a workaround; it's how the spec intends public clients to work.

Not handling token expiry proactively

Every access token has an expires_in value. If you hold onto a token until it fails with a 401 and then re-auth, users hit mysterious errors. Check expiry before making requests, refresh proactively (30 seconds before expiry is a reasonable buffer), and handle the rare case where a refresh token itself has expired.

Inspecting Tokens While You Work

Most OAuth providers issue JWTs as access tokens. The payload is base64url-encoded and readable without the private key — only the signature needs the key to verify. When you're debugging a flow and want to see the claims, scopes, or expiry in a token, paste it into the Decodificador JWT.

If you're hand-testing PKCE and need to verify what a base64url-encoded code_challenge decodes to, the Base64 converter handles standard and URL-safe variants.

La versión breve

One question determines the right flow: does your runtime have a secure place to keep a secret?

  • Backend server → Authorization Code or Client Credentials (secret stays on the server)
  • Browser or mobile app → Authorization Code + PKCE (no secret at all)
  • No user involved → Client Credentials

PKCE works for every flow involving a redirect — there's no downside to using it even when your provider doesn't require it yet. OAuth 2.1 will.

¿Quieres eliminar publicidad? Adiós publicidad hoy

Instalar extensiones

Agregue herramientas IO a su navegador favorito para obtener acceso instantáneo y búsquedas más rápidas

añadir Extensión de Chrome añadir Extensión de borde añadir Extensión de Firefox añadir Extensión de Opera

¡El marcador ha llegado!

Marcador es una forma divertida de llevar un registro de tus juegos, todos los datos se almacenan en tu navegador. ¡Próximamente habrá más funciones!

ANUNCIO · ¿ELIMINAR?
ANUNCIO · ¿ELIMINAR?
ANUNCIO · ¿ELIMINAR?

Noticias Aspectos técnicos clave

Involucrarse

Ayúdanos a seguir brindando valiosas herramientas gratuitas

Invítame a un café
ANUNCIO · ¿ELIMINAR?