TutorielGuides

Écrire un nœud d'authentification en Java

Sur cette page

Quand les nœuds livrés avec TOSIAM et les scripts ne suffisent pas, un graphe peut utiliser des nœuds écrits en Java : une règle métier, un appel à un service interne, une vérification propre à votre organisation. Ce tutoriel écrit deux nœuds :

  • « Horaires d’accès » (HorairesAccesNode) : une décision sans écran, qui regarde l’heure et le jour dans un fuseau horaire et sort par dansLesHoraires ou horsHoraires ;
  • « Code du jour » (CodeDuJourNode) : demande un code dans un champ masqué, le compare au code réglé dans le graphe, sort par correct ou incorrect, et affiche « Code incorrect. » au-dessus d’un nouveau champ après une erreur.

Le graphe connexion-horaires les utilise : identifiant, mot de passe, puis, en dehors des horaires de bureau, le code du jour en plus. Les deux nœuds n’utilisent que des écrans standard (champ masqué, message), que la page de connexion XUI affiche sans code JavaScript supplémentaire.

Le code est dans le dépôt tosiam-samples, dossier tosiam-custom-node : un projet Maven qui produit un jar de quelques kilo-octets. Pour un nœud qui a besoin d’un écran sur mesure dans la nouvelle interface de connexion, voir Nœud d’authentification personnalisé.

Étape 1 : la forme d’un nœud#

Un nœud est une classe qui implémente GraphNode. À chaque passage, sa méthode process reçoit le contexte du graphe et rend un NodeResult : une sortie (NodeResult.goTo("…")) ou une demande de saisie (NodeResult.requestInput(callbacks)). L’annotation @NodeMetadata dit à TOSIAM le type du nœud (le nom qu’on écrit dans le graphe), l’interface de ses réglages et la liste de ses sorties :

java
@NodeMetadata(type = "HorairesAccesNode",
        configClass = HorairesAccesNode.Config.class,
        outcomeProvider = HorairesAccesNode.Outcomes.class)
public class HorairesAccesNode implements GraphNode {

    public interface Config {
        @Attribute(order = 100, label = "Opening hour", description = "Hour the access opens, 0 to 23 (included)")
        default int heureOuverture() { return 8; }

        @Attribute(order = 200, label = "Closing hour", description = "Hour the access closes, 0 to 24 (excluded)")
        default int heureFermeture() { return 18; }

        @Attribute(order = 300, label = "Opening days", description = "Comma-separated French day names")
        default String jours() { return "LUNDI,MARDI,MERCREDI,JEUDI,VENDREDI"; }

        @Attribute(order = 400, label = "Time zone", description = "Time zone of the opening hours")
        default String fuseau() { return "Europe/Paris"; }
    }

    public static final class Outcomes implements OutcomeProvider {
        @Override
        public List<String> getOutcomes() { return List.of("dansLesHoraires", "horsHoraires"); }
    }

    private final Config config;

    public HorairesAccesNode(Config config) { this.config = config; }

    @Override
    public NodeResult process(GraphContext context) {
        // heure et jour courants dans le fuseau réglé → "dansLesHoraires" ou "horsHoraires"
    }
}
  • Les réglages sont les méthodes de l’interface Config : le nom de la méthode est la clé dans le graphe ("config": { "heureOuverture": 8 }), une méthode default donne la valeur par défaut. TOSIAM construit le nœud en passant ces réglages au constructeur.
  • Les sorties de OutcomeProvider sont celles qu’on relie dans le graphe ; le nœud ne doit en rendre aucune autre.
  • Un réglage invalide ne doit pas faire échouer la connexion en erreur : HorairesAccesNode lit tous ses réglages dans un même try, écrit le problème dans le journal de TOSIAM et sort par horsHoraires (heure hors limites, « 8h » au lieu d’un nombre, jour ou fuseau inconnu).
  • Quand la fermeture est plus petite que l’ouverture (22 → 6), la nuit appartient au jour d’ouverture : avec les jours ouvrés, la nuit de vendredi à samedi est ouverte, celle de dimanche à lundi ne l’est pas.

Étape 2 : demander une saisie, afficher une erreur#

CodeDuJourNode demande le code dans un PasswordCallback. Au premier passage, rien n’est soumis : il rend l’écran. Au passage suivant, il lit la saisie, la retire (consume) pour que le nœud suivant ne la voie pas, la compare et choisit sa sortie :

java
@Override
public List<Callback> getInitialCallbacks() {
    return List.of(new PasswordCallback("Code du jour", false));
}

@Override
public NodeResult process(GraphContext context) throws NodeProcessingException {
    String attendu = config.code();
    if (attendu == null || attendu.isBlank()) {
        throw new NodeProcessingException("CodeDuJourNode: the « code » setting is not set");
    }
    char[] saisi = context.submittedCallbacks().getSecretOrNull(SHARED_STATE_PASSWORD);
    if (saisi == null) {
        if (context.getShared(ERREUR) == null) {
            return NodeResult.requestInput(getInitialCallbacks());
        }
        context.removeShared(ERREUR);
        return NodeResult.requestInput(List.of(
                new TextOutputCallback(TextOutputCallback.ERROR, "Code incorrect."), getInitialCallbacks().get(0)));
    }
    context.submittedCallbacks().consume(SHARED_STATE_PASSWORD);
    boolean correct = egal(saisi, attendu);      // MessageDigest.isEqual : temps constant
    Arrays.fill(saisi, '\0');
    if (correct) {
        context.removeShared(ERREUR);
        return NodeResult.goTo("correct");
    }
    context.putShared(ERREUR, Boolean.TRUE);
    return NodeResult.goTo("incorrect");
}
  • Le message d’erreur : la sortie incorrect passe par un RetryLimitNode qui ramène au nœud. Le nœud, rappelé sans saisie, trouve sa note dans l’état partagé du graphe (putShared / getShared) et affiche le message avant un nouveau champ.
  • Le code saisi est comparé en temps constant et jamais écrit dans un journal. getSecretOrNull rend une copie : le nœud efface sa copie, et la valeur soumise elle-même (find(...).ifPresent(SubmittedCallbackValue::clearSecrets)) avant consume.
  • Le réglage code est déclaré secret = true : la liste des types de nœuds le décrit comme un secret (champ de type mot de passe). Il a une valeur par défaut vide : sans elle, un graphe qui oublie le réglage ferait échouer le nœud par une erreur Java (AbstractMethodError). Le nœud écrit la raison dans le journal avant de lever son exception : TOSIAM ne journalise que la classe de l’exception d’un nœud. Le code reste en clair dans le graphe et ses exports : secret ne change que sa description.
  • Seuls les callbacks Name, Password, Hidden et Choice soumis arrivent à un nœud ; les invites des nœuds livrés avec TOSIAM sont traduites, les vôtres s’affichent telles qu’écrites.

Étape 3 : enregistrer les nœuds auprès de TOSIAM#

TOSIAM découvre les nœuds au démarrage par un module Guice : une classe annotée @GuiceModule qui ajoute les classes des nœuds à un ensemble.

java
@GuiceModule
public class ExempleNodesModule extends AbstractModule {
    @Override
    protected void configure() {
        Multibinder<Class<? extends GraphNode>> nodes = Multibinder.newSetBinder(binder(), new TypeLiteral<>() { });
        nodes.addBinding().toInstance(HorairesAccesNode.class);
        nodes.addBinding().toInstance(CodeDuJourNode.class);
    }
}

Le module est listé dans le fichier src/main/resources/META-INF/services/com.google.inject.AbstractModule du jar :

org.tst.tosiam.samples.nodes.ExempleNodesModule

Sans ce fichier, ou sans l’annotation, TOSIAM ignore le module : le graphe échoue à l’exécution sur un type inconnu.

Étape 4 : construire et tester#

Le pom.xml déclare la bibliothèque des nœuds en provided : TOSIAM la fournit à l’exécution, et le jar ne contient que les classes de l’exemple. Deux profils Maven :

Faites défiler le tableau
ProfilDépendanceQuand
tosiam-local (par défaut)org.tst.tosiam:tosiam-graph:3.38.0-SNAPSHOTAprès un build local de TOSIAM (mvn install), sans jeton
tosiam-githuborg.tst.tosiam:tosiam-server-lib:3.37.0, sur GitHub PackagesSans build local ; jeton GitHub déclaré dans settings.xml (serveur tosiam_github)
bash
mvn package                    # build local de TOSIAM
mvn -Ptosiam-github package    # sinon

Les nœuds se testent sans TOSIAM : ConfigBinder construit les réglages, un GraphContext simule le graphe. Par exemple :

java
CodeDuJourNode node = new CodeDuJourNode(new ConfigBinder().bind(CodeDuJourNode.Config.class, Map.of("code", "2468")));
PasswordCallback champ = new PasswordCallback("Code du jour", false);
champ.setPassword("1357".toCharArray());
assertEquals("incorrect", node.process(new GraphContext(sharedState, new Callback[] {champ})).getOutcome());

L’exemple compte 20 tests (horaires de part et d’autre de minuit, jours, fuseaux, réglages invalides, message après un code faux, code consommé, module Guice, graphe de l’exemple qui relie toutes les sorties).

Étape 5 : déployer dans TOSIAM#

Le jar se copie dans WEB-INF/lib de TOSIAM, puis TOSIAM redémarre : il ne charge les nœuds qu’à son démarrage. Avec la QuickStart :

bash
"$TOSIAM_HOME"/bin/tosiam.sh down
cp target/tosiam-custom-node-1.0.0.jar "$TOSIAM_HOME"/apache-tomcat-11.0.14/webapps/tosiam/WEB-INF/lib/
"$TOSIAM_HOME"/bin/tosiam.sh up

Une fois TOSIAM redémarré, les deux types figurent dans la liste des nœuds connus, avec leurs réglages (session d’administrateur) :

bash
curl -s -b "iPlanetDirectoryPro=…" -H "Accept: application/vnd.tosiam+json" \
  http://localhost:8080/tosiam/api/authgraph-node-types

Extrait de la réponse :

json
{ "type": "CodeDuJourNode", "providesIdentity": false,
  "attributes": [ { "name": "code", "label": "Code", "type": "password", "required": true, "secret": true } ] }

Étape 6 : un graphe qui les utilise#

La palette de l’éditeur ne propose que les nœuds livrés avec TOSIAM : un graphe qui utilise des nœuds personnalisés s’écrit en JSON et se crée par l’API, par import ou par ssoadm. tosiam/connexion-horaires.json :

json
"horaires": { "type": "HorairesAccesNode",
  "config": { "heureOuverture": 8, "heureFermeture": 18, "jours": "LUNDI,MARDI,MERCREDI,JEUDI,VENDREDI", "fuseau": "Europe/Paris" },
  "outcomes": { "dansLesHoraires": "succes", "horsHoraires": "code" } },
"code": { "type": "CodeDuJourNode", "config": { "code": "2468" },
  "outcomes": { "correct": "succes", "incorrect": "essais" } },
"essais": { "type": "RetryLimitNode", "config": { "retryLimit": 2 },
  "outcomes": { "true": "code", "false": "echec" } }
bash
ssoadm create-auth-graph    --realm ref --name connexion-horaires --datafile connexion-horaires.json
ssoadm create-auth-instance --realm ref --name connexion-horaires --authtype AuthGraph
ssoadm update-auth-instance --realm ref --name connexion-horaires --attributevalues tosiam-auth-graph-id=connexion-horaires

TOSIAM ne vérifie les types de nœuds qu’à l’exécution : créé sans le jar, le graphe fait échouer chaque connexion. Ouvert dans l’éditeur, il s’affiche, mais les nœuds personnalisés n’y ont ni port de sortie, ni lien dessiné, ni réglage :

Éditeur de graphes, connexion-horaires : les nœuds Horaires d'accès (HorairesAccesNode) et Code du jour (CodeDuJourNode) sans port de sortie ni lien sortant ; Trois codes au plus relié au Code du jour et à l'Échec Éditeur de graphes, connexion-horaires : les nœuds Horaires d'accès (HorairesAccesNode) et Code du jour (CodeDuJourNode) sans port de sortie ni lien sortant ; Trois codes au plus relié au Code du jour et à l'Échec
« Horaires d’accès » (1) et « Code du jour » (2) : l’éditeur ne connaît pas leurs sorties. Il les garde pourtant : un enregistrement depuis l’éditeur conserve leurs liens et leurs réglages.

Étape 7 : se connecter#

Connectez-vous avec la page XUI et le graphe de l’exemple :

http://localhost:8080/tosiam/XUI/?realm=/ref&authIndexType=module&authIndexValue=connexion-horaires#login

Pendant les horaires de bureau, l’identifiant et le mot de passe suffisent. En dehors, TOSIAM demande aussi le code du jour, 2468. Un code faux affiche le message du nœud :

Page XUI : Code incorrect., champ masqué Code du jour, bouton Se connecter Page XUI : Code incorrect., champ masqué Code du jour, bouton Se connecter
Le message renvoyé par CodeDuJourNode après un code faux (1) et le nouveau champ (2). Après trois codes faux, la connexion est refusée.

En cas de problème#

Faites défiler le tableau
SymptômeCause probable
mvn : Could not resolve dependencies … tosiam-graphPas de build local de TOSIAM : mvn install dans le dépôt tosiam, ou mvn -Ptosiam-github package avec un jeton
La connexion échoue juste après le mot de passe, pour tous les utilisateursJar absent, ou TOSIAM pas redémarré depuis la copie : le type du nœud est inconnu
Le type n’apparaît pas dans authgraph-node-typesModule sans @GuiceModule, ou absent de META-INF/services/com.google.inject.AbstractModule
Erreurs 500 partout après avoir changé un jarJar retiré ou remplacé pendant que TOSIAM tournait : redémarrez TOSIAM
Le code est demandé en pleine journéeFuseau ou jours du nœud horaires
« Authentication failed » après le mot de passe pour un seul utilisateurLe compte a été bloqué 15 minutes par trois mots de passe faux dans un graphe qui borne les essais (TOS-1707) ; ce graphe-ci ne bloque pas. Un redémarrage de TOSIAM lève le blocage

Pour aller plus loin#

Mis à jour le