Endpoints de prueba para cada esquema de autenticación de API
Basic, Digest, tokens bearer, claves de API, firmas HMAC, JWT, OAuth 1.0a y OAuth 2.0 con PKCE, con credenciales.
Ejemplos ejecutados por última vez contra la API el
Para probar la autenticación de un cliente de API sin montar un proveedor de identidad, usa los endpoints de prueba públicos de https://api.testingapis.com/api/authinicia sesión para verlo. Hay un endpoint por esquema: HTTP Basic, Digest, tokens bearer fijos, claves de API en cabecera, query o cookie, y firma HMAC de peticiones. Un login JWT tiene refresh tokens y un token caducado para pruebas negativas, un servidor OAuth 2.0 admite PKCE y client credentials, y un servidor OAuth 1.0a comprueba peticiones firmadas. Las credenciales son fijas y públicas, están abajo y en GET /api/authinicia sesión para verlo. Unas credenciales incorrectas devuelven un 401 como es debido, y un token válido sin el rol adecuado devuelve un 403.
Endpoints y credenciales
| Esquema | Endpoint | Credenciales |
|---|---|---|
| HTTP Basic | GET /api/auth/basicinicia sesión para verlo | apibee / password123 |
| HTTP Digest (MD5, qop=auth) | GET /api/auth/digestinicia sesión para verlo | apibee / password123 |
| Token bearer | GET /api/auth/bearerinicia sesión para verlo | Authorization: Bearer apibee-token-123 |
| Clave de API, cabecera | GET /api/auth/api-key/headerinicia sesión para verlo | X-API-Key: apibee-key-123 |
| Clave de API, query | GET /api/auth/api-key/queryinicia sesión para verlo | ?api_key=apibee-key-123 |
| Clave de API, cookie | GET /api/auth/api-key/cookieinicia sesión para verlo | Cookie: api_key=apibee-key-123 |
| Firma HMAC-SHA256 | POST /api/auth/hmacinicia sesión para verlo | Secreto apibee-hmac-secret |
| Roles y scopes | GET /api/auth/roles/admininicia sesión para verlo | admin-token, user-token, readonly-token |
| JWT | POST /api/auth/jwt/logininicia sesión para verlo | admin / admin123, user / user123 |
| OAuth 2.0 | /api/auth/oauth/*inicia sesión para verlo | apibee-client / apibee-secret, cliente público apibee-public |
| OAuth 1.0a | /api/auth/oauth1/*inicia sesión para verlo | Consumer apibee-consumer-key / apibee-consumer-secret, token apibee-access-token / apibee-token-secret |
Basic y Digest
Una contraseña que falta o es incorrecta devuelve 401 con una cabecera WWW-Authenticate, que es lo que hace que los navegadores y las librerías HTTP pidan credenciales o reintenten con ellas:
$ curl -i -u apibee:password123 https://api.testingapis.com/api/auth/basic
HTTP/1.1 200 OK
{"authenticated":true,"scheme":"basic","user":"apibee"}
$ curl -i https://api.testingapis.com/api/auth/basic
HTTP/1.1 401 Unauthorized
www-authenticate: Basic realm="apibee", charset="UTF-8"
{"status":401,"error":"Unauthorized","message":"Missing or malformed Basic Authorization header."}Digest necesita dos idas y vueltas. El primer 401 trae un nonce, y el cliente responde con un hash de la contraseña en lugar de la propia contraseña. --digest de curl hace las dos cosas:
$ curl -i --digest -u apibee:password123 https://api.testingapis.com/api/auth/digest
HTTP/1.1 401 Unauthorized
www-authenticate: Digest realm="apibee", qop="auth", algorithm=MD5, nonce="MTc5...", opaque="069d..."
HTTP/1.1 200 OK
{"authenticated":true,"scheme":"digest","user":"apibee","qop":"auth","nc":"00000001"}Para probar un cliente que tiene que lidiar con servidores que ocultan recursos protegidos, /api/auth/basic-hidden/{user}/{pass}inicia sesión para verlo devuelve 404 en lugar de 401 cuando las credenciales no coinciden.
Tokens bearer y claves de API
Los tres endpoints de clave de API aceptan la misma clave en sitios distintos, así que puedes comprobar que tu cliente la pone donde la API la espera. Un token bearer incorrecto devuelve WWW-Authenticate: Bearer error="invalid_token".
/api/auth/api-key/header401 frente a 403
401 significa que el servidor no sabe quién eres: no hay credenciales, o son malas. 403 significa que sabe perfectamente quién eres y que no tienes permiso. Un cliente debe tratarlos de forma distinta. Tras un 401 puede volver a iniciar sesión o renovar un token. Tras un 403, volver a iniciar sesión no cambia nada. Los endpoints de roles devuelven cada uno:
$ curl -i https://api.testingapis.com/api/auth/roles/admin
HTTP/1.1 401 Unauthorized
www-authenticate: Bearer realm="apibee"
$ curl -i -H "Authorization: Bearer user-token" https://api.testingapis.com/api/auth/roles/admin
HTTP/1.1 403 Forbidden
{"status":403,"error":"Forbidden","message":"This endpoint requires the 'admin' role.","roles":["user"],"scopes":["read","write"]}
$ curl -H "Authorization: Bearer admin-token" https://api.testingapis.com/api/auth/roles/admin
{"authorized":true,"user":"Alice Admin","roles":["admin","user"],"scopes":["read","write","delete"]}DELETE /api/auth/roles/resourceinicia sesión para verlo comprueba un scope en lugar de un rol. readonly-token solo tiene read, así que recibe un 403 que nombra el scope delete que le falta.
Firma HMAC de peticiones
Las APIs de pagos y webhooks suelen firmar las peticiones en lugar de enviar un secreto. Aquí la firma es el HMAC-SHA256 en hexadecimal y minúsculas de timestamp + "." + raw body, enviado en X-Signature con el timestamp en X-Timestamp. Firma exactamente los bytes que envías. Volver a serializar el JSON después de firmar es la forma más habitual de romperla.
import { createHmac } from "node:crypto";
const secret = "apibee-hmac-secret";
const body = JSON.stringify({ orderId: 1001, amount: 49.99 });
const timestamp = Math.floor(Date.now() / 1000).toString();
const signature = createHmac("sha256", secret)
.update(`${timestamp}.${body}`)
.digest("hex");
const res = await fetch("https://api.testingapis.com/api/auth/hmac", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Timestamp": timestamp,
"X-Signature": signature,
},
body,
});
console.log(res.status, await res.json());200 { authenticated: true, scheme: 'hmac-sha256', timestamp: 1791341858, bodyLength: 31 }
# The same signature sent with a different body:
401 {
status: 401,
error: 'Unauthorized',
message: 'Signature mismatch.',
hint: 'X-Signature = lowercase hex HMAC-SHA256(secret, X-Timestamp + "." + raw request body). Optional \'sha256=\' prefix.',
bodyLength: 30
}Para comprobar una firma a mano, el generador de hashes calcula HMAC-SHA256 en el navegador.
Login JWT, caducidad y renovación
/api/auth/jwt/logininicia sesión para verlo devuelve un access token firmado que dura 15 minutos y un refresh token. Los refresh tokens rotan: cada renovación devuelve uno nuevo y el anterior deja de funcionar. /api/auth/jwt/expiredinicia sesión para verlo entrega un token bien firmado que ya ha caducado, para probar el camino de la caducidad sin esperar 15 minutos.
const BASE = "https://api.testingapis.com/api/auth/jwt";
const login = await fetch(`${BASE}/login`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ username: "user", password: "user123" }),
});
const tokens = await login.json();
// { accessToken, tokenType: "Bearer", expiresIn: 900, refreshToken, user }
const auth = { Authorization: `Bearer ${tokens.accessToken}` };
console.log((await fetch(`${BASE}/me`, { headers: auth })).status); // 200
console.log((await fetch(`${BASE}/admin`, { headers: auth })).status); // 403: role is "user"
// A correctly signed token that expired an hour ago, for the negative test.
const { accessToken: expired } = await (await fetch(`${BASE}/expired`)).json();
const old = await fetch(`${BASE}/me`, { headers: { Authorization: `Bearer ${expired}` } });
console.log(old.status, old.headers.get("www-authenticate"));
// 401 Bearer error="invalid_token", error_description="The token expired at '...'"
// Refresh tokens rotate: use the new one next time.
const refreshed = await fetch(`${BASE}/refresh`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ refreshToken: tokens.refreshToken }),
});
console.log(refreshed.status); // 200Pega cualquiera de estos tokens en el decodificador JWT para ver sus claims y su hora de caducidad.
OAuth 2.0 con PKCE
El servidor OAuth admite el grant authorization code con PKCE, client credentials, password y refresh token, además de introspección y revocación. La autorización se aprueba automáticamente como el usuario apibee, así que el flujo entero corre en un script. Las redirect URIs en localhost o 127.0.0.1 (cualquier puerto) y el callback de Postman reciben una redirección 302 real.
Authorization code con PKCE
import { createHash, randomBytes } from "node:crypto";
const BASE = "https://api.testingapis.com/api/auth/oauth";
const redirectUri = "http://localhost:3000/callback";
// 1. A random code verifier and its S256 challenge.
const verifier = randomBytes(32).toString("base64url");
const challenge = createHash("sha256").update(verifier).digest("base64url");
// 2. Send the user to /authorize. This server approves at once and redirects
// to the redirect URI with ?code=...&state=...
const authorize = new URL(`${BASE}/authorize`);
authorize.search = new URLSearchParams({
response_type: "code",
client_id: "apibee-public",
redirect_uri: redirectUri,
scope: "read profile",
state: "xyz123",
code_challenge: challenge,
code_challenge_method: "S256",
});
const redirect = await fetch(authorize, { redirect: "manual" });
const code = new URL(redirect.headers.get("location")).searchParams.get("code");
// 3. Exchange the code and the verifier for tokens.
const tokenRes = await fetch(`${BASE}/token`, {
method: "POST",
body: new URLSearchParams({
grant_type: "authorization_code",
client_id: "apibee-public",
code,
redirect_uri: redirectUri,
code_verifier: verifier,
}),
});
const tokens = await tokenRes.json();
// 4. Call a protected endpoint.
const me = await fetch(`${BASE}/userinfo`, {
headers: { Authorization: `Bearer ${tokens.access_token}` },
});
console.log(me.status, await me.json());200 {
sub: 'apibee',
name: 'TestingAPIs Tester',
preferred_username: 'apibee',
email: null,
email_verified: null
}Los dos fallos que debe manejar un cliente PKCE:
# Sending the same code a second time
400 { error: 'invalid_grant', error_description: 'Authorization code is invalid, expired or already used.' }
# Sending a code_verifier that doesn't match the challenge
400 { error: 'invalid_grant', error_description: 'PKCE verification failed: code_verifier does not match code_challenge.' }Client credentials
Para llamadas entre servicios, sin ningún usuario de por medio:
$ curl -X POST https://api.testingapis.com/api/auth/oauth/token \
-d "grant_type=client_credentials&client_id=apibee-client&client_secret=apibee-secret&scope=read"
{"access_token":"oat_3fc4...","token_type":"Bearer","expires_in":3600,"scope":"read"}Peticiones firmadas con OAuth 1.0a
OAuth 1.0a firma cada petición con dos secretos, el del consumer y el del token, en lugar de enviar un token sin más. El servidor de prueba acepta firmas HMAC-SHA1, HMAC-SHA256, HMAC-SHA512 y PLAINTEXT. Usa el access token fijo apibee-access-token / apibee-token-secret, que no caduca nunca, o ejecuta el flujo three-legged para obtener un token propio. La autorización se aprueba automáticamente como el usuario apibee.
POST /api/auth/oauth1/request_tokeninicia sesión para verlo, firmado solo con la clave y el secreto del consumer, conoauth_callbackpuesto a una URL o aoob.GET /api/auth/oauth1/authorize?oauth_token=…inicia sesión para verlo redirige a un callback de localhost o de Postman conoauth_verifier, y lo devuelve como JSON paraoob.POST /api/auth/oauth1/access_tokeninicia sesión para verlo, firmado con el request token y su secreto, másoauth_verifier. El access token dura una hora.
The protected resource, with the static access token:
1. New request: GET https://api.testingapis.com/api/auth/oauth1
2. Authorization → Type: OAuth 1.0
Signature Method: HMAC-SHA1
Consumer Key: apibee-consumer-key
Consumer Secret: apibee-consumer-secret
Access Token: apibee-access-token
Token Secret: apibee-token-secret
Add authorization data to: Request Headers
3. Send. The response has "authenticated": true.
The three-legged flow, with the same Authorization settings:
1. POST https://api.testingapis.com/api/auth/oauth1/request_token
Leave Access Token and Token Secret empty.
Advanced → Callback URL: oob
2. Open GET https://api.testingapis.com/api/auth/oauth1/authorize?oauth_token=<oauth_token from step 1>
and copy oauth_verifier from the JSON.
3. POST https://api.testingapis.com/api/auth/oauth1/access_token
Access Token: oauth_token from step 1
Token Secret: oauth_token_secret from step 1
Advanced → Verifier: oauth_verifier from step 2
4. Use the oauth_token and oauth_token_secret it returns as
Access Token and Token Secret for GET /api/auth/oauth1.PLAINTEXT, en la pestaña cURL, no necesita librería de firma: la firma es la propia clave de firma, consumer_secret&token_secret, con el & enviado como %26. Úsala para comprobar las credenciales y luego pasa a HMAC-SHA1 para probar tu código de firma.
Cuando una firma no coincide, el 401 incluye la cadena base de firma que construyó el servidor, para que la compares con la de tu cliente. Los fallos que merece la pena probar:
# Signed with the wrong token secret
401 signature_invalid: Signature mismatch.
details.signatureBaseString: GET&https%3A%2F%2Fapi.testingapis.com%2Fapi%2Fauth%2Foauth1&oauth_consumer_key%3D...
# The same oauth_nonce sent twice
401 nonce_used: This oauth_nonce was already used with this timestamp. Send a new nonce.
# The same request token exchanged twice
401 token_used: This request token was already exchanged for an access token.Las reglas de firma de OAuth 1.0a explican dónde van los parámetros, la ventana del timestamp y cada código oauth_problem.
Pruebas negativas que merece la pena escribir
- Sin credenciales: espera un 401 y una cabecera
WWW-Authenticate. - Contraseña incorrecta, clave incorrecta, token manipulado: 401, nunca 500.
- Usuario válido sin el rol o el scope: 403, y el cliente no cierra la sesión del usuario.
- JWT caducado: el cliente renueva una vez y luego reintenta la petición original.
- Un refresh token usado dos veces: el segundo uso falla.
- Un código OAuth usado dos veces, o con el verifier equivocado:
invalid_grant. - Una firma HMAC sobre otro cuerpo: 401.
- Una petición OAuth 1.0a con un nonce repetido o una firma mala: 401
nonce_usedosignature_invalid.
Los conceptos detrás de estas pruebas están en la lección de autenticación y autorización (en inglés).