Aller au contenu

Authentification (HMAC)

Tous les appels backend → relais sont signés avec le même schéma HMAC (parfois appelé « HMAC#1 »), identique pour l’Auth Relay et le Chat Relay. Apprenez-le ici ; les pages produit ne font qu’y renvoyer.

SurfaceAuthentification
Endpoints */relay/* (backend → relais)Signature HMAC (cette page)
Endpoints d’administration (/provision/*, /rotate/*, /fetch/*…)En-tête X-Admin-Key
Endpoints d’appareil de l’Auth Relay (/device/*)Authorization: Bearer <jeton>
Console du Chat Relay (/console/*)Authorization: Bearer <console_token>
Callbacks (relais → votre backend)Même signature HMAC, que vous vérifiez — voir Webhooks & callbacks

Chaque requête signée porte :

En-têteValeur
X-Bloonio-Tenant-IdVotre tenant_id.
X-Bloonio-TimestampL’horodatage Unix courant en millisecondes.
X-Bloonio-Signaturehex( hmac_sha256( tenant_secret, "{timestamp_ms}.{sha256_hex(body)}" ) )
  1. Sérialisez le corps de la requête exactement tel qu’il sera envoyé (mêmes octets). Pour une requête sans corps (GET), utilisez une chaîne vide.
  2. Calculez body_hash = sha256_hex(body).
  3. Relevez timestamp_ms, l’heure Unix courante en millisecondes.
  4. Construisez la chaîne à signer : "{timestamp_ms}.{body_hash}".
  5. Calculez signature = hmac_sha256(tenant_secret, chaîne_à_signer) en hexadécimal.
  6. Envoyez les trois en-têtes ci-dessus avec la requête.
import hashlib
import hmac
import json
import time
import httpx
TENANT_ID = "tnt_..." # depuis le provisioning
TENANT_SECRET = "sk_..." # secret, côté serveur uniquement
BASE_URL = "$BASE_URL" # ex. https://auth-relay.bloonio.com
def signed_headers(tenant_id: str, tenant_secret: str, body: bytes) -> dict:
ts_ms = str(int(time.time() * 1000))
body_hash = hashlib.sha256(body).hexdigest()
signing_string = f"{ts_ms}.{body_hash}"
signature = hmac.new(
tenant_secret.encode(),
signing_string.encode(),
hashlib.sha256,
).hexdigest()
return {
"X-Bloonio-Tenant-Id": tenant_id,
"X-Bloonio-Timestamp": ts_ms,
"X-Bloonio-Signature": signature,
"Content-Type": "application/json",
}
payload = {"example": "value"}
body = json.dumps(payload).encode() # ces octets exacts sont signés ET envoyés
resp = httpx.post(
f"{BASE_URL}/api/v1/relay/...",
headers=signed_headers(TENANT_ID, TENANT_SECRET, body),
content=body, # on envoie `content`, pas `json=`, pour figer les octets
timeout=10,
)
resp.raise_for_status()

À réception, le relais :

  1. exige la présence des trois en-têtes ;
  2. vérifie que X-Bloonio-Timestamp est dans une fenêtre d’environ 30 secondes (anti-rejeu temporel) ;
  3. vérifie que le tenant_id existe et que le tenant est actif (sinon 403) ;
  4. rejette toute signature déjà vue (protection anti-rejeu, sur une courte fenêtre configurable) ;
  5. recalcule la signature et la compare en temps constant.
SymptômeCause probable
401 signature invalideLe corps signé diffère du corps envoyé, ou mauvais tenant_secret.
401 horodatage hors fenêtreHorloge décalée, ou requête rejouée trop tard. Synchronisez via NTP.
403 tenant inactifTenant suspendu — voir Cycle de vie d’un tenant.
401 rejeu détectéLa même signature a déjà été utilisée ; générez un nouvel horodatage.