Verificación de tokens en tu API

El verificador de tokens del SDK comprueba los tokens de acceso y los tokens de ID emitidos por tu entorno de KeenAuth. Úsalo en la API que proteges. Los tres SDK (Go, TypeScript y Python) lo incluyen, y todos siguen las mismas reglas y los mismos vectores de prueba.

Go

import "github.com/keenauth/keenauth-go/verify"

v, err := verify.New(verify.Config{
	Issuer:   "https://acme.eu.keenauth.app", // el emisor de tu entorno, exactamente
	Audience: "https://api.acme.example",     // el identificador de tu API
})
if err != nil {
	return err
}

// En tu manejador HTTP:
claims, err := v.VerifyAccessToken(r.Context(), token, &verify.DPoPInput{
	Proof:  r.Header.Get("DPoP"),
	Method: r.Method,
	URL:    "https://api.acme.example" + r.URL.Path,
})
if err != nil {
	w.Header().Set("WWW-Authenticate", `Bearer error="invalid_token"`)
	http.Error(w, "unauthorized", http.StatusUnauthorized)
	return
}
// claims.Subject, claims.ClientID, claims.Scope

Para los tokens de ID, establece ClientID en la configuración y llama a v.VerifyIDToken(ctx, idToken, verify.IDTokenOptions{Nonce: nonce}).

TypeScript

El verificador de TypeScript usa solo WebCrypto y fetch de la plataforma (Node.js 22 o posterior) y no tiene dependencias.

import { createVerifier, VerifyError } from "@keenauth/node";

const verifier = createVerifier({
  issuer: "https://acme.eu.keenauth.app", // el emisor de tu entorno, exactamente
  audience: "https://api.acme.example",   // el identificador de tu API
});

// En tu manejador HTTP:
try {
  const claims = await verifier.verifyAccessToken(token, {
    proof: req.get("DPoP") ?? "",
    method: req.method,
    url: "https://api.acme.example" + req.path,
  });
  // claims.subject, claims.clientId, claims.scope
} catch (e) {
  if (!(e instanceof VerifyError)) throw e;
  res.set("WWW-Authenticate", 'Bearer error="invalid_token"').status(401).send("unauthorized");
}

Para los tokens de ID, establece clientId en la configuración y llama a verifier.verifyIdToken(idToken, { nonce }).

Python

El verificador de Python necesita el paquete cryptography, que se instala con el extra verify: pip install "keenauth-python[verify]". Las llamadas son síncronas y seguras para hilos.

from keenauth.verify import DPoPInput, Verifier, VerifierConfig, VerifyError

verifier = Verifier(VerifierConfig(
    issuer="https://acme.eu.keenauth.app",  # el emisor de tu entorno, exactamente
    audience="https://api.acme.example",    # el identificador de tu API
))

# En tu manejador HTTP:
try:
    claims = verifier.verify_access_token(
        token,
        DPoPInput(
            proof=request.headers.get("DPoP", ""),
            method=request.method,
            url="https://api.acme.example" + request.path,
        ),
    )
    # claims.subject, claims.client_id, claims.scope
except VerifyError:
    return Response("unauthorized", status=401, headers={"WWW-Authenticate": 'Bearer error="invalid_token"'})

Para los tokens de ID, establece client_id en la configuración y llama a verifier.verify_id_token(id_token, nonce=nonce).

Step-up (RFC 9470)

Para proteger una operación sensible, exige un inicio de sesión más fuerte o más reciente en cada llamada. Pasa las referencias de clase de contexto de autenticación aceptables (acr) y la antigüedad máxima del inicio de sesión (auth_time). dpop de abajo es la entrada DPoP de los ejemplos anteriores (nil/undefined/None si no usas DPoP).

Go

age := 15 * time.Minute
claims, err := v.VerifyAccessToken(r.Context(), token, dpop, verify.AccessTokenOptions{
	AcceptableACR: []string{"urn:idp:acr:mfa", "urn:idp:acr:phr"},
	MaxAuthAge:    &age,
})
if challenge, ok := verify.StepUpChallenge(err); ok {
	w.Header().Set("WWW-Authenticate", challenge)
	http.Error(w, "step-up required", http.StatusUnauthorized)
	return
}

TypeScript

import { createVerifier, stepUpChallenge } from "@keenauth/node";

try {
  const claims = await verifier.verifyAccessToken(token, dpop, {
    acceptableAcr: ["urn:idp:acr:mfa", "urn:idp:acr:phr"],
    maxAuthAgeSeconds: 900,
  });
} catch (e) {
  const challenge = stepUpChallenge(e);
  if (challenge === undefined) throw e;
  res.set("WWW-Authenticate", challenge).status(401).send("step-up required");
}

Python

from datetime import timedelta
from keenauth.verify import VerifyError, step_up_challenge

try:
    claims = verifier.verify_access_token(
        token,
        dpop,
        acceptable_acr=["urn:idp:acr:mfa", "urn:idp:acr:phr"],
        max_auth_age=timedelta(minutes=15),
    )
except VerifyError as err:
    challenge = step_up_challenge(err)
    if challenge is None:
        raise
    return Response("step-up required", status=401, headers={"WWW-Authenticate": challenge})

Las comprobaciones se ejecutan después de todas las demás, así que un token no válido siempre devuelve invalid_token. Un token válido que no cumple el requisito recibe el código insufficient_user_authentication, y el error incluye los acr_values y max_age requeridos (solo los que hayas establecido). StepUpChallenge (Go), stepUpChallenge (TypeScript) y step_up_challenge (Python) lo convierten en el valor de la cabecera WWW-Authenticate de RFC 9470 §3, por ejemplo Bearer error="insufficient_user_authentication", error_description="authentication too old", max_age="900". Un cliente que entiende el desafío inicia una nueva solicitud de autorización con esos acr_values y max_age. acr debe ser igual exactamente a uno de los valores (distingue mayúsculas y minúsculas); auth_time es obligatorio cuando estableces una antigüedad máxima, y la tolerancia de desfase del reloj se aplica igual que en los tokens de ID. Los valores urn:idp:acr:1fa, urn:idp:acr:mfa y urn:idp:acr:phr son los que tu entorno publica en acr_values_supported.

DPoP y el esquema de Authorization (RFC 9449)

Un token vinculado con DPoP (cnf.jkt) no sirve sin la clave privada de su cliente, así que una copia robada no puede reproducirse. Dos reglas mantienen esta protección, y el verificador las aplica cuando pasas el esquema de la cabecera Authorization de la solicitud (Bearer o DPoP):

Para aceptar solo tokens con restricción de remitente en una API (recomendado para API sensibles), establece también "requerir DPoP": un token que no esté vinculado con DPoP recibe el código dpop_required. El asistente de desafío DPoP convierte cualquier error 401 en el valor WWW-Authenticate de RFC 9449 §7.1, por ejemplo DPoP error="invalid_token", error_description="a DPoP-bound access token is required", algs="ES256 EdDSA PS256 RS256".

Go

claims, err := v.VerifyAccessToken(r.Context(), token, dpop, verify.AccessTokenOptions{
	Scheme:      scheme, // "Bearer" o "DPoP", de la cabecera Authorization
	RequireDPoP: true,
})
if challenge, ok := verify.DPoPChallenge(err); ok {
	w.Header().Set("WWW-Authenticate", challenge)
	http.Error(w, "unauthorized", http.StatusUnauthorized)
	return
}

TypeScript

import { dpopChallenge } from "@keenauth/node";

try {
  const claims = await verifier.verifyAccessToken(token, dpop, { scheme, requireDPoP: true });
} catch (e) {
  const challenge = dpopChallenge(e);
  if (challenge === undefined) throw e;
  res.set("WWW-Authenticate", challenge).status(401).send("unauthorized");
}

Python

from keenauth.verify import VerifyError, dpop_challenge

try:
    claims = verifier.verify_access_token(token, dpop, scheme=scheme, require_dpop=True)
except VerifyError as err:
    challenge = dpop_challenge(err)
    if challenge is None:
        raise
    return Response("unauthorized", status=401, headers={"WWW-Authenticate": challenge})

El middleware del servidor de recursos (Go protect, TypeScript @keenauth/node/middleware, Python keenauth.middleware) lee el esquema por ti. Su ajuste "requerir DPoP" (por API o por ruta) responde a una solicitud Bearer solo con 401 y el desafío DPoP.

Qué comprueba el verificador

Errores

Cada error tiene un Code. El mensaje nunca contiene el token ni sus claims.

Código Significado Responde con
invalid_token El token no es válido, ha caducado o no es para esta API. 401
jwks_unavailable No se pudieron obtener las claves del emisor y no hay ninguna en caché. 503
dpop_invalid La prueba DPoP falta o no es válida. 401 con WWW-Authenticate del asistente de desafío DPoP
dpop_required Requieres DPoP y el token no está vinculado con DPoP. 401 con WWW-Authenticate del asistente de desafío DPoP
insufficient_user_authentication El token es válido, pero su acr o auth_time no cumple tu requisito (RFC 9470). 401 con WWW-Authenticate del asistente de step-up
config_invalid La configuración del verificador es incorrecta. Corrige la configuración.

Si usas otra biblioteca JWT

Aplica la misma lista de comprobación: vincula el emisor a su documento de descubrimiento, toma las claves solo de ese jwks_uri, exige que el alg de la clave sea igual al alg del token, y rechaza none y los algoritmos HMAC.

Pruebas sin conexión

Para pruebas unitarias y CI, usa el kit de pruebas sin conexión. Genera tokens de prueba que solo acepta un verificador en modo de prueba explícito; un verificador de producción siempre los rechaza.