El login web se complica cuando las cookies de sesión, JWT, OAuth y OpenID Connect se tratan como sinónimos. No lo son. Este es el modelo que uso cuando un callback entra en bucle, una cookie desaparece o un token perfectamente válido sigue devolviendo un 401.
🎙️ Publicado y grabado: ·
Para un sitio web normal, empezaría con un ID de sesión opaco en una cookie HttpOnly. El servidor almacena la sesión y puede revocarla al instante. Un JWT es un paquete firmado de claims, útil cuando varios servicios deben verificar la misma credencial de corta vida sin un session store compartido. No es una cookie de sesión de lujo.
# Opaque session cookie: meaningless to the browser
Set-Cookie: __Host-session=s%3A8f1...; Path=/; Secure; HttpOnly; SameSite=Lax
# JWT payload is readable, not encrypted
{"sub":"user_42","aud":"api","exp":1784905200}
OAuth es autorización delegada. El resource owner es el usuario. El client es tu app. El authorization server pide consentimiento y emite tokens. El resource server es la API que acepta esos tokens. "Client" no significa navegador, y el authorization server no tiene que alojar la API.
resource owner: the person
client: your photo-printing site
authorization server: accounts.example
resource server: photos API
scope: permission such as photos.read
Los scopes describen lo que el client puede hacer, no lo que la persona puede hacer. Un token con photos.read sigue sin deber leer el álbum de otra persona. La API comprueba tanto scope como propiedad.
OAuth no le dice a tu app quién inició sesión. Delega acceso a algo. OpenID Connect añade la capa de identidad: un ID token, un endpoint UserInfo, metadata de descubrimiento y reglas para validar claims de identidad. "Iniciar sesión con…" debería usar OIDC, no inventar identidad consultando un endpoint de perfil OAuth.
# Access token: for the API; audience is the resource server
Authorization: Bearer <access_token>
# ID token: for the client; establishes the login event
iss = https://accounts.example
aud = your_client_id
sub = stable-provider-user-id
nonce = value-bound-to-this-login
issuer + subject. Valida firma, issuer, audience, expiración y nonce antes de confiar en un ID token. Decodificar su JSON no es validación.El flujo práctico actual envía al navegador al authorization server, recibe un código de un solo uso de corta vida, e intercambia ese código por un canal trasero. PKCE vincula el intercambio a la instancia de la app que lo inició. La app crea un verifier aleatorio, envía su hash como challenge, y luego demuestra posesión enviando el verifier en el intercambio de token.
1. app stores code_verifier + state + nonce
2. /authorize?response_type=code&code_challenge=HASH&code_challenge_method=S256
3. callback?code=ONE_TIME_CODE&state=...
4. POST /token with code + code_verifier
5. validate ID token; create your own app session
Los access tokens deben ser de corta vida. Un refresh token obtiene nuevos sin pedirle al usuario que inicie sesión de nuevo, lo que lo convierte en la credencial más valiosa. Mantenlo en un backend de confianza o en un diseño de cookie estrictamente protegido, rótalo en cada uso y revoca la familia de tokens cuando aparezca un token rotado antiguo.
POST /oauth/token
grant_type=refresh_token
refresh_token=<secret>
client_id=<client>
response: access_token + new_refresh_token
store the new refresh token; invalidate the old one
CSRF funciona porque un navegador puede adjuntar la cookie de tu sitio a una petición iniciada por otro sitio. SameSite ayuda, pero los flujos de login también necesitan un valor state aleatorio vinculado a la sesión del navegador. En el callback, compáralo exactamente y consúmelo una vez. State no es decoración y no es la URL de retorno.
OAuthCallbackError: state mismatch
# Diagnose before retrying:
1. Was state stored before redirect?
2. Did the same browser/session return?
3. Did a proxy change host or scheme, losing the cookie?
4. Was the callback opened twice or state consumed early?
# Fix the session/cookie/proxy issue. Never skip state validation.
next=https://attacker.example sin filtrar crea un open redirect que hace tu dominio de login de confianza útil para phishing.Un token en localStorage es legible por cualquier JavaScript que se ejecute en la página, incluyendo una dependencia comprometida o script inyectado. Una cookie HttpOnly oculta la credencial de JavaScript, aunque JavaScript malicioso puede seguir haciendo peticiones mientras la página está comprometida. No hay truco de almacenamiento que haga aceptable un XSS.
# Avoid this default for website login
localStorage.setItem("access_token", token)
# Prefer a backend-for-frontend session
browser --HttpOnly session cookie--> your backend
backend --access token--> provider/API
Los fallos de callback OAuth suelen ser precisos, no místicos. Los proveedores comparan redirect URIs exactamente. Scheme, host, puerto, path y a veces trailing slash deben coincidir con el valor registrado. Después de eso, los authorization codes son de corta vida, de un solo uso y vinculados al client, redirect URI y PKCE verifier.
Error 400: redirect_uri_mismatch
# Sent: http://localhost:3000/auth/callback
# Registered: http://localhost:3000/auth/callback/
# Fix the registration or generated URI so they match exactly.
{"error":"invalid_grant","error_description":"Bad Request"}
# Common causes: code reused/expired, wrong code_verifier,
# wrong redirect_uri, clock skew, or code issued to another client.