É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 pardansLesHorairesouhorsHoraires; - « Code du jour » (
CodeDuJourNode) : demande un code dans un champ masqué, le compare au code réglé dans le graphe, sort parcorrectouincorrect, 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 :
@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éthodedefaultdonne la valeur par défaut. TOSIAM construit le nœud en passant ces réglages au constructeur. - Les sorties de
OutcomeProvidersont 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 :
HorairesAccesNodelit tous ses réglages dans un mêmetry, écrit le problème dans le journal de TOSIAM et sort parhorsHoraires(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 :
@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
incorrectpasse par unRetryLimitNodequi 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.
getSecretOrNullrend une copie : le nœud efface sa copie, et la valeur soumise elle-même (find(...).ifPresent(SubmittedCallbackValue::clearSecrets)) avantconsume. - Le réglage
codeest 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 :secretne 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.
@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.ExempleNodesModuleSans 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 :
| Profil | Dépendance | Quand |
|---|---|---|
tosiam-local (par défaut) | org.tst.tosiam:tosiam-graph:3.38.0-SNAPSHOT | Après un build local de TOSIAM (mvn install), sans jeton |
tosiam-github | org.tst.tosiam:tosiam-server-lib:3.37.0, sur GitHub Packages | Sans build local ; jeton GitHub déclaré dans settings.xml (serveur tosiam_github) |
mvn package # build local de TOSIAM
mvn -Ptosiam-github package # sinonLes nœuds se testent sans TOSIAM : ConfigBinder construit les réglages, un GraphContext simule le graphe. Par exemple :
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 :
"$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 upUne fois TOSIAM redémarré, les deux types figurent dans la liste des nœuds connus, avec leurs réglages (session d’administrateur) :
curl -s -b "iPlanetDirectoryPro=…" -H "Accept: application/vnd.tosiam+json" \
http://localhost:8080/tosiam/api/authgraph-node-typesExtrait de la réponse :
{ "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 :
"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" } }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-horairesTOSIAM 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 :
É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#loginPendant 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 :
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#
| Symptôme | Cause probable |
|---|---|
mvn : Could not resolve dependencies … tosiam-graph | Pas 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 utilisateurs | Jar 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-types | Module sans @GuiceModule, ou absent de META-INF/services/com.google.inject.AbstractModule |
| Erreurs 500 partout après avoir changé un jar | Jar retiré ou remplacé pendant que TOSIAM tournait : redémarrez TOSIAM |
| Le code est demandé en pleine journée | Fuseau ou jours du nœud horaires |
| « Authentication failed » après le mot de passe pour un seul utilisateur | Le 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#
- Graphes d’authentification : le moteur, les nœuds livrés, la validation.
- Un graphe construit dans la console : Construire un graphe dans l’éditeur.
- Un nœud avec son propre écran dans la nouvelle interface de connexion, et un projet généré par le plugin Maven de TOSIAM : Nœud d’authentification personnalisé.
Mis à jour le