Authentification REST sans navigateur
Sur cette page
L’API REST d’authentification de TOSIAM permet de se connecter sans page de connexion ni navigateur : le client demande à TOSIAM ce qu’il attend (des callbacks), répond, et reçoit à la fin le jeton de la session. C’est ce qu’utilise une page de connexion personnalisée, une application mobile de votre organisation ou un test automatisé.
Le tutoriel Callbacks et formulaire montre cet échange à la main, avec curl. Celui-ci construit un vrai client : il répond aux callbacks sans connaître à l’avance les étapes du graphe, signale une réponse refusée, gère les erreurs, puis se sert de la session et la ferme. Le code est dans le dépôt tosiam-samples, dossier tosiam-rest-auth : un client Node.js sans dépendance.
client ──① POST /json/ref/authenticate?authIndexType=service&authIndexValue=connexion-api──▶ TOSIAM
◀── étape : authId + callbacks (NameCallback) ─────────────────────────────────────────
──② POST /json/ref/authenticate : la même étape, callbacks remplis ───────────────────▶
◀── étape suivante (PasswordCallback) … puis tokenId : la session est ouverte ──────────
──③ /json/ref/sessions, /json/ref/users, jeton dans l'en-tête iPlanetDirectoryPro ────▶Étape 1 : le graphe de connexion#
Le client fonctionne avec n’importe quelle chaîne ou n’importe quel graphe. L’exemple crée dans le royaume ref un graphe connexion-api qui demande l’identifiant puis le mot de passe sur deux étapes, les vérifie dans le datastore et accorde trois essais :
{
"startNodeId": "identifiant",
"steps": {
"identifiant": { "type": "UsernameCollectorNode", "outcomes": { "outcome": "motDePasse" } },
"motDePasse": { "type": "PasswordCollectorNode", "outcomes": { "outcome": "verification" } },
"verification": {
"type": "DataStoreNode",
"config": { "authLevel": 0 },
"outcomes": { "true": "succes", "false": "essais" }
},
"essais": {
"type": "RetryLimitNode",
"config": { "retryLimit": 2 },
"outcomes": { "true": "motDePasse", "false": "echec" }
},
"succes": { "type": "SuccessNode" },
"echec": { "type": "FailureNode" }
}
}(Le fichier de l’exemple contient aussi la position et le nom de chaque nœud dans l’éditeur, layout.)
retryLimit compte les nouveaux essais : avec 2, l’utilisateur a droit à trois mots de passe en tout ; le troisième refus termine la connexion. Le compteur est tenu par utilisateur pendant 15 minutes, pas par connexion : recommencer depuis le début ne rend pas les essais, et un échec de plus termine aussitôt la nouvelle connexion. Le bon mot de passe reste accepté et remet le compteur à zéro (voir RetryLimitNode).
Pour s’y connecter par l’API, le graphe doit être exposé : une instance de module AuthGraph qui le désigne, et une configuration d’authentification du même nom (voir Exposer un graphe comme point d’entrée). Les commandes ssoadm de l’exemple :
create-auth-graph --realm ref --name connexion-api --datafile connexion-api.json
create-auth-instance --realm ref --name connexion-api --authtype AuthGraph
update-auth-instance --realm ref --name connexion-api --attributevalues tosiam-auth-graph-id=connexion-api
create-auth-cfg --realm ref --name connexion-api
update-auth-cfg-entr --realm ref --name connexion-api --entries connexion-api|REQUISITEupdate-auth-cfg-entr remplace la liste des entrées de la configuration : relancé, il ne crée pas de doublon, contrairement à add-auth-cfg-entr.
Étape 2 : le dialogue avec l’API#
Toutes les requêtes portent deux en-têtes, sans lesquels TOSIAM répond 403 : X-Requested-With (protection CSRF) et Accept-API-Version. Attention, la version dépend de la ressource :
| Ressource | Accept-API-Version | Usage |
|---|---|---|
/json/ref/authenticate | resource=2.0, protocol=1.0 | Connexion |
/json/ref/users | resource=2.0, protocol=1.0 | Profil de l’utilisateur |
/json/ref/sessions | resource=1.1, protocol=1.0 | Validation, durée, déconnexion |
Avec resource=2.0, toutes les actions de /sessions répondent 404 Resource '' not found : cette ressource n’existe qu’en version 1.1.
Le client du dossier cli regroupe ces appels dans tosiam.js :
async function call(method, path, { version, token, body } = {}) {
// X-Requested-With and Accept-API-Version are required: without them TOSIAM answers 403
const headers = { 'X-Requested-With': 'XMLHttpRequest', 'Accept-API-Version': version, 'Content-Type': 'application/json' };
// The session token travels in a header named like the session cookie
if (token) headers.iPlanetDirectoryPro = token;
...
}
return {
/** First step of the chain or graph exposed under this name (authIndexType=service). */
start: service => call('POST', `/authenticate?authIndexType=service&authIndexValue=${encodeURIComponent(service)}`,
{ version: AUTHENTICATE }),
/** Posts the answered step back, authId included: TOSIAM answers with the next step or the session. */
submit: stage => call('POST', '/authenticate', { version: AUTHENTICATE, body: stage }),
...
};Ce que TOSIAM répond, selon le cas :
| Réponse | Signification |
|---|---|
200 avec authId et callbacks | Étape suivante : la remplir et la renvoyer |
200 avec tokenId | Session ouverte ; tokenId est le jeton de session |
401 Authentication failed | Identifiants refusés par une chaîne, ou essais épuisés dans un graphe |
408 Session has timed out | authId expiré, ou déjà utilisé par une tentative qui a échoué : il faut recommencer depuis le début |
400 No Configuration found | Aucune chaîne ni aucun graphe exposé sous ce nom |
Étape 3 : répondre aux callbacks#
Chaque étape contient un ou plusieurs callbacks. Le client ne sait pas quel graphe il parcourt : il remplit chaque callback selon son type, à partir des textes que TOSIAM fournit (prompt, liste de choix…).
export async function answer(stage, user) {
const filled = structuredClone(stage);
for (const callback of filled.callbacks ?? []) {
switch (callback.type) {
case 'NameCallback':
callback.input[0].value = await user.name(output(callback, 'prompt'));
break;
case 'PasswordCallback':
callback.input[0].value = await user.password(output(callback, 'prompt'));
break;
case 'ChoiceCallback':
callback.input[0].value = await user.choice(output(callback, 'prompt'), output(callback, 'choices'),
output(callback, 'defaultChoice'));
break;
// ConfirmationCallback, TextOutputCallback, HiddenValueCallback...
default:
throw new UnsupportedCallbackError(callback.type);
}
}
return filled;
}| Callback | Ce qu’en fait le client |
|---|---|
NameCallback | Demande le texte ; TOSIAM_USER répond à la première demande seulement |
PasswordCallback | Demande la valeur sans l’afficher ; TOSIAM_PASSWORD répond à la première demande seulement : un code à usage unique ou un nouveau mot de passe, eux aussi des PasswordCallback, attendent une personne |
ChoiceCallback, ConfirmationCallback | Propose les choix ; sans terminal, le numéro du choix doit arriver sur l’entrée standard : conditions d’utilisation et consentement ne sont jamais acceptés à la place de l’utilisateur |
TextOutputCallback | Affiche le message au terminal, mais pas hors terminal (un message peut contenir des codes de récupération) ; un script destiné au navigateur (type 4) n’est pas pris en charge |
HiddenValueCallback | Renvoie telle quelle une valeur d’information (otp.length, totp.digit, totp.attemptsLeft) ; toute autre valeur cachée est remplie par un navigateur (captcha, clé d’accès, lien magique, QR code d’enrôlement) : le client s’arrête |
| Autre type | S’arrête en nommant le type |
S’arrêter est volontaire : renvoyer une valeur vide ferait échouer la connexion sans explication, et certains nœuds (captcha, lien magique) reposent alors la même question sans fin. Par précaution, le client s’arrête aussi au-delà de 25 étapes.
Étape 4 : la boucle de connexion#
Le client renvoie chaque étape remplie jusqu’à recevoir tokenId. Un point demande une attention particulière : quand le mot de passe est refusé, le graphe le redemande sans message d’erreur (RetryLimitNode renvoie vers l’étape du mot de passe). TOSIAM ne dit pas qu’une réponse a été refusée : ce client le devine quand la même question revient deux fois de suite. C’est une heuristique, juste pour ce graphe, pas une règle : un graphe qui revient à l’identifiant après un échec, ou qui pose deux fois la même question à dessein, la trompe.
/** What a step asks for: when TOSIAM asks the same thing again, the previous answer was refused. */
const question = stage => JSON.stringify([stage.stage,
(stage.callbacks ?? []).map(c => [c.type, c.output?.find(o => o.name === 'prompt')?.value])]);
export async function signIn(client, service, user, { onRefused } = {}) {
let stage = await client.start(service);
let previous;
for (let steps = 0; !stage.tokenId; steps++) {
if (steps >= MAX_STEPS) throw new TosiamError('loop', `Plus de ${MAX_STEPS} étapes`);
if (!Array.isArray(stage.callbacks)) throw new TosiamError('http', 'Réponse inattendue de TOSIAM');
const current = question(stage);
if (current === previous) onRefused?.(stage);
previous = current;
stage = await client.submit(await answer(stage, user));
}
return stage.tokenId;
}Seule une personne réessaie : quand la réponse refusée venait de TOSIAM_PASSWORD, le client s’arrête (onRefused lève une erreur), au lieu de renvoyer la même valeur et d’épuiser les essais de l’utilisateur.
La chaîne par défaut du royaume (ldapService) se comporte autrement : identifiant et mot de passe arrivent ensemble dans une seule étape, et un refus répond aussitôt 401.
Étape 5 : utiliser la session#
Le tokenId est la session TOSIAM : celui qui le détient agit au nom de l’utilisateur. Le client l’envoie dans l’en-tête iPlanetDirectoryPro, ne l’affiche jamais et ne l’écrit dans aucun journal.
| Appel | Réponse |
|---|---|
POST /json/ref/sessions/?_action=validate | {"valid": true, "uid": "dduck", "realm": "/ref"} |
POST /json/ref/sessions/?_action=getTimeLeft | {"maxtime": 7200} : secondes avant la fin de la session |
POST /json/ref/sessions/?_action=getMaxIdle | {"maxidletime": 30} : minutes d’inactivité tolérées |
POST /json/ref/users?_action=idFromSession | {"id": "dduck", "realm": "/ref", …} |
GET /json/ref/users/dduck | Le profil : givenName, sn, mail… (un utilisateur ne lit que le sien : 403 pour un autre) |
POST /json/ref/sessions/?_action=logout | {"result": "Successfully logged out"} ; ensuite validate répond {"valid": false} |
Étape 6 : lancer et tester#
cd tosiam-rest-auth
./up.sh
cd cli
node connexion.jsConnexion à http://localhost:8080/tosiam, royaume ref, service connexion-api
Username dduck
Password
Connecté : dduck (royaume /ref)
Profil : Donald DUCK, donald_duck@tosiam.io
Temps restant : 120 min, inactivité maximale : 30 min
Session fermée.
Session encore valide : nonAvec un mauvais mot de passe au terminal, le refus est signalé et le mot de passe redemandé ; au troisième refus, la connexion échoue (code de sortie 1) :
Connexion à http://localhost:8080/tosiam, royaume ref, service connexion-api
Username dduck
Password
Réponse refusée par TOSIAM : la même information est redemandée.
Password
Réponse refusée par TOSIAM : la même information est redemandée.
Password
Échec de l'authentification : identifiants refusés, ou nombre d'essais dépassé.Sans terminal, par exemple dans une intégration continue, les réponses viennent des variables d’environnement ou de l’entrée standard. Le mot de passe ne se passe jamais en argument : il resterait dans l’historique du shell et dans la liste des processus ; prenez-le dans les secrets du système d’intégration.
TOSIAM_USER=dduck TOSIAM_PASSWORD="$MOT_DE_PASSE" node connexion.js
node connexion.js --service ldapService # la chaîne par défaut du royaumeUne valeur refusée n’est pas renvoyée :
Connexion à http://localhost:8080/tosiam, royaume ref, service connexion-api
Réponse refusée par TOSIAM : la même information est redemandée.
Échec de l'authentification : la réponse venait de TOSIAM_USER ou TOSIAM_PASSWORD, elle n'est pas renvoyée.En cas de problème#
| Symptôme | Cause probable |
|---|---|
403 sur chaque appel | En-tête X-Requested-With ou Accept-API-Version absent |
404 Resource '' not found sur /sessions | Accept-API-Version: resource=2.0 au lieu de resource=1.1 |
400 No Configuration found | Graphe non exposé : instance de module ou configuration d’authentification manquante, ou nom différent |
408 Session has timed out en renvoyant une étape | authId expiré ou déjà utilisé par une tentative échouée : recommencer depuis la première requête |
| Le mot de passe est redemandé sans message | Refus du mot de passe dans un graphe avec RetryLimitNode : c’est le comportement attendu, à signaler à l’utilisateur |
401 dès le premier mot de passe faux, sans nouvel essai | Essais déjà épuisés pour cet utilisateur : le compteur de RetryLimitNode survit à la connexion, pour une fenêtre de 15 minutes qui commence au premier refus, et il est commun à tous les graphes du royaume ; le bon mot de passe le remet à zéro |
Plus de 25 étapes : la connexion tourne en boucle | Graphe sans fin, ou étape qui revient sans cesse parce que le client ne sait pas la compléter |
401 sur /users/<id> ou /sessions après une connexion réussie | Session fermée ou expirée, ou jeton absent de l’en-tête iPlanetDirectoryPro |
En production#
- HTTPS pour chaque appel : le mot de passe et le jeton de session circulent dans les requêtes. Le client prévient quand l’adresse n’est pas en HTTPS (hors
localhost) et ne suit aucune redirection, qui renverrait le mot de passe ou le jeton vers une autre adresse. - Le jeton de session est un secret : jamais dans les journaux, les adresses ou les messages d’erreur ; fermez la session (
logout) dès qu’elle ne sert plus. - Limitez les essais, sans vous y fier seul :
RetryLimitNodeborne les essais d’une connexion, mais chaque nouvelle connexion en accorde encore un. Contre les essais répétés, ajoutez une limitation de débit en amont (proxy, pare-feu applicatif) ; le verrouillage de compte (LockoutNode) a ses propres risques, voir Nœuds : décisions. - Une page de connexion web sur un autre domaine que TOSIAM appelle l’API depuis le navigateur : l’en-tête
X-Requested-Withdéclenche une requête préalable CORS, il faut donc une configuration CORS pour son origine. - Méthodes à navigateur (clé d’accès, scripts) : un client comme celui-ci ne peut pas les traiter ; proposez alors la page de connexion de TOSIAM, ou OpenID Connect.
Pour aller plus loin#
- Callbacks et formulaire : l’échange REST pas à pas avec
curl, et comment la page de connexion de TOSIAM génère son formulaire à partir des callbacks. - Graphes au format JSON : écrire, exporter et importer des graphes.
- Application web Spring Boot avec OpenID Connect : se connecter par la page de TOSIAM, sans que l’application voie le mot de passe.
Mis à jour le