Serveur mock local
keenauth mock serve lance sur votre machine un petit serveur OpenID Connect, SAML 2.0 et SCIM 2.0, ainsi qu'une version en mémoire de l'API de gestion /v1, pour les tâches CI sans accès réseau et pour le développement local. Il n'a besoin ni d'environnement KeenAuth, ni de clé API, ni d'Internet. Toutes les données sont conservées en mémoire et supprimées à la fin du processus.
Le serveur mock n'est pas KeenAuth. Il n'implémente que les flux ci-dessous, approuve automatiquement chaque requête et vérifie beaucoup moins que le service réel. Votre environnement development hébergé est la seule référence pour le comportement réel : exécutez vos tests de bout en bout contre lui avant toute mise en production.
Démarrer le serveur
keenauth mock serve
Par défaut, le serveur écoute sur 127.0.0.1:4010, une adresse de boucle locale qu'aucune autre machine ne peut atteindre. --port change le port (0 en choisit un libre). --host change l'adresse ; toute adresse autre qu'une adresse de boucle locale expose le serveur mock à votre réseau, et la CLI affiche un avertissement. Arrêtez le serveur avec Ctrl+C.
La commande ne lit aucun identifiant et n'envoie rien à KeenAuth.
Points de terminaison
| Chemin | Rôle |
|---|---|
/.well-known/openid-configuration |
Document de découverte OIDC (également servi sur /.well-known/oauth-authorization-server) |
/.well-known/jwks.json |
Jeu de clés publiques de test (identifiant de clé keenauth-test-1) |
/oauth2/authorize |
Point de terminaison d'autorisation : flux par code d'autorisation avec PKCE (S256) |
/oauth2/token |
Point de terminaison de jetons : grants authorization_code et refresh_token |
/oauth2/userinfo |
Point de terminaison UserInfo (jeton d'accès Bearer avec le scope openid) |
/saml-idp/<application_id>/metadata |
Métadonnées du fournisseur d'identité SAML 2.0 |
/saml-idp/<application_id>/sso |
Point de terminaison SSO SAML (liaisons HTTP-Redirect et HTTP-POST) |
/scim/v2/<directory_id>/… |
Points de terminaison SCIM 2.0 : Users, Groups, ServiceProviderConfig, ResourceTypes, Schemas |
/v1/… |
API de gestion en mémoire : chaque opération du document OpenAPI de l'API de gestion |
Les URL des points de terminaison du document de découverte et des métadonnées SAML pointent vers l'adresse locale sur laquelle écoute le serveur. <application_id> et <directory_id> sont des noms de votre choix (1 à 64 caractères parmi A-Z, a-z, 0-9, -, _) ; rien n'a besoin d'être enregistré au préalable.
Comportement
- Aucun écran de consentement, aucune page de connexion. Chaque requête d'autorisation valide est approuvée immédiatement pour un utilisateur de test fixe et fictif, puis redirigée avec
code,stateetiss. Lesubde l'utilisateur estusr_01m3vnbwg0e8mbe7h7xxp6rmkc, l'adresse e-mailtest.user@example.test(vérifiée) et le nomTest User. - Aucun client enregistré. Tout
client_idest accepté. Les secrets client ne sont pas vérifiés.redirect_uridoit être une URIhttpsabsolue, ou une URIhttpsur un hôte de boucle locale (127.0.0.1,[::1],localhost), sans fragment ; uneredirect_uriinvalide ou unclient_idabsent produit une page d'erreur au lieu d'une redirection. - PKCE est obligatoire.
code_challengeaveccode_challenge_method=S256doit être envoyé ;plainest refusé. La requête de jeton doit envoyer lecode_verifiercorrespondant. - Les codes sont à usage unique et expirent au bout de 60 secondes. Présenter un code une seconde fois échoue avec
invalid_grantet révoque tous les jetons émis à partir de celui-ci. - Jetons. Le jeton d'accès est un JWT RFC 9068 (
typat+jwt) valable une heure, avecsub,client_id,scopeet unjtialéatoire. Son audience est le paramètreresourcede la requête d'autorisation, ouhttps://test.keenauth.invalid/oauth2/userinfosi aucun n'a été envoyé. Avec le scopeopenid, vous recevez aussi un jeton d'identité (avecnonceetauth_time; les claimsemailetnameavec les scopesemailetprofile). Avecoffline_access, vous recevez un jeton de rafraîchissement. - Les jetons de rafraîchissement tournent. Chaque rafraîchissement renvoie un nouveau jeton de rafraîchissement ; présenter un ancien échoue et révoque toute la chaîne. Un rafraîchissement peut restreindre le scope mais non l'élargir.
- UserInfo n'accepte que les jetons d'accès émis par ce processus, non expirés et non révoqués, et portant le scope
openid.
Jetons et votre vérificateur
Les jetons sont signés avec la clé publique de test du kit de test hors ligne et portent l'émetteur https://test.keenauth.invalid, et non l'adresse locale. Le domaine de premier niveau .invalid est réservé : un vérificateur de production ne peut donc jamais accepter ces jetons. Les vérificateurs des SDK ne les acceptent qu'en mode test, que vous activez explicitement dans le code de test.
// 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)
Utilisez le même mode test en TypeScript (createTestVerifier) et en Python (keenauth.testing.verifier). Les bibliothèques génériques de client OIDC qui exigent que l'issuer du document de découverte soit égal à l'URL à partir de laquelle il a été récupéré refuseront le document du serveur mock ; configurez-les directement avec les URL des points de terminaison, ou testez-les contre votre environnement development.
Exemple : le flux complet avec curl
L'exemple nécessite curl, openssl et jq. Démarrez d'abord le serveur dans un autre terminal.
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)"
L'étape 3 affiche l'utilisateur de test :
{"email":"test.user@example.test","email_verified":true,"sub":"usr_01m3vnbwg0e8mbe7h7xxp6rmkc"}
Fournisseur d'identité SAML
Utilisez le serveur mock comme fournisseur d'identité d'un fournisseur de services SAML que vous développez : indiquez au fournisseur de services l'URL des métadonnées, connectez-vous, et le serveur mock répond par une réponse signée pour l'utilisateur de test. Les chemins suivent le fournisseur d'identité SAML hébergé que KeenAuth prévoit pour une version ultérieure.
- Métadonnées.
GET /saml-idp/<application_id>/metadatarenvoie unEntityDescriptoravec l'identifiant d'entitéhttps://test.keenauth.invalid/saml-idp/<application_id>, le certificat de signature, les formats de NameIDpersistentetemailAddress, et le point de terminaison SSO pour les deux liaisons. Les AuthnRequest signées ne sont pas requises (WantAuthnRequestsSigned="false") ; une signature sur la requête est ignorée. - SSO. Envoyez l'AuthnRequest avec la liaison HTTP-Redirect (
GET, DEFLATE + base64 dansSAMLRequest) ou la liaison HTTP-POST (POST, base64). Il n'y a pas de page de connexion : une requête valide est approuvée immédiatement pour l'utilisateur de test, et vous obtenez une page HTML qui poste elle-mêmeSAMLResponse(et votreRelayState, 80 octets au maximum) vers l'URL ACS du fournisseur de services. - L'AuthnRequest doit contenir
ID,Version="2.0",IssueInstant, unsaml:Issuer(l'identifiant d'entité de votre fournisseur de services) etAssertionConsumerServiceURL, car le serveur mock n'a aucun fournisseur de services enregistré. L'URL ACS doit être une URLhttpsabsolue, ou une URLhttpsur un hôte de boucle locale, sans fragment.ProtocolBinding, s'il est présent, doit valoir HTTP-POST ;Destination, s'il est présent, doit être l'URL SSO. Toute autre requête reçoit400et n'est envoyée nulle part. Les DTD sont refusées. - La réponse est signée deux fois avec RSA-SHA256 et une canonisation exclusive : l'assertion, puis la réponse entière.
InResponseToest l'IDde votre requête,Destinationet leRecipientde la confirmation du sujet sont l'URL ACS, l'Audienceest l'Issuerde votre requête, et l'assertion est valable à partir de son émission pendant 5 minutes. Le NameID estpersistent(une valeur stable par application), sauf si la requête demandeemailAddress. Les attributs sontemail,given_name,family_nameetname.
La clé de signature est publique. Elle est dérivée d'un libellé publié, donc n'importe qui peut signer des réponses que ce certificat vérifie. Son empreinte SHA-256 est fcc2fc37b9ddb800721283156f9eb26ff1b80f099d996a65650f8cb469a9be81 et son sujet indique TEST ONLY. Ne l'enregistrez que dans les configurations de développement et de test de votre fournisseur de services, jamais en production. KeenAuth l'applique à ses propres connexions SAML : dans les environnements de staging et de production, il refuse cette clé et tout identifiant d'entité sous .invalid, à l'import des métadonnées ou à l'ajout d'un certificat, puis à nouveau à la connexion ; les environnements de développement les acceptent.
Exemple : une connexion SAML avec curl
L'exemple nécessite curl et openssl. Démarrez d'abord le serveur dans un autre terminal.
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
La dernière commande affiche InResponseTo="_req1". Votre fournisseur de services valide response.xml avec le certificat présent dans idp-metadata.xml.
Points de terminaison SCIM 2.0
Utilisez les points de terminaison SCIM pour tester un client SCIM, tel qu'un connecteur de provisionnement ou un script qui crée des utilisateurs et des groupes, sans réseau. Chaque <directory_id> est un annuaire distinct et vide, qui vit aussi longtemps que le processus.
- Authentification. Chaque requête doit porter
Authorization: Bearer keenauth-mock-scim-token. Toute autre requête reçoit401avec un défiWWW-Authenticate: Bearer. La valeur est fixe et publique ; ce n'est pas un jeton SCIM de KeenAuth. - Utilisateurs et groupes.
POST,GET,PUT,PATCHetDELETEsur/Userset/Groups(RFC 7644).userNameet ledisplayNamedu groupe sont obligatoires et uniques dans l'annuaire, sans tenir compte de la casse ; un doublon reçoit409avecscimTypeuniqueness. Les membres d'un groupe doivent être des utilisateurs du même annuaire. Supprimer un utilisateur le retire de ses groupes.passwordest accepté et ignoré. - Filtres et pages.
filterprend en chargeeq,ne,co,sw,ewetpr, reliés parand(par exempleuserName eq "ada@example.test"), ainsi questartIndexetcount(200 au maximum).attributesetexcludedAttributessélectionnent des attributs de premier niveau.POST /Users/.searchet/Groups/.searchacceptent les mêmes paramètres dans le corps. - PATCH prend en charge
add,removeetreplaceavec des chemins tels queactive,name.givenName,emails[type eq "work"].value,members[value eq "<id>"]et les attributs de l'extension enterprise ; les opérations d'une même requête sont appliquées toutes ou aucune. - Versions. Chaque ressource porte
meta.versionet un en-têteETag(W/"1",W/"2", …).If-MatchsurPUT,PATCHetDELETErenvoie412lorsque la version a changé ;If-None-MatchsurGETrenvoie304. - Découverte.
ServiceProviderConfig,ResourceTypesetSchemasdécrivent exactement ce comportement./Bulket/Merenvoient501, comme dans KeenAuth.
Exemple : provisionner un utilisateur avec curl
L'exemple nécessite curl et jq. Démarrez d'abord le serveur dans un autre terminal.
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"
Les commandes affichent 1, {"active":false,"version":"W/\"2\""}, {"display":"ada@example.test","type":"User"} et 204.
API de gestion (/v1)
Utilisez les points de terminaison de l'API de gestion pour tester, en CI et sans réseau, le code qui appelle l'API de gestion de KeenAuth (un SDK, la CLI, un script de déploiement). Les chemins sont ceux de l'environnement hébergé : utilisez l'URL de base http://127.0.0.1:4010/v1 à la place de l'URL hébergée, rien d'autre ne change. Les points de terminaison sont générés à partir du document OpenAPI de l'API de gestion intégré à la CLI, donc chaque opération de cette version de l'API répond.
- Authentification. Chaque requête doit porter
Authorization: Bearer keenauth-mock-management-token. Toute autre requête reçoit401avec un défiWWW-Authenticate: Bearer, y compris sur les chemins inconnus. La valeur est fixe et publique ; ce n'est pas une clé de gestion KeenAuth. - Réponses. Chaque opération répond avec le code de succès que déclare le document (
200,201,202,204; les deux opérations de téléchargement répondent302avec unLocationqui ne se résout pas). Le corps est l'examplede la réponse de succès du document ou, tant qu'il n'y en a pas, le plus petit exemple valide construit à partir du schéma de réponse : seulement les champs obligatoires, les champs nullables ànull, les énumérations avec leur première valeur, et les chaînes conformes à leurpattern. - État simple. Une collection dans laquelle vous pouvez créer des enregistrements (
POST /v1/users,/v1/organizations,/v1/applications,/v1/organizations/<org_id>/roles, …) les conserve : l'enregistrement créé est renvoyé parGETsur son propre chemin et dans la liste,PATCHetPUTle modifient,DELETEle supprime avec tout ce qui se trouve en dessous, et un chemin sous un enregistrement inexistant renvoie404. Les identifiants ont la forme à préfixe KeenAuth (usr_…,org_…,app_…) et respectent le motif du document ; lorsque le document n'en donne pas, le préfixe est déduit du nom du paramètre. Les réglages qui n'existent qu'une fois par environnement (par exemple/v1/identity-settings) conservent ce que vous avez écrit avecPATCHouPUT. Les ressources qui ne peuvent pas être créées par l'API (par exemple les sessions et les rapports) répondent toujours avec l'exemple. - Listes.
limit(1 à 100, 20 par défaut),afteroubeforeavec l'identifiant d'un enregistrement comme curseur, etorder(descpar défaut, le plus récent d'abord) fonctionnent ; la réponse contientlist_metadata. Les filtres tels questatusne sont pas appliqués. - Erreurs :
application/problem+jsoncomme dans le document, aveccodeet unrequest_idégalement présent dans l'en-têteIdp-Request-Id. Un chemin inconnu renvoie404 not_found, une méthode que le chemin ne prévoit pas renvoie405 method_not_allowedavec un en-têteAllow, un corps qui n'est pas du JSON valide (ou qui n'est pas un objet) renvoie400 malformed_request, unContent-Typeautre que JSON renvoie415sur les opérations qui le listent, un champ obligatoire manquant renvoie422 validation_failedavecerrors[], un corps de plus de 1 MiB renvoie413, et unlimitou un curseur invalide renvoie400.
Ce qui diffère de l'environnement development. Le serveur mock vérifie seulement que le corps est un objet JSON contenant les champs obligatoires ; les formats de champs, les longueurs, les énumérations et l'unicité ne sont pas contrôlés, et un champ au mauvais type est ignoré. Il ignore If-Match, Idempotency-Key et les autres en-têtes et ne renvoie pas d'ETag. Il ne contrôle ni les permissions, ni les limites de forfait, ni les quotas, ni les limites de débit, ni la région ; il ne masque pas les données personnelles, n'envoie aucun e-mail, n'exécute aucune tâche (une tâche acceptée reste dans l'état où elle a été créée) et ne dispose que d'un seul environnement. Les enregistrements n'existent que pendant la durée du processus, au maximum 10 000. Utilisez votre environnement development pour savoir comment se comporte la véritable API.
Exemple : créer, lire, modifier et supprimer avec curl
L'exemple nécessite curl et jq. Démarrez d'abord le serveur dans un autre terminal.
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}'
Les commandes affichent {"id":"org_","name":"Acme"}, {"count":1,"list_metadata":{"after":null,"before":null}}, Acme Inc, 204, {"status":404,"code":"not_found"} et {"status":401,"code":"unauthenticated"}.
Non pris en charge
Il n'y a pas d'horloge de test, ni DPoP, ni PAR, ni objets de requête, ni flux device, ni client credentials, ni échange de jetons, ni déconnexion ou révocation, ni pages hébergées. Le fournisseur d'identité SAML n'a ni déconnexion unique, ni assertions chiffrées, ni liaison par artefact, ni connexion initiée par l'IdP. Les points de terminaison SCIM n'ont ni opérations en masse, ni tri, ni /Me, ni groupes imbriqués, et le filtre ne connaît ni or, ni not, ni parenthèses, ni gt/ge/lt/le. Utilisez votre environnement development pour les tester.