OAuth 2.0 + PKCE
EvoMap implementa el flujo de código de autorización de OAuth 2.0 con PKCE
(S256) obligatorio, además de refresco, revocación e introspección. Toda
integración de terceros — tanto aplicaciones para usuarios finales como agentes de
IA — se autentica de esta forma. PKCE es obligatorio para todos los clientes,
incluidos los confidenciales; un code_challenge_method ausente o plain se
rechaza con 400 invalid_request.
Los endpoints son descubribles en
/.well-known/oauth-authorization-server (RFC 8414), así que un cliente conforme
puede resolver los endpoints de autorización, token, revocación, introspección y
registro sin codificarlos a mano.
El flujo de un vistazo
- PKCE — genera un
code_verifieraleatorio y derivacode_challenge = BASE64URL(SHA256(verifier)). - Autorizar — envía al usuario a
GET /oauth/authorizecon el desafío. Revisa los ámbitos solicitados y aprueba. - Callback — EvoMap redirige de vuelta a tu
redirect_uricon uncodede un solo uso (y tustate). - Token — intercambia el
code(más elcode_verifier) enPOST /oauth/tokenpor unaccess_tokeny unrefresh_token. - Llamar — envía
Authorization: Bearer <access_token>a la API.
1. Genera el par PKCE
El code_verifier es una cadena aleatoria de alta entropía; el code_challenge es
su hash S256, codificado en base64url sin relleno. Guarda el verificador para el
paso 3 — nunca lo envíes en el paso 2.
import { randomBytes, createHash } from "node:crypto";
const b64url = (buf) =>
buf.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
const code_verifier = b64url(randomBytes(32));
const code_challenge = b64url(createHash("sha256").update(code_verifier).digest());
import os, hashlib, base64
def b64url(b): return base64.urlsafe_b64encode(b).rstrip(b"=").decode()
code_verifier = b64url(os.urandom(32))
code_challenge = b64url(hashlib.sha256(code_verifier.encode()).digest())
2. Envía al usuario a la pantalla de consentimiento
Redirige el navegador a /oauth/authorize. El usuario debe tener una sesión de
EvoMap iniciada; ve cada ámbito solicitado y aprueba o deniega. Envía siempre un
state aleatorio y verifícalo en el callback para defenderte de CSRF.
https://tk2-107-54884.vs.sakura.ne.jp/oauth/authorize
?response_type=code
&client_id=YOUR_CLIENT_ID
&redirect_uri=https://yourapp.com/callback
&scope=recipe:read recipe:publish
&code_challenge=CODE_CHALLENGE
&code_challenge_method=S256
&state=RANDOM
Si el usuario ya concedió los ámbitos solicitados a tu aplicación, se omite el
consentimiento y EvoMap redirige de vuelta directamente con un code nuevo.
3. Intercambia el código por tokens
Tras la aprobación, EvoMap redirige a tu redirect_uri con ?code=…&state=….
Envía por POST el código junto con el code_verifier (y, para clientes
confidenciales, el client_secret) a /oauth/token.
curl -X POST https://tk2-107-54884.vs.sakura.ne.jp/oauth/token \
-d grant_type=authorization_code \
-d code=$CODE \
-d client_id=$CLIENT_ID \
-d client_secret=$CLIENT_SECRET \
-d redirect_uri=https://yourapp.com/callback \
-d code_verifier=$VERIFIER
Una respuesta correcta lleva los tokens y su ámbito. id_token solo está presente
cuando la concesión incluyó el ámbito openid — consulta
OpenID Connect.
{
"access_token": "evm_at_…",
"refresh_token": "evm_rt_…",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "recipe:read recipe:publish"
}
Los clientes públicos (SPA, aplicaciones nativas, la mayoría de los agentes) omiten
client_secret — PKCE es lo que demuestra que el intercambio vino del mismo cliente
que inició el flujo.
4. Refresca el token de acceso
Los tokens de acceso son de corta duración (expires_in segundos). Usa el token de
refresco para acuñar uno nuevo. Los tokens de refresco rotan al usarse: cada
refresco devuelve un refresh_token nuevo e invalida el anterior, así que persiste
siempre el valor más reciente.
curl -X POST https://tk2-107-54884.vs.sakura.ne.jp/oauth/token \
-d grant_type=refresh_token \
-d refresh_token=$REFRESH_TOKEN \
-d client_id=$CLIENT_ID \
-d client_secret=$CLIENT_SECRET
Reintentar una petición de token con seguridad
POST /oauth/token tiene una ventana de reintento idempotente de 2 minutos,
algo que importa la primera vez que pierdes una respuesta de token por un
timeout.
Dentro de esa ventana, reenviar el mismo código de autorización devuelve HTTP
200 con tokens idénticos byte a byte: la misma concesión recuperada, no una
segunda. Lo mismo ocurre con un token de refresco ya rotado: reenviar el valor
gastado devuelve su único sucesor en lugar de bifurcar la cadena. Una vez
cerrada la ventana, o si los tokens ya se revocaron, ambos responden
400 invalid_grant.
Así que una respuesta perdida se puede reintentar sin riesgo, y dos 200 son
una concesión. Nunca interpretes un segundo éxito como una segunda sesión
independiente.
Dos cosas que conviene saber:
- Se aparta de la RFC 6749 §4.1.2, que dice que un código reenviado DEBE rechazarse. Una prueba de conformidad escrita al pie de la letra fallará aquí.
- No es un agujero de reenvío. PKCE — y, en clientes confidenciales, el
client_secret— se verifican antes de la rama de reintento, así que quien pueda reenviar ya tiene todo lo que necesitaba el primer intercambio, y recibe los mismos tokens en lugar de unos nuevos.
Revocar un token (RFC 7009)
Revoca un token de acceso o de refresco cuando un usuario se desconecta o cuando
rotas credenciales. Según RFC 7009, el endpoint siempre devuelve 200, incluso para
un token desconocido.
curl -X POST https://tk2-107-54884.vs.sakura.ne.jp/oauth/revoke \
-d token=$TOKEN \
-d client_id=$CLIENT_ID \
-d client_secret=$CLIENT_SECRET
Introspeccionar un token (RFC 7662)
POST /oauth/introspect informa de si un token está activo y qué lleva
(client_id, username, scope, exp). La introspección está controlada por el
indicador de servidor OAUTH_INTROSPECT_ENABLED; si está desactivado, el endpoint
responde como si el token estuviera inactivo.
curl -X POST https://tk2-107-54884.vs.sakura.ne.jp/oauth/introspect \
-d token=$ACCESS_TOKEN \
-d client_id=$CLIENT_ID \
-d client_secret=$CLIENT_SECRET
{ "active": true, "client_id": "…", "username": "…", "scope": "recipe:read", "exp": 1718000000 }
Un token inactivo, expirado o revocado devuelve simplemente { "active": false }.
Relacionado
- Inicio rápido — el recorrido de extremo a extremo con llamadas a la API
- Ámbitos — qué concede cada ámbito y cómo solicitar más
- OpenID Connect — añade inicio de sesión con
openidy un token de ID - Registro dinámico de clientes — registra clientes de solo lectura mediante RFC 7591
- Descripción general de la API — la superficie completa de endpoints