02 Auth DAP IoBase

Enrôlement, tokens single-use, mutex, persistance, retry 401, refresh on WAN-up
⚠ Point le plus sensible du projet
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'admin Terega + recommencer toute la procédure d'enrôlement. Toute l'architecture ci-dessous 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 {"name": "<device_name>"} 200 + {device_code} 403 name déjà pris
/Token POST {"device_code": "<...>", "client_id": "<device_name>"} 200 + {access_token, refresh_token} 425 en attente validation admin Terega
/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) ────────────┐ │ │ inj DAP register (manuel) ───▶│ build_dap_register ───▶ POST /register │ │ │ │ ◀── 200 device_code ─── handle_dap_resp │ │ │ │ │ ├─▶ global.dap_device_code = code │ │ └─▶ persist_dap → save_dap_state → /mnt/data/.../json │ │ │ │ ┌──────── Validation manuelle Jean-Marc dans IoBase ──────┐ │ │ │ (sans cette étape, /Token retournera 425 Pending) │ │ │ └──────────────────────────────────────────────────────────┘ │ │ │ inj DAP get token (manuel) ──▶│ build_dap_token (mutex) ─▶ POST /Token │ │ │ │ ◀── 200 access + refresh ─── handle_dap_resp │ │ │ │ │ ├─▶ global.iobase_token = access │ │ ├─▶ global.iobase_refresh_token = refresh │ │ ├─▶ reset mutex │ │ └─▶ persist_dap → save_dap_state │ └──────────────────────────────────────────────────────────────┘ ┌─────────── Phase 2 : Refresh automatique ──────────────────┐ │ │ cron 50 min ─────────────────▶│ build_dap_refresh (mutex) ─▶ POST /refresh │ sur 401 retry ───────────────▶│ │ sur WAN false→true ──────────▶│ │ │ │ │ ◀── 200 nouveaux tokens ─── handle_dap_resp │ │ │ │ │ ├─▶ globals updates │ │ ├─▶ reset mutex │ │ └─▶ persist_dap → save_dap_state │ └──────────────────────────────────────────────────────────────┘

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 Jean-Marc. 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 en échec est mise dans la queue offline (queueenabled=true) et sera replay automatiquement avec le nouveau token.

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 WAN longue (> 50 min, le cron ratera) on est sûr d'avoir un access_token valide avant que la queue ne se déverse → 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

Globals utilisés

global.dap_host
URL du DAP IoBase. Format 'host:port' (préfixe http:// ajouté par les builds).
global.cloudgate_device_name
Le name envoyé à /register et client_id envoyé à /Token + /refresh. Default : cloudgate-terega-01.
global.dap_device_code
Set par handle_dap_resp sur register OK. Persisté.
global.iobase_token
Access token Bearer. Set par token/refresh/restore. Persisté. Lu par les chaînes IoBase + Health.
global.iobase_refresh_token
Refresh token. Set par token/refresh/restore. Persisté.
global.dap_refreshing
Flag mutex. true pendant un refresh ou Token en cours. Reset par handle_dap_resp.
global.dap_token_ts
Timestamp epoch du dernier token reçu. Pour debug/observabilité.

Procédure de premier enrôlement

  1. Set global.dap_host par inject startup ou console Lua → URL réelle Terega
  2. Tirer l'inject DAP register (manuel)
  3. Vérifier dans le canvas debug : DAP_REGISTER_OK device_code=DC...
  4. Demander à Jean-Marc (Terega) de valider le device dans le portail IoBase → « Services et équipements »« Valider l'équipement »
  5. Une fois validé, tirer l'inject DAP get token
  6. Vérifier DAP_TOKEN_OK token_len=... dans le debug
  7. À partir de là, le cron DAP refresh (50 min) prend le relais automatiquement
À partir d'ici tout est automatique
Sans intervention humaine, les tokens se rafraîchissent toutes les 50 minutes, sont persistés en flash, sont restaurés en cas de reboot, et la chaîne IoBase a toujours un Bearer valide. Si un POST IoBase prend 401 (timing pathologique), le refresh auto se déclenche immédiatement et la trame est replay.