
# Servidor mock local

`keenauth mock serve` ejecuta en su máquina un pequeño servidor de OpenID Connect, SAML 2.0 y SCIM 2.0, y una versión en memoria de la API de gestión `/v1`, para trabajos de CI sin acceso a la red y para el desarrollo local. No necesita un entorno de KeenAuth, ninguna clave de API ni Internet. Todos los datos viven en memoria y se eliminan cuando el proceso termina.

El servidor mock no es KeenAuth. Implementa solo los flujos que se indican abajo, aprueba automáticamente cada solicitud y comprueba bastante menos que el servicio real. **Su entorno `development` alojado es la única referencia para el comportamiento real**: ejecute sus pruebas de extremo a extremo contra él antes de publicar.

## Iniciar el servidor

```sh
keenauth mock serve
```

De forma predeterminada, el servidor escucha en `127.0.0.1:4010`, una dirección de bucle local a la que otras máquinas no pueden acceder. `--port` cambia el puerto (`0` elige uno libre). `--host` cambia la dirección; cualquier dirección que no sea de bucle local expone el servidor mock a su red, y la CLI muestra una advertencia. Detenga el servidor con Ctrl+C.

El comando no lee credenciales y no envía nada a KeenAuth.

## Endpoints

| Ruta | Función |
|---|---|
| `/.well-known/openid-configuration` | Documento de descubrimiento OIDC (también servido en `/.well-known/oauth-authorization-server`) |
| `/.well-known/jwks.json` | Conjunto de claves públicas de prueba (ID de clave `keenauth-test-1`) |
| `/oauth2/authorize` | Endpoint de autorización: flujo de código de autorización con PKCE (`S256`) |
| `/oauth2/token` | Endpoint de token: concesiones `authorization_code` y `refresh_token` |
| `/oauth2/userinfo` | Endpoint UserInfo (token de acceso Bearer con el scope `openid`) |
| `/saml-idp/<application_id>/metadata` | Metadatos del proveedor de identidad SAML 2.0 |
| `/saml-idp/<application_id>/sso` | Endpoint de inicio de sesión único SAML (bindings HTTP-Redirect y HTTP-POST) |
| `/scim/v2/<directory_id>/…` | Endpoints SCIM 2.0: `Users`, `Groups`, `ServiceProviderConfig`, `ResourceTypes`, `Schemas` |
| `/v1/…` | API de gestión en memoria: cada operación del documento OpenAPI de la API de gestión |

Las URL de los endpoints del documento de descubrimiento y de los metadatos SAML apuntan a la dirección local en la que escucha el servidor. `<application_id>` y `<directory_id>` son nombres que usted elige (de 1 a 64 caracteres de `A-Z`, `a-z`, `0-9`, `-`, `_`); no hace falta registrar nada antes.

## Cómo se comporta

- **Sin pantalla de consentimiento ni página de inicio de sesión.** Cada solicitud de autorización válida se aprueba de inmediato para un usuario de prueba fijo y ficticio, y se redirige de vuelta con `code`, `state` e `iss`. El `sub` del usuario es `usr_01m3vnbwg0e8mbe7h7xxp6rmkc`, la dirección de correo `test.user@example.test` (verificada) y el nombre `Test User`.
- **Sin clientes registrados.** Se acepta cualquier `client_id`. Los secretos de cliente no se comprueban. `redirect_uri` debe ser una URI `https` absoluta, o una URI `http` en un host de bucle local (`127.0.0.1`, `[::1]`, `localhost`), sin fragmento; una `redirect_uri` no válida o un `client_id` ausente devuelve una página de error en lugar de una redirección.
- **PKCE es obligatorio.** Hay que enviar `code_challenge` con `code_challenge_method=S256`; `plain` se rechaza. La solicitud de token debe enviar el `code_verifier` correspondiente.
- **Los códigos son de un solo uso** y caducan a los 60 segundos. Presentar un código por segunda vez falla con `invalid_grant` y revoca todos los tokens emitidos a partir de él.
- **Tokens.** El token de acceso es un JWT según RFC 9068 (`typ` `at+jwt`), válido durante una hora, con `sub`, `client_id`, `scope` y un `jti` aleatorio. Su audiencia es el parámetro `resource` de la solicitud de autorización, o `https://test.keenauth.invalid/oauth2/userinfo` si no se envió ninguno. Con el scope `openid` también obtiene un token de ID (con `nonce` y `auth_time`; las claims `email` y `name` con los scopes `email` y `profile`). Con `offline_access` obtiene un token de actualización.
- **Los tokens de actualización rotan.** Cada actualización devuelve un token de actualización nuevo; presentar uno antiguo falla y revoca toda la cadena. Una actualización puede reducir el scope, pero no ampliarlo.
- **UserInfo** acepta solo tokens de acceso emitidos por este proceso, que no hayan caducado ni sido revocados, y que lleven el scope `openid`.

## Tokens y su verificador

Los tokens se firman con la clave pública de prueba del [kit de pruebas sin conexión](/docs/sdk/testing) y llevan el emisor `https://test.keenauth.invalid`, no la dirección local. El dominio de primer nivel `.invalid` está reservado, así que un verificador de producción nunca puede aceptar estos tokens: los verificadores del SDK solo los aceptan en modo de prueba, que usted activa explícitamente en el código de prueba.

```go
// Go: accept tokens from the mock server in a test.
v, err := keenauthtest.NewVerifier(verify.Config{Audience: "https://test.keenauth.invalid/oauth2/userinfo"})
if err != nil {
	t.Fatal(err)
}
claims, err := v.VerifyAccessToken(ctx, accessToken, nil)
```

Use el mismo modo de prueba en TypeScript (`createTestVerifier`) y en Python (`keenauth.testing.verifier`). Las bibliotecas genéricas de cliente OIDC que exigen que el `issuer` del documento de descubrimiento sea igual a la URL desde la que se obtuvo rechazarán el documento del servidor mock; configúrelas con las URL de los endpoints directamente, o pruébelas contra su entorno `development`.

## Ejemplo: el flujo completo con curl

El ejemplo necesita `curl`, `openssl` y `jq`. Inicie primero el servidor en otra terminal.

```sh
BASE=http://127.0.0.1:4010
REDIRECT=http://127.0.0.1:8080/callback
VERIFIER=$(openssl rand -hex 32)
CHALLENGE=$(printf '%s' "$VERIFIER" | openssl dgst -sha256 -binary | openssl base64 -A | tr '+/' '-_' | tr -d '=')

# 1. Authorize: the server answers with a redirect that carries the code.
LOCATION=$(curl -s -o /dev/null -w '%{redirect_url}' --get "$BASE/oauth2/authorize" \
  --data-urlencode response_type=code --data-urlencode client_id=app_local \
  --data-urlencode redirect_uri="$REDIRECT" --data-urlencode 'scope=openid email offline_access' \
  --data-urlencode state=xyz --data-urlencode code_challenge="$CHALLENGE" \
  --data-urlencode code_challenge_method=S256)
CODE=$(printf '%s' "$LOCATION" | sed -E 's/.*[?&]code=([^&]+).*/\1/')

# 2. Exchange the code for tokens.
TOKENS=$(curl -s "$BASE/oauth2/token" -d grant_type=authorization_code -d client_id=app_local \
  --data-urlencode code="$CODE" --data-urlencode redirect_uri="$REDIRECT" -d code_verifier="$VERIFIER")

# 3. Call UserInfo with the access token.
curl -s "$BASE/oauth2/userinfo" -H "Authorization: Bearer $(printf '%s' "$TOKENS" | jq -r .access_token)"

# 4. Refresh.
curl -s "$BASE/oauth2/token" -d grant_type=refresh_token -d client_id=app_local \
  --data-urlencode refresh_token="$(printf '%s' "$TOKENS" | jq -r .refresh_token)"
```

El paso 3 muestra el usuario de prueba:

```json
{"email":"test.user@example.test","email_verified":true,"sub":"usr_01m3vnbwg0e8mbe7h7xxp6rmkc"}
```

## Proveedor de identidad SAML

Use el servidor mock como proveedor de identidad de un proveedor de servicios SAML que esté desarrollando: apunte el proveedor de servicios a la URL de metadatos, inicie sesión y el servidor mock responderá con una respuesta firmada para el usuario de prueba. Las rutas siguen al proveedor de identidad SAML alojado que KeenAuth planea para una versión posterior.

- **Metadatos.** `GET /saml-idp/<application_id>/metadata` devuelve un `EntityDescriptor` con el ID de entidad `https://test.keenauth.invalid/saml-idp/<application_id>`, el certificado de firma, los formatos de NameID `persistent` y `emailAddress`, y el endpoint SSO para ambos bindings. No se exigen AuthnRequest firmadas (`WantAuthnRequestsSigned="false"`); una firma en la solicitud se ignora.
- **SSO.** Envíe la AuthnRequest con el binding HTTP-Redirect (`GET`, DEFLATE + base64 en `SAMLRequest`) o con el binding HTTP-POST (`POST`, base64). No hay página de inicio de sesión: una solicitud válida se aprueba de inmediato para el usuario de prueba y usted recibe una página HTML que envía por sí sola `SAMLResponse` (y su `RelayState`, de como máximo 80 bytes) a la URL ACS del proveedor de servicios.
- **La AuthnRequest debe incluir** `ID`, `Version="2.0"`, `IssueInstant`, un `saml:Issuer` (el ID de entidad de su proveedor de servicios) y `AssertionConsumerServiceURL`, porque el servidor mock no tiene proveedores de servicios registrados. La URL ACS debe ser una URL `https` absoluta, o una URL `http` en un host de bucle local, sin fragmento. `ProtocolBinding`, si está presente, debe ser HTTP-POST; `Destination`, si está presente, debe ser la URL de SSO. Cualquier otra solicitud recibe `400` y no se envía a ningún sitio. Los DTD se rechazan.
- **La respuesta** se firma dos veces con RSA-SHA256 y canonicalización exclusiva: la aserción y, después, la respuesta completa. `InResponseTo` es el `ID` de su solicitud, `Destination` y el `Recipient` de la confirmación del sujeto son la URL ACS, el `Audience` es el `Issuer` de su solicitud, y la aserción es válida desde el momento de su emisión durante 5 minutos. El NameID es `persistent` (un valor estable por aplicación) salvo que la solicitud pida `emailAddress`. Los atributos son `email`, `given_name`, `family_name` y `name`.

**La clave de firma es pública.** Se deriva de una etiqueta publicada, así que cualquiera puede firmar respuestas que este certificado verifica. Su huella SHA-256 es `fcc2fc37b9ddb800721283156f9eb26ff1b80f099d996a65650f8cb469a9be81` y su subject dice `TEST ONLY`. Regístrela solo en configuraciones de desarrollo y prueba de su proveedor de servicios, nunca en producción. KeenAuth lo aplica a sus propias conexiones SAML: en entornos de staging y producción rechaza esta clave y cualquier ID de entidad bajo `.invalid`, al importar metadatos o añadir un certificado, y de nuevo al iniciar sesión; los entornos de desarrollo las aceptan.

### Ejemplo: un inicio de sesión SAML con curl

El ejemplo necesita `curl` y `openssl`. Inicie primero el servidor en otra terminal.

```sh
BASE=http://127.0.0.1:4010
APP=app_local
ACS=http://127.0.0.1:8080/saml/acs

# 1. IdP metadata: entity ID, signing certificate and SSO URL for your service provider.
curl -s "$BASE/saml-idp/$APP/metadata" -o idp-metadata.xml

# 2. Send an AuthnRequest with the HTTP-POST binding (base64, no compression).
NOW=$(date -u +%Y-%m-%dT%H:%M:%SZ)
REQUEST=$(printf '<samlp:AuthnRequest xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="_req1" Version="2.0" IssueInstant="%s" AssertionConsumerServiceURL="%s"><saml:Issuer>https://sp.example.com</saml:Issuer></samlp:AuthnRequest>' "$NOW" "$ACS" | openssl base64 -A)
curl -s "$BASE/saml-idp/$APP/sso" --data-urlencode SAMLRequest="$REQUEST" --data-urlencode RelayState=xyz -o form.html

# 3. The page posts SAMLResponse to the ACS URL by itself; decode it to look at the signed response.
sed -n 's/.*name="SAMLResponse" value="\([^"]*\)".*/\1/p' form.html | openssl base64 -d -A > response.xml
grep -o 'InResponseTo="[^"]*"' response.xml | head -1
```

El último comando muestra `InResponseTo="_req1"`. Su proveedor de servicios valida `response.xml` con el certificado de `idp-metadata.xml`.

## Endpoints SCIM 2.0

Use los endpoints SCIM para probar un cliente SCIM, como un conector de aprovisionamiento o un script que crea usuarios y grupos, sin red. Cada `<directory_id>` es un directorio separado y vacío que vive mientras dure el proceso.

- **Autenticación.** Cada solicitud necesita `Authorization: Bearer keenauth-mock-scim-token`. Cualquier otra cosa recibe `401` con un desafío `WWW-Authenticate: Bearer`. El valor es fijo y público; no es un token SCIM de KeenAuth.
- **Usuarios y grupos.** `POST`, `GET`, `PUT`, `PATCH` y `DELETE` sobre `/Users` y `/Groups` (RFC 7644). `userName` y el `displayName` del grupo son obligatorios y únicos en el directorio, sin distinguir mayúsculas; un duplicado recibe `409` con `scimType` `uniqueness`. Los miembros de un grupo deben ser usuarios del mismo directorio. Eliminar un usuario lo quita de sus grupos. `password` se acepta y se descarta.
- **Filtros y páginas.** `filter` admite `eq`, `ne`, `co`, `sw`, `ew` y `pr` unidos con `and` (por ejemplo `userName eq "ada@example.test"`), además de `startIndex` y `count` (como máximo 200). `attributes` y `excludedAttributes` seleccionan atributos de primer nivel. `POST /Users/.search` y `/Groups/.search` aceptan los mismos parámetros en el cuerpo.
- **PATCH** admite `add`, `remove` y `replace` con rutas como `active`, `name.givenName`, `emails[type eq "work"].value`, `members[value eq "<id>"]` y atributos de la extensión enterprise; las operaciones de una solicitud se aplican todas o ninguna.
- **Versiones.** Cada recurso lleva `meta.version` y una cabecera `ETag` (`W/"1"`, `W/"2"`, …). `If-Match` en `PUT`, `PATCH` y `DELETE` devuelve `412` cuando la versión ha cambiado; `If-None-Match` en `GET` devuelve `304`.
- **Descubrimiento.** `ServiceProviderConfig`, `ResourceTypes` y `Schemas` describen exactamente este comportamiento. `/Bulk` y `/Me` devuelven `501`, como también en KeenAuth.

### Ejemplo: aprovisionar un usuario con curl

El ejemplo necesita `curl` y `jq`. Inicie primero el servidor en otra terminal.

```sh
SCIM=http://127.0.0.1:4010/scim/v2/dir_local
AUTH='Authorization: Bearer keenauth-mock-scim-token'
CT='Content-Type: application/scim+json'

# 1. Create a user; the response carries the id, meta.version and an ETag header.
USER_ID=$(curl -s "$SCIM/Users" -H "$AUTH" -H "$CT" -d '{
  "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
  "userName": "ada@example.test",
  "name": {"givenName": "Ada", "familyName": "Lovelace"},
  "emails": [{"value": "ada@example.test", "type": "work", "primary": true}]
}' | jq -r .id)

# 2. Find it with a filter.
curl -s -G "$SCIM/Users" -H "$AUTH" --data-urlencode 'filter=userName eq "ada@example.test"' | jq .totalResults

# 3. Deactivate it with PATCH, guarded by the version you read.
curl -s -X PATCH "$SCIM/Users/$USER_ID" -H "$AUTH" -H "$CT" -H 'If-Match: W/"1"' -d '{
  "schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
  "Operations": [{"op": "replace", "path": "active", "value": false}]
}' | jq -c '{active, version: .meta.version}'

# 4. Create a group with the user as a member, then delete the user.
curl -s "$SCIM/Groups" -H "$AUTH" -H "$CT" -d '{
  "schemas": ["urn:ietf:params:scim:schemas:core:2.0:Group"],
  "displayName": "Engineering",
  "members": [{"value": "'"$USER_ID"'"}]
}' | jq -c '.members[0] | {display, type}'
curl -s -o /dev/null -w '%{http_code}\n' -X DELETE "$SCIM/Users/$USER_ID" -H "$AUTH"
```

Los comandos muestran `1`, `{"active":false,"version":"W/\"2\""}`, `{"display":"ada@example.test","type":"User"}` y `204`.

## API de gestión (`/v1`)

Use los endpoints de la API de gestión para probar en CI, sin red, el código que llama a la API de gestión de KeenAuth (un SDK, la CLI, un script de despliegue). Las rutas son las alojadas: use la URL base `http://127.0.0.1:4010/v1` en lugar de la alojada y nada más cambia. Los endpoints se generan a partir del documento OpenAPI de la API de gestión incluido en la CLI, así que responde cada operación de esa versión de la API.

- **Autenticación.** Cada solicitud necesita `Authorization: Bearer keenauth-mock-management-token`. Cualquier otra cosa recibe `401` con un desafío `WWW-Authenticate: Bearer`, también en rutas desconocidas. El valor es fijo y público; no es una clave de gestión de KeenAuth.
- **Respuestas.** Cada operación responde con el código de éxito que declara el documento (`200`, `201`, `202`, `204`; las dos operaciones de descarga responden `302` con un `Location` que no se resuelve). El cuerpo es el `example` de la respuesta de éxito del documento o, mientras no lo haya, el ejemplo válido más pequeño construido a partir del esquema de la respuesta: solo los campos obligatorios, los campos nulables en `null`, las enumeraciones con su primer valor y las cadenas que cumplen su `pattern`.
- **Estado sencillo.** Una colección en la que puede crear registros (`POST /v1/users`, `/v1/organizations`, `/v1/applications`, `/v1/organizations/<org_id>/roles`, …) los conserva: el registro creado lo devuelve `GET` en su propia ruta y en la lista, `PATCH` y `PUT` lo modifican, `DELETE` lo elimina junto con todo lo que hay debajo, y una ruta bajo un registro que no existe es `404`. Los identificadores tienen la forma de prefijo de KeenAuth (`usr_…`, `org_…`, `app_…`) y cumplen el patrón del documento; cuando el documento no da ninguno, el prefijo se deriva del nombre del parámetro. Los ajustes que existen una sola vez por entorno (por ejemplo `/v1/identity-settings`) conservan lo que escribió con `PATCH` o `PUT`. Los recursos que no se pueden crear por la API (por ejemplo sesiones e informes) responden siempre con el ejemplo.
- **Listas.** Funcionan `limit` (de 1 a 100, por defecto 20), `after` o `before` con un identificador de registro como cursor, y `order` (`desc` por defecto, el más reciente primero); la respuesta incluye `list_metadata`. Los filtros como `status` no se aplican.
- **Los errores** son `application/problem+json` como en el documento, con `code` y un `request_id` que también aparece en la cabecera `Idp-Request-Id`. Una ruta desconocida es `404 not_found`, un método que la ruta no tiene es `405 method_not_allowed` con una cabecera `Allow`, un cuerpo que no es JSON válido (o no es un objeto) es `400 malformed_request`, un `Content-Type` distinto de JSON es `415` en las operaciones que lo listan, un campo obligatorio ausente es `422 validation_failed` con `errors[]`, un cuerpo de más de 1 MiB es `413`, y un `limit` o un cursor no válido es `400`.

**En qué se diferencia del entorno `development`.** El servidor mock solo valida que el cuerpo sea un objeto JSON con los campos obligatorios; no se comprueban los formatos de los campos, las longitudes, las enumeraciones ni la unicidad, y un campo con el tipo equivocado se ignora. Ignora `If-Match`, `Idempotency-Key` y el resto de cabeceras y no devuelve `ETag`. No comprueba permisos, límites de plan, cuotas, límites de frecuencia ni la región, no enmascara datos personales, no envía correos, no ejecuta trabajos (un trabajo aceptado permanece como se creó) y tiene un solo entorno. Los registros existen solo mientras dure el proceso, como máximo 10.000. Use su entorno `development` para saber cómo se comporta la API real.

### Ejemplo: crear, leer, modificar y eliminar con curl

El ejemplo necesita `curl` y `jq`. Inicie primero el servidor en otra terminal.

```sh
API=http://127.0.0.1:4010/v1
AUTH='Authorization: Bearer keenauth-mock-management-token'
CT='Content-Type: application/json'

# 1. Create an organization.
ORG_ID=$(curl -s "$API/organizations" -H "$AUTH" -H "$CT" -d '{"name": "Acme"}' | jq -r .id)

# 2. Read it back and list.
curl -s "$API/organizations/$ORG_ID" -H "$AUTH" | jq -c '{id: (.id | .[0:4]), name}'
curl -s "$API/organizations" -H "$AUTH" | jq -c '{count: (.data | length), list_metadata}'

# 3. Rename it with a merge patch.
curl -s -X PATCH "$API/organizations/$ORG_ID" -H "$AUTH" -H 'Content-Type: application/merge-patch+json' -d '{"name": "Acme Inc"}' | jq -r .name

# 4. Delete it; a second read is a 404 problem.
curl -s -o /dev/null -w '%{http_code}\n' -X DELETE "$API/organizations/$ORG_ID" -H "$AUTH"
curl -s "$API/organizations/$ORG_ID" -H "$AUTH" | jq -c '{status, code}'

# 5. Without the token.
curl -s "$API/organizations" | jq -c '{status, code}'
```

Los comandos muestran `{"id":"org_","name":"Acme"}`, `{"count":1,"list_metadata":{"after":null,"before":null}}`, `Acme Inc`, `204`, `{"status":404,"code":"not_found"}` y `{"status":401,"code":"unauthenticated"}`.

## No compatible

No hay reloj de pruebas, ni DPoP, ni PAR, ni objetos de solicitud, ni flujo de dispositivo, ni client credentials, ni intercambio de tokens, ni cierre de sesión o revocación, ni páginas alojadas. El proveedor de identidad SAML no tiene cierre de sesión único, ni aserciones cifradas, ni binding de artefacto, ni inicio de sesión iniciado por el IdP. Los endpoints SCIM no tienen operaciones masivas, ordenación, `/Me` ni grupos anidados, y el filtro no admite `or`, `not`, paréntesis ni `gt`/`ge`/`lt`/`le`. Use su entorno `development` para probarlos.
