TutorielGuides

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.

texte
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 :

Éditeur de graphes de la console : graphe connexion-api, Identifiant puis Mot de passe, Vérification datastore vers Succès, ou vers Trois essais au plus (limite 2) qui revient au mot de passe ou mène à Échec Éditeur de graphes de la console : graphe connexion-api, Identifiant puis Mot de passe, Vérification datastore vers Succès, ou vers Trois essais au plus (limite 2) qui revient au mot de passe ou mène à Échec
Identifiant (1) et mot de passe (2) sur deux étapes, vérification dans le datastore (3) et limite de tentatives (4) qui redemande le mot de passe ou termine en échec.
json· tosiam/connexion-api.json
{
  "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 :

texte· tosiam/ssoadm.cfg (extrait)
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|REQUISITE

update-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 :

Faites défiler le tableau
RessourceAccept-API-VersionUsage
/json/ref/authenticateresource=2.0, protocol=1.0Connexion
/json/ref/usersresource=2.0, protocol=1.0Profil de l’utilisateur
/json/ref/sessionsresource=1.1, protocol=1.0Validation, 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 :

javascript· cli/tosiam.js (extrait)
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 :

Faites défiler le tableau
RéponseSignification
200 avec authId et callbacksÉtape suivante : la remplir et la renvoyer
200 avec tokenIdSession ouverte ; tokenId est le jeton de session
401 Authentication failedIdentifiants refusés par une chaîne, ou essais épuisés dans un graphe
408 Session has timed outauthId expiré, ou déjà utilisé par une tentative qui a échoué : il faut recommencer depuis le début
400 No Configuration foundAucune 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…).

javascript· cli/callbacks.js (extrait)
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;
}
Faites défiler le tableau
CallbackCe qu’en fait le client
NameCallbackDemande le texte ; TOSIAM_USER répond à la première demande seulement
PasswordCallbackDemande 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, ConfirmationCallbackPropose 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
TextOutputCallbackAffiche 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
HiddenValueCallbackRenvoie 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 typeS’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.

javascript· cli/tosiam.js (extrait)
/** 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.

Faites défiler le tableau
AppelRé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/dduckLe 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#

bash
cd tosiam-rest-auth
./up.sh
cd cli
node connexion.js
texte
Connexion à 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 : non

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

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

bash
TOSIAM_USER=dduck TOSIAM_PASSWORD="$MOT_DE_PASSE" node connexion.js
node connexion.js --service ldapService      # la chaîne par défaut du royaume

Une valeur refusée n’est pas renvoyée :

texte
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#

Faites défiler le tableau
SymptômeCause probable
403 sur chaque appelEn-tête X-Requested-With ou Accept-API-Version absent
404 Resource '' not found sur /sessionsAccept-API-Version: resource=2.0 au lieu de resource=1.1
400 No Configuration foundGraphe non exposé : instance de module ou configuration d’authentification manquante, ou nom différent
408 Session has timed out en renvoyant une étapeauthId expiré ou déjà utilisé par une tentative échouée : recommencer depuis la première requête
Le mot de passe est redemandé sans messageRefus 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 essaiEssais 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 boucleGraphe 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éussieSession 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 : RetryLimitNode borne 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-With dé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#

Mis à jour le