
# Server mock locale

`keenauth mock serve` avvia sulla tua macchina un piccolo server OpenID Connect, SAML 2.0 e SCIM 2.0, e una versione in memoria dell'API di gestione `/v1`, per i job CI senza accesso di rete e per lo sviluppo locale. Non richiede un ambiente KeenAuth, una chiave API né Internet. Tutti i dati risiedono in memoria e vengono eliminati quando il processo termina.

Il server mock non è KeenAuth. Implementa solo i flussi elencati qui sotto, approva automaticamente ogni richiesta e verifica molto meno del servizio reale. **Il tuo ambiente `development` ospitato è l'unico riferimento per il comportamento reale**: esegui i test end-to-end contro di esso prima di rilasciare.

## Avviare il server

```sh
keenauth mock serve
```

Per impostazione predefinita il server ascolta su `127.0.0.1:4010`, un indirizzo loopback che altre macchine non possono raggiungere. `--port` cambia la porta (`0` ne sceglie una libera). `--host` cambia l'indirizzo; qualsiasi indirizzo diverso da un loopback espone il server mock alla tua rete, e la CLI stampa un avviso. Arresta il server con Ctrl+C.

Il comando non legge credenziali e non invia nulla a KeenAuth.

## Endpoint

| Percorso | Scopo |
|---|---|
| `/.well-known/openid-configuration` | Documento di discovery OIDC (servito anche su `/.well-known/oauth-authorization-server`) |
| `/.well-known/jwks.json` | Set di chiavi pubbliche di test (ID chiave `keenauth-test-1`) |
| `/oauth2/authorize` | Endpoint di autorizzazione: flusso con authorization code e PKCE (`S256`) |
| `/oauth2/token` | Endpoint dei token: grant `authorization_code` e `refresh_token` |
| `/oauth2/userinfo` | Endpoint UserInfo (access token Bearer con scope `openid`) |
| `/saml-idp/<application_id>/metadata` | Metadati dell'identity provider SAML 2.0 |
| `/saml-idp/<application_id>/sso` | Endpoint SAML single sign-on (binding HTTP-Redirect e HTTP-POST) |
| `/scim/v2/<directory_id>/…` | Endpoint SCIM 2.0: `Users`, `Groups`, `ServiceProviderConfig`, `ResourceTypes`, `Schemas` |
| `/v1/…` | API di gestione in memoria: ogni operazione del documento OpenAPI dell'API di gestione |

Gli URL degli endpoint nel documento di discovery e nei metadati SAML puntano all'indirizzo locale su cui ascolta il server. `<application_id>` e `<directory_id>` sono nomi a tua scelta (da 1 a 64 caratteri di `A-Z`, `a-z`, `0-9`, `-`, `_`); non occorre registrare nulla in anticipo.

## Come si comporta

- **Nessuna schermata di consenso, nessuna pagina di accesso.** Ogni richiesta di autorizzazione valida viene approvata subito per un utente di test fisso e fittizio e reindirizzata con `code`, `state` e `iss`. Il `sub` dell'utente è `usr_01m3vnbwg0e8mbe7h7xxp6rmkc`, l'indirizzo email `test.user@example.test` (verificato) e il nome `Test User`.
- **Nessun client registrato.** Viene accettato qualsiasi `client_id`. I client secret non vengono verificati. `redirect_uri` deve essere un URI `https` assoluto, oppure un URI `http` su un host loopback (`127.0.0.1`, `[::1]`, `localhost`), senza frammento; una `redirect_uri` non valida o un `client_id` mancante producono una pagina di errore invece di un reindirizzamento.
- **PKCE è obbligatorio.** Va inviato `code_challenge` con `code_challenge_method=S256`; `plain` viene rifiutato. La richiesta di token deve inviare il `code_verifier` corrispondente.
- **I code sono monouso** e scadono dopo 60 secondi. Presentare un code una seconda volta fallisce con `invalid_grant` e revoca ogni token emesso a partire da esso.
- **Token.** L'access token è un JWT RFC 9068 (`typ` `at+jwt`), valido per un'ora, con `sub`, `client_id`, `scope` e un `jti` casuale. Il suo audience è il parametro `resource` della richiesta di autorizzazione, oppure `https://test.keenauth.invalid/oauth2/userinfo` se non ne è stato inviato uno. Con lo scope `openid` ottieni anche un ID token (con `nonce` e `auth_time`; claim `email` e `name` con gli scope `email` e `profile`). Con `offline_access` ottieni un refresh token.
- **I refresh token ruotano.** Ogni refresh restituisce un nuovo refresh token; presentarne uno vecchio fallisce e revoca l'intera catena. Un refresh può restringere lo scope ma non ampliarlo.
- **UserInfo** accetta solo access token emessi da questo processo, non scaduti né revocati, e che contengono lo scope `openid`.

## Token e il tuo verificatore

I token sono firmati con la chiave pubblica di test del [kit di test offline](/docs/sdk/testing) e portano l'issuer `https://test.keenauth.invalid`, non l'indirizzo locale. Il dominio di primo livello `.invalid` è riservato, quindi un verificatore di produzione non può mai accettare questi token: i verificatori degli SDK li accettano solo in modalità test, che attivi esplicitamente nel codice di test.

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

Usa la stessa modalità di test in TypeScript (`createTestVerifier`) e in Python (`keenauth.testing.verifier`). Le librerie generiche per client OIDC che pretendono che l'`issuer` del documento di discovery coincida con l'URL da cui è stato recuperato rifiuteranno il documento del server mock; configurale con gli URL degli endpoint direttamente, oppure testale contro il tuo ambiente `development`.

## Esempio: il flusso completo con curl

L'esempio richiede `curl`, `openssl` e `jq`. Avvia prima il server in un altro terminale.

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

Il passo 3 stampa l'utente di test:

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

## Identity provider SAML

Usa il server mock come identity provider di un service provider SAML che stai sviluppando: punta il service provider all'URL dei metadati, accedi e il server mock risponderà con una risposta firmata per l'utente di test. I percorsi seguono l'identity provider SAML ospitato che KeenAuth prevede per una release successiva.

- **Metadati.** `GET /saml-idp/<application_id>/metadata` restituisce un `EntityDescriptor` con l'entity ID `https://test.keenauth.invalid/saml-idp/<application_id>`, il certificato di firma, i formati NameID `persistent` ed `emailAddress` e l'endpoint SSO per entrambi i binding. Le AuthnRequest firmate non sono richieste (`WantAuthnRequestsSigned="false"`); una firma sulla richiesta viene ignorata.
- **SSO.** Invia la AuthnRequest con il binding HTTP-Redirect (`GET`, DEFLATE + base64 in `SAMLRequest`) oppure con il binding HTTP-POST (`POST`, base64). Non c'è una pagina di accesso: una richiesta valida viene approvata subito per l'utente di test e ricevi una pagina HTML che invia da sola `SAMLResponse` (e il tuo `RelayState`, al massimo 80 byte) all'URL ACS del service provider.
- **La AuthnRequest deve contenere** `ID`, `Version="2.0"`, `IssueInstant`, un `saml:Issuer` (l'entity ID del tuo service provider) e `AssertionConsumerServiceURL`, perché il server mock non ha service provider registrati. L'URL ACS deve essere un URL `https` assoluto, oppure un URL `http` su un host loopback, senza frammento. `ProtocolBinding`, se presente, deve essere HTTP-POST; `Destination`, se presente, deve essere l'URL SSO. Ogni altra richiesta riceve `400` e non viene inviata a nessuna parte. I DTD vengono rifiutati.
- **La risposta** è firmata due volte con RSA-SHA256 e canonicalizzazione esclusiva: l'asserzione, poi la risposta intera. `InResponseTo` è l'`ID` della tua richiesta, `Destination` e il `Recipient` della subject confirmation sono l'URL ACS, l'`Audience` è l'`Issuer` della tua richiesta, e l'asserzione è valida dal momento dell'emissione per 5 minuti. Il NameID è `persistent` (un valore stabile per applicazione) salvo che la richiesta chieda `emailAddress`. Gli attributi sono `email`, `given_name`, `family_name` e `name`.

**La chiave di firma è pubblica.** Deriva da un'etichetta pubblicata, quindi chiunque può firmare risposte che questo certificato verifica. La sua impronta SHA-256 è `fcc2fc37b9ddb800721283156f9eb26ff1b80f099d996a65650f8cb469a9be81` e il suo subject contiene `TEST ONLY`. Registrala solo nelle configurazioni di sviluppo e test del tuo service provider, mai in produzione. KeenAuth lo applica alle proprie connessioni SAML: negli ambienti di staging e produzione rifiuta questa chiave e qualsiasi entity ID sotto `.invalid`, quando importi metadati o aggiungi un certificato, e di nuovo all'accesso; gli ambienti di sviluppo le accettano.

### Esempio: un accesso SAML con curl

L'esempio richiede `curl` e `openssl`. Avvia prima il server in un altro terminale.

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

L'ultimo comando stampa `InResponseTo="_req1"`. Il tuo service provider convalida `response.xml` con il certificato in `idp-metadata.xml`.

## Endpoint SCIM 2.0

Usa gli endpoint SCIM per testare un client SCIM, come un connettore di provisioning o uno script che crea utenti e gruppi, senza rete. Ogni `<directory_id>` è una directory separata e vuota che vive finché vive il processo.

- **Autenticazione.** Ogni richiesta richiede `Authorization: Bearer keenauth-mock-scim-token`. Qualsiasi altra richiesta riceve `401` con una sfida `WWW-Authenticate: Bearer`. Il valore è fisso e pubblico; non è un token SCIM di KeenAuth.
- **Utenti e gruppi.** `POST`, `GET`, `PUT`, `PATCH` e `DELETE` su `/Users` e `/Groups` (RFC 7644). `userName` e il `displayName` del gruppo sono obbligatori e univoci nella directory, senza distinzione tra maiuscole e minuscole; un duplicato riceve `409` con `scimType` `uniqueness`. I membri di un gruppo devono essere utenti della stessa directory. Eliminare un utente lo rimuove dai suoi gruppi. `password` viene accettata e scartata.
- **Filtri e pagine.** `filter` supporta `eq`, `ne`, `co`, `sw`, `ew` e `pr` uniti con `and` (per esempio `userName eq "ada@example.test"`), oltre a `startIndex` e `count` (al massimo 200). `attributes` ed `excludedAttributes` selezionano attributi di primo livello. `POST /Users/.search` e `/Groups/.search` accettano gli stessi parametri nel corpo.
- **PATCH** supporta `add`, `remove` e `replace` con percorsi come `active`, `name.givenName`, `emails[type eq "work"].value`, `members[value eq "<id>"]` e attributi dell'estensione enterprise; le operazioni di una richiesta vengono applicate tutte o nessuna.
- **Versioni.** Ogni risorsa riporta `meta.version` e un header `ETag` (`W/"1"`, `W/"2"`, …). `If-Match` su `PUT`, `PATCH` e `DELETE` restituisce `412` quando la versione è cambiata; `If-None-Match` su `GET` restituisce `304`.
- **Discovery.** `ServiceProviderConfig`, `ResourceTypes` e `Schemas` descrivono esattamente questo comportamento. `/Bulk` e `/Me` restituiscono `501`, come anche in KeenAuth.

### Esempio: effettuare il provisioning di un utente con curl

L'esempio richiede `curl` e `jq`. Avvia prima il server in un altro terminale.

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

I comandi stampano `1`, `{"active":false,"version":"W/\"2\""}`, `{"display":"ada@example.test","type":"User"}` e `204`.

## API di gestione (`/v1`)

Usa gli endpoint dell'API di gestione per testare in CI, senza rete, il codice che chiama l'API di gestione di KeenAuth (un SDK, la CLI, uno script di deployment). I percorsi sono quelli ospitati: usa l'URL base `http://127.0.0.1:4010/v1` al posto di quello ospitato e nient'altro cambia. Gli endpoint sono generati dal documento OpenAPI dell'API di gestione incluso nella CLI, quindi risponde ogni operazione di quella versione dell'API.

- **Autenticazione.** Ogni richiesta richiede `Authorization: Bearer keenauth-mock-management-token`. Qualsiasi altra richiesta riceve `401` con una sfida `WWW-Authenticate: Bearer`, anche sui percorsi sconosciuti. Il valore è fisso e pubblico; non è una chiave di gestione di KeenAuth.
- **Risposte.** Ogni operazione risponde con lo stato di successo dichiarato dal documento (`200`, `201`, `202`, `204`; le due operazioni di download rispondono `302` con un `Location` che non si risolve). Il corpo è il `example` della risposta di successo nel documento o, finché non ce n'è uno, il più piccolo esempio valido costruito dallo schema della risposta: solo i campi obbligatori, i campi nullable a `null`, le enumerazioni con il loro primo valore, le stringhe conformi al loro `pattern`.
- **Stato semplice.** Una collezione in cui puoi creare record (`POST /v1/users`, `/v1/organizations`, `/v1/applications`, `/v1/organizations/<org_id>/roles`, …) li conserva: il record creato viene restituito da `GET` sul proprio percorso e nella lista, `PATCH` e `PUT` lo modificano, `DELETE` lo rimuove insieme a tutto ciò che sta sotto, e un percorso sotto un record inesistente è `404`. Gli identificatori hanno la forma con prefisso KeenAuth (`usr_…`, `org_…`, `app_…`) e rispettano il pattern del documento; dove il documento non ne indica uno, il prefisso è derivato dal nome del parametro. Le impostazioni che esistono una sola volta per ambiente (per esempio `/v1/identity-settings`) mantengono ciò che hai scritto con `PATCH` o `PUT`. Le risorse che non si possono creare tramite API (per esempio sessioni e report) rispondono sempre con l'esempio.
- **Liste.** Funzionano `limit` (da 1 a 100, predefinito 20), `after` o `before` con un identificatore di record come cursore, e `order` (`desc` di default, il più recente per primo); la risposta contiene `list_metadata`. I filtri come `status` non vengono applicati.
- **Errori** sono `application/problem+json` come nel documento, con `code` e un `request_id` che compare anche nell'header `Idp-Request-Id`. Un percorso sconosciuto è `404 not_found`, un metodo che il percorso non prevede è `405 method_not_allowed` con un header `Allow`, un corpo che non è JSON valido (o non è un oggetto) è `400 malformed_request`, un `Content-Type` diverso da JSON è `415` sulle operazioni che lo elencano, un campo obbligatorio mancante è `422 validation_failed` con `errors[]`, un corpo più grande di 1 MiB è `413`, e un `limit` o un cursore non validi sono `400`.

**Cosa cambia rispetto all'ambiente `development`.** Il server mock verifica solo che il corpo sia un oggetto JSON con i campi obbligatori; non controlla formati dei campi, lunghezze, enumerazioni né unicità, e un campo di tipo errato viene ignorato. Ignora `If-Match`, `Idempotency-Key` e gli altri header e non restituisce `ETag`. Non controlla permessi, limiti di piano, quote, rate limit né la regione, non maschera i dati personali, non invia email, non esegue job (un job accettato resta com'era stato creato) e ha un solo ambiente. I record esistono solo per la durata del processo, al massimo 10.000. Usa il tuo ambiente `development` per scoprire come si comporta l'API reale.

### Esempio: creare, leggere, modificare ed eliminare con curl

L'esempio richiede `curl` e `jq`. Avvia prima il server in un altro terminale.

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

I comandi stampano `{"id":"org_","name":"Acme"}`, `{"count":1,"list_metadata":{"after":null,"before":null}}`, `Acme Inc`, `204`, `{"status":404,"code":"not_found"}` e `{"status":401,"code":"unauthenticated"}`.

## Non supportato

Non ci sono test clock, né DPoP, né PAR, né request object, né device flow, né client credentials, né token exchange, né logout o revoca, né pagine ospitate. L'identity provider SAML non ha single logout, né asserzioni cifrate, né artifact binding, né accesso avviato dall'IdP. Gli endpoint SCIM non hanno operazioni bulk, ordinamento, `/Me` né gruppi annidati, e il filtro non supporta `or`, `not`, parentesi né `gt`/`ge`/`lt`/`le`. Usa il tuo ambiente `development` per testarli.
