T2 Authentification IoBase

Enrôlement, tokens single-use, mutex, persistance, retry 401, refresh on WAN-up
⚠ Mécanisme à risque single-use
Les tokens DAP sont single-use. Un appel concurrent à /Token ou /refresh avec le même token = device en erreur permanent. Recovery = suppression manuelle du device par l'administrateur IoBase + recommencer toute la procédure d'enrôlement. Toute l'architecture ci-dessous existe pour rendre cette situation impossible.
ÉtapeAppelCe qui en sortBloquant
1 · Inscription/registerun code d'appareil —
2 · Validationaucun— humain, côté plateforme — sinon 425
3 · Premier jeton/token jeton d'accès + jeton de renouvellement—
4 · Renouvellement/refreshdeux nouveaux jetons toutes les 12 h, automatique
Le jeton de renouvellement est à usage unique

L'utiliser deux fois met l'équipement en erreur permanente côté plateforme, et la récupération passe par une intervention humaine. Tout le reste de ce chapitre — verrou, garde des six heures, cloisonnement — existe pour rendre cette situation impossible.

Source de vérité

La doc officielle : Centre d'aide io-base · Procédure d'authentification via le DAP (extraits ci-dessous).

Endpoints DAP

EndpointMéthodeBodyRéponse OKRéponse KO
/register POST {"client_id": "<device_name>"} 200 + {device_code} 403 name déjà pris, 400 "Missing mandatory parameter(s) client_id" si mauvaise clé
/Token POST {"device_code": "<...>", "client_id": "<device_name>"} 200 + {access_token, refresh_token} 425 en attente validation administrateur IoBase
/refresh POST {"refresh_token": "<...>", "client_id": "<device_name>"} 200 + {access_token, refresh_token} (nouveaux) 401 refresh_token déjà consommé
Single-use rappel
Citation doc Intercom : « un refresh_token ne peut être utilisé qu'une seule fois » et « si le même token est utilisé deux fois, l'équipement se met en erreur ». La récupération demande « suppression du device par un administrateur dans Io-base, et il faut recommencer la procédure ».

Cycle de vie

Phase 1 — Enrôlement (1 seule fois)

1
Inject manuel · DAP register
build_dap_register émet un POST /register avec body {"client_id":"<device_name>"}.
opérateur clique sur le bouton inject du canvas
2
Réponse 200 · device_code généré
handle_dap_resp stocke global.dap_device_code et persiste l'état dans /mnt/data/luvitred/dap_state.json.
device_code single-use · ne sera consommé qu'une fois par /Token
3
Validation manuelle · administrateur IoBase
Action humaine côté portail IoBase : « Services et équipements » → « Valider l'équipement ». Sans cette étape, l'appel suivant retournera HTTP 425 Pending.
étape bloquante hors canvas · attend l'admin IoBase
4
Inject manuel · DAP get token
build_dap_token (mutex protégé) émet un POST /Token avec body {"device_code","client_id"}.
opérateur clique après confirmation de validation
5
Réponse 200 · access + refresh
handle_dap_resp stocke global.iobase_token + iobase_refresh_token, reset le mutex, persiste flash.
à partir d'ici, les chaînes IoBase ont un Bearer valide

Phase 2 — Refresh automatique (en continu)

↻
Triggers · 4 sources possibles
a. cron périodique repeat=43200 (toutes les 12 h)
b. au boot : inject once=true + onceDelay=120 (refresh ~120 s après démarrage, le temps que wwan0 monte)
c. 401 reçu sur un POST IoBase ou Health → fn_trigger_refresh_on_401
d. transition WAN false→true (retour de connectivité après une coupure longue)
mutex global.dap_refreshing · empêche tout appel concurrent
Fix 2026-06-10 : le boot-trigger (b) a été ajouté (once était à false) — sans lui, après chaque reboot le timer 12 h repartait de zéro et le refresh ne se déclenchait jamais si la passerelle redémarrait avant 12 h. Le refresh_token (single-use) n'est consommé que si le POST aboutit, donc tenter au boot est sûr.
→
POST /refresh
body {"refresh_token","client_id"} · une seule requête à la fois grâce au mutex.
✓
Réponse 200 · nouveaux tokens
Globals mis à jour, mutex reset, fichier flash dap_state.json ré-écrit avant tout usage du nouveau Bearer.
si KO 401 (refresh_token déjà consommé) → device en erreur, re-enrôlement obligatoire

Les 5 protections critiques

1 · Mutex single-use

Au début de build_dap_token et build_dap_refresh, on vérifie le flag global.dap_refreshing. Si déjà true, on log DAP_REFRESH_SKIP mutex et on retourne nil. Sinon on le set à true et on continue.

Dans handle_dap_resp, le flag est reset à false aussi bien sur succès que sur échec, garantissant qu'aucun deadlock n'est possible.

2 · Persistance flash

À chaque token update (register, token, refresh), handle_dap_resp propage msg.dap_persist contenant la nouvelle state. fn_persist_dap attache msg.filename = '/mnt/data/luvitred/dap_state.json', le node save dap state (file out, overwriteFile=true) écrit. Format :

{
  "device_code":   "DC...",
  "access_token":  "AT...",
  "refresh_token": "RT...",
  "saved_at":      1714056789
}

3 · Restore au boot

Au démarrage du flow (deploy ou reboot CG) :

  1. inj_load_dap_state (once @startup, +3s)
  2. file_load_dap_state lit le JSON depuis flash
  3. fn_restore_dap_state parse via regex (sandbox Lua n'a pas json.decode garanti) et set les globals

Résultat : un reboot CG ne nécessite pas de re-enrôlement administrateur IoBase. Les tokens restent valides jusqu'à expiration naturelle.

4 · Retry sur 401

handle_iobase_resp et handle_health_resp émettent msg avec statusCode=401 vers fn_trigger_refresh_on_401 qui filtre, puis émet vers build_dap_refresh. Le mutex protège des refreshes en rafale (par ex. 10 trames qui prennent 401 d'affilée → 1 seul refresh est tiré).

La trame qui a essuyé le 401 est perdue : il n'existe pas de file de rejeu sur cette passerelle. Le renouvellement protège les trames suivantes, pas celle-là.

5 · Refresh préventif au retour de WAN

handle_cg_metrics (kind=internet) maintient global.wan_connected. Sur transition false → true, il émet un msg vers build_dap_refresh. Avantage : après une coupure longue on repart avec un jeton valide sans attendre la prochaine échéance du cron, qui n'a lieu que toutes les 12 heures → pas de boucle de 401.

Validation par tests

ScénarioTestRésultat attenduMesuré
Refresh concurrent Tirer 3 fois en parallèle l'inject DAP refresh Mock voit 1 POST /refresh, pas 3 delta = 1
Persistance après token Run register + Token, puis cat /mnt/data/luvitred/dap_state.json Fichier 159 bytes avec device_code + access + refresh + saved_at OK 159 bytes
Restore au boot Tirer load dap @startup (sans refaire register/Token) Globals repopulés depuis le fichier flash DAP_RESTORE_OK age=12s
Retry 401 Force un faux access_token, trigger un health POST Mock 401 → trigger refresh auto → has_access redevient True (delta DAP=1) DAP_REFRESH_OK delta=1
WAN false→true Logique en place (revue de code), pas testé en live (demande coupure WAN) Trigger refresh préventif code review only

Deux environnements, cloisonnés

La passerelle peut s'authentifier auprès de deux environnements IoBase distincts. Rien n'est partagé entre eux — c'est la condition pour que le mécanisme à usage unique reste sûr :

Environnement principalSecond environnement
Identifiantsdap_host, dap_client_id, dap_api_key les mêmes, suffixés _b
Jetonsiobase_token, iobase_refresh_token iobase_token_b, iobase_refresh_token_b
Verroudap_refreshingdap_refreshing_b
Fichier d'étatdap_state.jsondap_state_b.json
Déclencheurinjection DAP refreshinjection DAP refresh B

Chaque déclencheur porte son environnement dans le message ; les fonctions le lisent et choisissent le jeu de clés correspondant. Un renouvellement bloqué sur un environnement ne bloque pas l'autre, et un ré-enrôlement en cours sur l'un n'entraîne pas l'autre sur la mauvaise route.

Pourquoi un déclencheur par environnement, et non un seul qui se dédouble

Le moteur de la passerelle n'émet qu'un seul message par sortie. Une première version faisait éclater un déclencheur unique en plusieurs messages : elle a interrompu le renouvellement des jetons pendant 48 heures, du 29 au 31 août 2026. La forme retenue — un déclencheur par environnement, chacun portant le sien — supprime le problème au lieu de le contourner.

Restauration au démarrage

Chaque environnement recharge son propre fichier d'état au démarrage. Sans cela, le second environnement perdrait son jeton de renouvellement à chaque redémarrage du flux — un jeton à usage unique, obtenu par un enrôlement validé par un administrateur : le perdre coûte un aller-retour de plusieurs jours avec la plateforme.

Le journal le montre au démarrage, un message par environnement :

DAP_RESTORE_OK        env='a'  has_refresh=true
DAP_REFRESH_SKIP_FRESH env='a'  saved_age_s=2047
DAP_RESTORE_OK        env='b'  has_refresh=false

La garde DAP_REFRESH_SKIP_FRESH est essentielle : tout redéploiement redémarre les flux et rejoue cette restauration. Sans elle, chaque déploiement consommerait un jeton. Elle n'agit que si l'état a moins de six heures ; au-delà, un renouvellement part — c'est normal, mais c'est à savoir avant de déployer.

Variables utilisées

Voir la référence des variables globales pour la liste complète, qui indique aussi lesquelles se saisissent au cockpit. Les quatre valeurs de configuration (dap_host, dap_client_id, dap_api_key et leurs variantes _b) s'y modifient sans redéploiement du flux ; les jetons, eux, sont produits par l'enrôlement et ne se saisissent pas.

La valeur saisie l'emporte sur le fichier d'état

Le fichier d'état conserve une copie de l'hôte et des identifiants. Si une valeur est saisie au cockpit, c'est elle qui s'applique, y compris après un redémarrage ; le fichier d'état est un cache de survie, pas la source de vérité de la configuration. Vider le champ rend la main au fichier. Les jetons ne sont jamais figés par une saisie — les geler empêcherait leur restauration.

Premier enrôlement

  1. Renseigner dap_host, dap_client_id et dap_api_key dans le cockpit (onglet Globals, rôle admin)
  2. Déclencher l'injection DAP register depuis le canvas
  3. Vérifier DAP_REGISTER_OK device_code=… dans le journal
  4. Un administrateur de la plateforme valide l'équipement (« Services et équipements » → « Valider l'équipement »). Sans cette étape, l'appel suivant répond 425 Pending. C'est une dépendance externe : elle se demande tôt.
  5. Une fois validé, déclencher l'injection DAP get token
  6. Vérifier DAP_TOKEN_OK et l'écriture du fichier d'état

Le second environnement suit exactement la même procédure, avec ses propres identifiants et sa propre validation côté plateforme.

Ensuite, plus rien à faire

Le renouvellement est automatique : une tâche périodique s'exécute toutes les 12 heures, les jetons sont écrits sur la mémoire de la passerelle et rechargés à chaque redémarrage. Si un envoi essuie un 401, le renouvellement se déclenche immédiatement — la trame concernée est perdue, les suivantes passent.