
# 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

```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.

```ts
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.

```python
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

```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

```ts
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

```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`):

- Un token vinculado con DPoP enviado con el esquema `Bearer` se rechaza con `invalid_token`, aunque se adjunte una prueba válida (RFC 9449 §7.2).
- Un token enviado con el esquema `DPoP` debe estar vinculado con DPoP y venir con una prueba válida (RFC 9449 §7.1).

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

```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

```ts
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

```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

- **Emisor y claves:** el `iss` del token debe ser igual al emisor configurado. Las claves se obtienen únicamente del `jwks_uri` del documento de descubrimiento de ese emisor, y el `kid` se busca solo en ese conjunto de claves. Un token firmado con la clave de otro entorno se rechaza.
- **Algoritmo:** solo RS256, PS256, ES256 y EdDSA. El `alg` de la clave debe ser igual al `alg` del token; `none` y los algoritmos HMAC siempre se rechazan.
- **Tokens de acceso:** `typ` debe ser `at+jwt` (RFC 9068), `aud` debe contener la audiencia configurada, y `sub` y `client_id` deben estar presentes.
- **Tokens de ID:** `aud` debe contener tu ID de cliente, `azp` debe ser igual a él cuando esté presente, y `nonce` y `max_age` se comprueban cuando los pasas.
- **Tiempo:** `exp` es obligatorio; `nbf` e `iat` no pueden estar en el futuro. Se permite un desfase del reloj de como máximo 60 segundos.
- **DPoP:** cuando el token está vinculado a una clave (`cnf.jkt`), se requiere una prueba DPoP válida (RFC 9449). Las pruebas reutilizadas se rechazan. Con la opción de esquema, un token vinculado nunca se acepta como token bearer; con "requerir DPoP", solo se aceptan tokens vinculados.
- **Caché de claves:** las claves se almacenan en caché durante el `max-age` del servidor (300 segundos por defecto, como máximo 600). Un `kid` desconocido activa como máximo una nueva descarga por minuto.

## 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](/docs/sdk/testing). Genera tokens de prueba que solo acepta un verificador en modo de prueba explícito; un verificador de producción siempre los rechaza.
