@ollaid/native-sso 1.0.8 → 2.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -22,11 +22,12 @@ Package NPM Frontend-First pour l'authentification Native SSO Ollaid.
22
22
  13. [Migration Laravel](#migration-laravel)
23
23
  14. [Flux d'authentification](#flux-dauthentification)
24
24
  15. [Session & localStorage](#session--localstorage)
25
- 16. [OnboardingModal](#onboardingmodal)
26
- 17. [useTokenHealthCheck](#usetokenhealthcheck)
27
- 18. [Sécurité](#sécurité)
28
- 19. [Exports](#exports)
29
- 20. [Publication & Installation npm](#publication--installation-npm)
25
+ 16. [Déconnexion synchronisée](#déconnexion-synchronisée)
26
+ 17. [OnboardingModal](#onboardingmodal)
27
+ 18. [useTokenHealthCheck](#usetokenhealthcheck)
28
+ 19. [Sécurité](#sécurité)
29
+ 20. [Exports](#exports)
30
+ 21. [Publication & Installation npm](#publication--installation-npm)
30
31
 
31
32
  ---
32
33
 
@@ -155,21 +156,30 @@ déclenche la redirection côté frontend :
155
156
 
156
157
  ## Déconnexion externe (sans le composant)
157
158
 
158
- Quand l'utilisateur se déconnecte **depuis votre SaaS** (ex : bouton logout dans le backoffice), le composant `NativeSSOPage` n'est pas impliqué. Vous devez donc nettoyer manuellement la session SSO pour éviter que le package considère l'utilisateur comme encore connecté.
159
+ Quand l'utilisateur se déconnecte **depuis votre SaaS** (ex : bouton logout dans le backoffice), le composant `NativeSSOPage` n'est pas impliqué. Vous devez utiliser la fonction `logout()` du package pour garantir une déconnexion complète et synchronisée.
160
+
161
+ > ⚠️ **RÈGLE OBLIGATOIRE** : Toute déconnexion frontend **DOIT** passer par `logout()`. Ne jamais effacer le `localStorage` manuellement ni utiliser `clearAuthToken()` seul — cela laisserait des sessions orphelines actives sur l'IAM.
159
162
 
160
163
  ### Utilisation
161
164
 
162
165
  ```tsx
163
- import { clearAuthToken } from '@ollaid/native-sso';
166
+ import { logout } from '@ollaid/native-sso';
164
167
 
165
168
  const handleLogout = async () => {
166
- await revokeBackendToken(); // votre logique SaaS (révocation Sanctum, etc.)
167
- clearAuthToken(); // nettoie les 5 clés localStorage du package
169
+ await logout(); // Double revocation (SaaS + IAM) + nettoyage localStorage
168
170
  navigate('/auth/login');
169
171
  };
170
172
  ```
171
173
 
172
- ### Clés localStorage nettoyées par `clearAuthToken()`
174
+ ### Que fait `logout()` ?
175
+
176
+ 1. **Révoque le token SaaS** — `POST /api/native/logout` (supprime le Sanctum token)
177
+ 2. **Révoque la session IAM** — `POST /api/iam/disconnect` (avec `sanctum_token` + `app_access_token_ref`)
178
+ 3. **Nettoie le localStorage** — supprime les 6 clés du package
179
+
180
+ Les appels réseau sont en `Promise.allSettled` (best-effort) : même si le serveur est injoignable, le localStorage est **toujours** nettoyé.
181
+
182
+ ### Clés localStorage nettoyées
173
183
 
174
184
  | Clé | Description |
175
185
  |-----|-------------|
@@ -178,8 +188,23 @@ const handleLogout = async () => {
178
188
  | `user` | Objet utilisateur (avec `iam_reference`, `alias_reference`) |
179
189
  | `account_type` | Type de compte (`user` ou `client`) |
180
190
  | `alias_reference` | Référence de l'alias de connexion |
191
+ | `app_access_token_ref` | Référence de l'`AppAccessToken` IAM (pour revocation optimisée) |
192
+
193
+ ### ⛔ `clearAuthToken()` est déprécié
194
+
195
+ `clearAuthToken()` ne fait que vider le localStorage **sans révoquer les sessions** côté SaaS et IAM. Son utilisation seule crée des sessions orphelines.
196
+
197
+ ```tsx
198
+ // ❌ NE PAS FAIRE — sessions orphelines sur l'IAM
199
+ import { clearAuthToken } from '@ollaid/native-sso';
200
+ clearAuthToken();
201
+
202
+ // ✅ FAIRE — double revocation garantie
203
+ import { logout } from '@ollaid/native-sso';
204
+ await logout();
205
+ ```
181
206
 
182
- > **Important :** Si vous ne nettoyez pas ces clés, l'utilisateur verra l'écran "Déconnexion" au lieu du formulaire de connexion en revenant sur la page SSO.
207
+ > **Important :** Si vous ne déconnectez pas proprement, l'utilisateur verra l'écran "Déconnexion" au lieu du formulaire de connexion en revenant sur la page SSO.
183
208
 
184
209
  ---
185
210
 
@@ -334,13 +359,14 @@ class NativeConfigController extends Controller
334
359
  $payload = json_encode(['secret_key' => $secretKey, 'ts' => time()]);
335
360
  $key = hash('sha256', $secretKey, true);
336
361
  $iv = random_bytes(16);
337
- $encrypted = openssl_encrypt($payload, 'AES-256-CBC', $key, 0, $iv);
338
- $encryptedCredentials = base64_encode($iv . '::' . $encrypted);
362
+ $encrypted = openssl_encrypt($payload, 'AES-256-CBC', $key, OPENSSL_RAW_DATA, $iv);
363
+ $encryptedCredentials = base64_encode($iv . '::' . base64_encode($encrypted));
339
364
 
340
365
  return response()->json([
341
366
  'success' => true,
342
367
  'app_key' => $appKey,
343
368
  'encrypted_credentials' => $encryptedCredentials,
369
+ 'iam_api_url' => config("services.{$prefix}.api_url", 'https://identityam.ollaid.com/api'),
344
370
  'credentials_ttl' => 300,
345
371
  'debug' => $debug,
346
372
  ]);
@@ -707,6 +733,7 @@ $secretKey = $payload['secret_key'];
707
733
  "success": true,
708
734
  "token": "1|abc123def456ghi789...",
709
735
  "expires_at": "2026-04-23T03:12:32.000000Z",
736
+ "app_access_token_ref": "aat_ref_XXXXXXXX",
710
737
  "user": {
711
738
  "id": 1,
712
739
  "reference": "USR-XXXXXXXX",
@@ -766,11 +793,15 @@ Route::post('/native/exchange', function (Request $request) {
766
793
  // Générer un token Sanctum
767
794
  $token = $user->createToken('native-sso')->plainTextToken;
768
795
 
796
+ // Récupérer app_access_token_ref depuis la réponse IAM
797
+ $appAccessTokenRef = $response->json('user_infos.app_access_token_ref');
798
+
769
799
  return response()->json([
770
- 'success' => true,
771
- 'token' => $token,
772
- 'expires_at' => now()->addDays(30)->toISOString(),
773
- 'user' => [
800
+ 'success' => true,
801
+ 'token' => $token,
802
+ 'expires_at' => now()->addDays(30)->toISOString(),
803
+ 'app_access_token_ref' => $appAccessTokenRef,
804
+ 'user' => [
774
805
  'id' => $user->id,
775
806
  'reference' => $user->reference,
776
807
  'alias_reference' => $user->alias_reference,
@@ -790,16 +821,15 @@ Route::post('/native/exchange', function (Request $request) {
790
821
 
791
822
  ### `POST /api/native/check-token`
792
823
 
793
- Vérifie la validité du token Sanctum et retourne les `user_infos` fraîches. Appelé périodiquement par le package (2 min après login, puis toutes les 5 min).
824
+ Vérifie la validité du token Sanctum et retourne les infos utilisateur fraîches. Appelé périodiquement par le package (60 secondes après login, puis toutes les 2 minutes).
794
825
 
795
826
  **Headers requis :** `Authorization: Bearer {token}`
796
827
 
797
828
  **Réponse succès (200) :**
798
829
  ```json
799
830
  {
800
- "success": true,
801
- "valid": true,
802
- "user_infos": {
831
+ "status": "connected",
832
+ "user": {
803
833
  "name": "John Doe",
804
834
  "email": "john@example.com",
805
835
  "ccphone": "+221",
@@ -821,9 +851,8 @@ Route::post('/native/check-token', function (Request $request) {
821
851
  $user = $request->user();
822
852
 
823
853
  return response()->json([
824
- 'success' => true,
825
- 'valid' => true,
826
- 'user_infos' => [
854
+ 'status' => 'connected',
855
+ 'user' => [
827
856
  'name' => $user->name,
828
857
  'email' => $user->email,
829
858
  'ccphone' => $user->ccphone,
@@ -916,17 +945,19 @@ class NativeAuthController extends Controller
916
945
  $aesKey = hash('sha256', $secretKey, true);
917
946
  $iv = random_bytes(16);
918
947
  $payload = json_encode([
919
- 'app_key' => $appKey,
920
948
  'secret_key' => $secretKey,
921
949
  'ts' => time(),
922
950
  ]);
923
951
 
924
- $encrypted = openssl_encrypt($payload, 'aes-256-cbc', $aesKey, 0, $iv);
952
+ $encrypted = openssl_encrypt($payload, 'aes-256-cbc', $aesKey, OPENSSL_RAW_DATA, $iv);
953
+ $encryptedCredentials = base64_encode($iv . '::' . base64_encode($encrypted));
925
954
 
926
955
  return response()->json([
927
956
  'success' => true,
928
957
  'app_key' => $appKey, // En clair (non sensible, sert d'identifiant pour l'IAM)
929
- 'encrypted_credentials' => base64_encode($iv . '::' . $encrypted),
958
+ 'encrypted_credentials' => $encryptedCredentials,
959
+ 'iam_api_url' => config('services.iam.api_url', 'https://identityam.ollaid.com/api'),
960
+ 'credentials_ttl' => 300,
930
961
  'debug' => (bool) config('services.iam.debug'), // Contrôlé par IAM_DEBUG dans .env
931
962
  ]);
932
963
  }
@@ -955,18 +986,17 @@ class NativeAuthController extends Controller
955
986
  ], 400);
956
987
  }
957
988
 
958
- // ⚠️ IMPORTANT : Remplacer les espaces par '+' (encodage URL)
959
- $callbackToken = str_replace(' ', '+', $callbackToken);
960
-
961
989
  try {
962
- $response = Http::timeout(30)->post(
963
- config('services.iam.base_url') . '/iam/auth/decrypt',
964
- [
965
- 'app_key' => config('services.iam.app_key'),
966
- 'secret_key' => config('services.iam.secret_key'),
967
- 'callback_token' => $callbackToken,
968
- ]
969
- );
990
+ $response = Http::timeout(30)
991
+ ->withHeaders([
992
+ 'Content-Type' => 'application/json',
993
+ 'Accept' => 'application/json',
994
+ 'X-IAM-App-Key' => config('services.iam.app_key'),
995
+ 'X-IAM-Secret-Key' => config('services.iam.secret_key'),
996
+ ])
997
+ ->post(config('services.iam.api_url') . '/iam/auth/decrypt', [
998
+ 'token' => $callbackToken,
999
+ ]);
970
1000
 
971
1001
  if (!$response->successful() || !$response->json('success')) {
972
1002
  $error = $response->json();
@@ -1024,11 +1054,22 @@ class NativeAuthController extends Controller
1024
1054
  $expiresAt = now()->addDays(30);
1025
1055
  $token = $user->createToken('native-sso', ['*'], $expiresAt);
1026
1056
 
1057
+ // Récupérer app_access_token_ref depuis la racine de la réponse IAM
1058
+ $appAccessTokenRef = $data['app_access_token_ref'] ?? null;
1059
+
1060
+ // Stocker la ref dans le token Sanctum pour le webhook de révocation
1061
+ if ($appAccessTokenRef) {
1062
+ $token->accessToken->forceFill([
1063
+ 'app_access_token_ref' => $appAccessTokenRef,
1064
+ ])->save();
1065
+ }
1066
+
1027
1067
  return response()->json([
1028
- 'success' => true,
1029
- 'token' => $token->plainTextToken,
1030
- 'expires_at' => $expiresAt->toIso8601String(),
1031
- 'user' => [
1068
+ 'success' => true,
1069
+ 'token' => $token->plainTextToken,
1070
+ 'expires_at' => $expiresAt->toIso8601String(),
1071
+ 'app_access_token_ref' => $appAccessTokenRef,
1072
+ 'user' => [
1032
1073
  'id' => $user->id,
1033
1074
  'reference' => $iamReference,
1034
1075
  'alias_reference' => $aliasReference,
@@ -1083,9 +1124,9 @@ class NativeAuthController extends Controller
1083
1124
  $user = $request->user();
1084
1125
 
1085
1126
  return response()->json([
1086
- 'success' => true,
1087
- 'valid' => true,
1088
- 'user_infos' => [
1127
+ 'status' => 'connected',
1128
+ 'message' => 'Utilisateur connecté',
1129
+ 'user' => [
1089
1130
  'name' => $user->name,
1090
1131
  'email' => $user->email,
1091
1132
  'ccphone' => $user->ccphone,
@@ -1099,21 +1140,46 @@ class NativeAuthController extends Controller
1099
1140
  }
1100
1141
 
1101
1142
  // ════════════════════════════════════════
1102
- // POST /api/native/logout
1143
+ // POST /api/native/logout (déconnexion synchronisée)
1103
1144
  // ════════════════════════════════════════
1104
1145
 
1105
1146
  /**
1106
- * Révoque UNIQUEMENT le token Sanctum courant (single-session).
1147
+ * Révoque le token Sanctum courant (single-session)
1148
+ * ET notifie l'IAM pour révoquer l'AppAccessToken lié.
1107
1149
  * Route protégée par auth:sanctum.
1108
1150
  */
1109
1151
  public function logout(Request $request): JsonResponse
1110
1152
  {
1111
- $request->user()->currentAccessToken()->delete();
1112
-
1113
- return response()->json([
1114
- 'success' => true,
1115
- 'message' => 'Déconnexion réussie',
1116
- ]);
1153
+ try {
1154
+ $user = $request->user();
1155
+
1156
+ if ($user) {
1157
+ $sanctumTokenPlain = $request->bearerToken();
1158
+ $currentToken = $user->currentAccessToken();
1159
+ $appAccessTokenRef = $currentToken?->app_access_token_ref ?? null;
1160
+
1161
+ // Supprimer le token APRÈS avoir lu la ref
1162
+ $currentToken?->delete();
1163
+
1164
+ // Notifier l'IAM (fire-and-forget, timeout 5s)
1165
+ if ($sanctumTokenPlain || $appAccessTokenRef) {
1166
+ $iamPrefix = $request->attributes->get('iam_prefix', 'iam');
1167
+ $iamApiUrl = config("services.{$iamPrefix}.api_url", 'https://identityam.ollaid.com/api');
1168
+ try {
1169
+ Http::timeout(5)->post("{$iamApiUrl}/iam/disconnect", array_filter([
1170
+ 'sanctum_token' => $sanctumTokenPlain,
1171
+ 'app_access_token_ref' => $appAccessTokenRef,
1172
+ ]));
1173
+ } catch (\Exception $e) {
1174
+ Log::warning("[NativeSSO] IAM disconnect failed (non-blocking)", ['error' => $e->getMessage()]);
1175
+ }
1176
+ }
1177
+ }
1178
+
1179
+ return response()->json(['success' => true, 'message' => 'Déconnexion réussie']);
1180
+ } catch (\Exception $e) {
1181
+ return response()->json(['success' => true, 'message' => 'Déconnexion effectuée']);
1182
+ }
1117
1183
  }
1118
1184
  }
1119
1185
  ```
@@ -1830,7 +1896,7 @@ Toutes les APIs IAM retournent le même objet `user_infos` avec exactement **9 c
1830
1896
 
1831
1897
  ## Session & localStorage
1832
1898
 
1833
- Le package utilise **5 clés** dans `localStorage` pour persister la session :
1899
+ Le package utilise **6 clés** dans `localStorage` pour persister la session :
1834
1900
 
1835
1901
  | Clé | Contenu | Source | Valeurs possibles |
1836
1902
  |-----|---------|--------|-------------------|
@@ -1839,6 +1905,7 @@ Le package utilise **5 clés** dans `localStorage` pour persister la session :
1839
1905
  | `user` | Objet utilisateur enrichi sérialisé en JSON | Réponse de `/api/native/exchange` ou mise à jour via health check | `{"iam_reference":"USR-XXX","alias_reference":"ALI-XXX","name":"...","email":"...",...}` |
1840
1906
  | `account_type` | Type de compte | Déterminé lors du `exchange` selon le mode d'inscription | `"user"` (défaut) ou `"client"` (inscription phone-only) |
1841
1907
  | `alias_reference` | Référence alias utilisée lors de la connexion | Réponse de `/api/native/exchange` (champ `user.alias_reference`) | `"ALI-XXXXXXXX"` |
1908
+ | `app_access_token_ref` | Référence de l'`AppAccessToken` IAM | Réponse de `/api/native/exchange` (champ `app_access_token_ref`) | `"42"` ou `"aat_ref_abc123"` |
1842
1909
 
1843
1910
  > **Note :** `account_type` et `alias_reference` sont stockés **séparément** de l'objet `user` en plus d'être inclus dans le JSON de la clé `user`.
1844
1911
 
@@ -1850,7 +1917,7 @@ L'objet `user` stocké en localStorage contient deux champs ajoutés automatique
1850
1917
 
1851
1918
  ### Nettoyage
1852
1919
 
1853
- Lors du `logout()`, les 5 clés sont supprimées via `clearAuthToken()`.
1920
+ Lors du `logout()`, les 6 clés sont supprimées via `clearAuthToken()`.
1854
1921
 
1855
1922
  ### Accès programmatique
1856
1923
 
@@ -1865,7 +1932,127 @@ const aliasRef = localStorage.getItem('alias_reference'); // string | null
1865
1932
 
1866
1933
  ---
1867
1934
 
1868
- ## OnboardingModal
1935
+ ## Déconnexion synchronisée
1936
+
1937
+ Le package implémente une **double revocation** ultra-fiable : il contacte **en parallèle** le SaaS **et** l'IAM pour garantir la révocation de toutes les sessions.
1938
+
1939
+ ### Architecture — Double Revocation
1940
+
1941
+ ```
1942
+ ┌─────────────────┐
1943
+ │ Package │──────────────────────────────────┐
1944
+ │ logout() │ │
1945
+ └──────┬──────────┘ │
1946
+ │ ① POST /native/logout │ ② POST /iam/disconnect
1947
+ │ (Sanctum token) │ (sanctum_token + app_access_token_ref)
1948
+ ▼ ▼
1949
+ ┌─────────────────┐ ┌─────────────────┐
1950
+ │ SaaS API │──③ fire-and-forget──────▶│ IAM API │
1951
+ │ │ POST /iam/disconnect │ │
1952
+ └─────────────────┘ └─────────────────┘
1953
+ ```
1954
+
1955
+ **Trois niveaux de sécurité :**
1956
+
1957
+ | Scénario | Résultat |
1958
+ |----------|----------|
1959
+ | ✅ Tout fonctionne | SaaS + IAM révoquent tous les deux |
1960
+ | ⚠️ SaaS injoignable | L'IAM révoque quand même via l'appel direct ② |
1961
+ | ⚠️ IAM injoignable | Le SaaS révoque via ① puis retente via ③ |
1962
+ | ❌ Les deux injoignables | localStorage nettoyé, le health check détectera le 401 plus tard |
1963
+
1964
+ ### Comportement automatique
1965
+
1966
+ Quand `useNativeAuth.logout()` est appelé (ou que l'utilisateur clique "Se déconnecter" dans `NativeSSOPage`) :
1967
+
1968
+ 1. **Appels parallèles** (via `Promise.allSettled`, jamais bloquants) :
1969
+ - `POST /api/native/logout` → SaaS supprime le Sanctum token + notifie l'IAM (fire-and-forget)
1970
+ - `POST /api/iam/disconnect` → IAM révoque directement l'`AppAccessToken` via `sanctum_token` + `app_access_token_ref` (lookup optimisé)
1971
+ 2. **Nettoyage local garanti** : les 6 clés localStorage sont supprimées (`token`, `auth_token`, `user`, `account_type`, `alias_reference`, `app_access_token_ref`)
1972
+ 3. **Aucun blocage** : même si les deux appels échouent (offline, timeout), la déconnexion locale est instantanée
1973
+
1974
+ > **Fiabilité** : `Promise.allSettled` + `.catch()` sur chaque appel. L'appel IAM a un timeout court (5s) pour ne jamais ralentir l'UX.
1975
+
1976
+ ### API IAM de revocation — `POST /api/iam/disconnect`
1977
+
1978
+ Le package contacte directement l'IAM pour révoquer l'`AppAccessToken` lié à la session.
1979
+
1980
+ | Paramètre | Type | Description |
1981
+ |-----------|------|-------------|
1982
+ | `sanctum_token` | `string` | Le token Sanctum stocké localement, utilisé pour identifier l'`AppAccessToken` à révoquer |
1983
+ | `app_access_token_ref` | `string` | (recommandé) Référence directe de l'`AppAccessToken` IAM — lookup instantané par PK |
1984
+
1985
+ L'IAM cherche d'abord par `app_access_token_ref` (lookup par PK, O(1)), puis fallback sur `sanctum_token` (index), puis `iam_token` (hash). Le passage de `app_access_token_ref` est **recommandé** pour des performances optimales.
1986
+
1987
+ ### Déconnexion externe (hors package)
1988
+
1989
+ Si votre application gère une déconnexion **en dehors** de `NativeSSOPage` (ex : backoffice, bouton custom) :
1990
+
1991
+ > ⚠️ **OBLIGATOIRE** : utilisez `logout()` — jamais `clearAuthToken()` seul.
1992
+
1993
+ ```ts
1994
+ import { logout } from '@ollaid/native-sso';
1995
+
1996
+ // Double revocation complète (SaaS + IAM) + nettoyage localStorage
1997
+ await logout();
1998
+
1999
+ // Rediriger vers la page de connexion
2000
+ navigate('/auth/login');
2001
+ ```
2002
+
2003
+ `logout()` effectue automatiquement :
2004
+ 1. `POST /api/native/logout` → révoque le Sanctum token
2005
+ 2. `POST /api/iam/disconnect` → révoque l'AppAccessToken IAM (avec `sanctum_token` + `app_access_token_ref`)
2006
+ 3. `clearAuthToken()` → nettoie les 6 clés localStorage
2007
+
2008
+ ### Hook `useLogout()` (recommandé pour React)
2009
+
2010
+ Pour une intégration React avec gestion d'état (loading, error) et callbacks :
2011
+
2012
+ ```tsx
2013
+ import { useLogout } from '@ollaid/native-sso';
2014
+ import { useNavigate } from 'react-router-dom';
2015
+
2016
+ const LogoutButton = () => {
2017
+ const navigate = useNavigate();
2018
+ const { logout, loading, error } = useLogout({
2019
+ onSuccess: () => navigate('/auth/login'),
2020
+ onError: (err) => console.error('Logout failed:', err.message),
2021
+ });
2022
+
2023
+ return (
2024
+ <>
2025
+ <button onClick={logout} disabled={loading}>
2026
+ {loading ? 'Déconnexion...' : 'Se déconnecter'}
2027
+ </button>
2028
+ {error && <p className="text-red-500">{error}</p>}
2029
+ </>
2030
+ );
2031
+ };
2032
+ ```
2033
+
2034
+ | Propriété | Type | Description |
2035
+ |-----------|------|-------------|
2036
+ | **Options** | | |
2037
+ | `onSuccess` | `() => void` | Appelé après déconnexion réussie (redirection, toast, etc.) |
2038
+ | `onError` | `(error: Error) => void` | Appelé en cas d'échec |
2039
+ | **Retour** | | |
2040
+ | `logout` | `() => Promise<void>` | Déclenche la double revocation complète |
2041
+ | `loading` | `boolean` | `true` pendant l'appel |
2042
+ | `error` | `string \| null` | Message d'erreur ou `null` |
2043
+
2044
+ ### Détection automatique des sessions révoquées
2045
+
2046
+ Le [health check](#usetokenhealthcheck) (toutes les 2 min) détecte automatiquement si un token a été révoqué côté SaaS (par un admin, par la limite de sessions, etc.). Si le serveur répond `401`, le package **révoque aussi l'IAM** (`POST /api/iam/disconnect` avec `sanctum_token` + `app_access_token_ref`) puis déconnecte proprement l'utilisateur.
2047
+
2048
+ > **Important** : seul un `401` explicite déclenche la déconnexion automatique. Les erreurs réseau ou serveur (5xx) ne déclenchent **pas** de déconnexion pour éviter les déconnexions intempestives.
2049
+
2050
+ ### Prérequis backend
2051
+
2052
+ 1. Votre endpoint `POST /api/native/logout` **doit** notifier l'IAM après avoir supprimé le token Sanctum (voir [BACKEND_INTEGRATION.md](./BACKEND_INTEGRATION.md))
2053
+ 2. L'IAM **doit** exposer `POST /api/iam/disconnect` acceptant `{ sanctum_token, app_access_token_ref }` pour la revocation directe par le package
2054
+
2055
+ ---
1869
2056
 
1870
2057
  Modal post-connexion qui invite l'utilisateur à compléter les informations manquantes de son profil.
1871
2058
 
@@ -1928,42 +2115,55 @@ import { OnboardingModal } from '@ollaid/native-sso';
1928
2115
 
1929
2116
  ## useTokenHealthCheck
1930
2117
 
1931
- Hook qui vérifie périodiquement la validité du token Sanctum via `POST /api/native/check-token`.
2118
+ Hook qui vérifie périodiquement la validité du token Sanctum via `POST /api/native/check-token` du SaaS.
2119
+
2120
+ **C'est le package qui gère tout automatiquement** — le SaaS doit juste exposer l'endpoint auth check (voir [BACKEND_INTEGRATION.md](./BACKEND_INTEGRATION.md)).
1932
2121
 
1933
2122
  ### Timing
1934
2123
 
1935
2124
  | Événement | Délai |
1936
2125
  |-----------|-------|
1937
- | Premier check après login | **2 minutes** |
1938
- | Checks suivants | **toutes les 5 minutes** |
1939
- | Requêtes par heure | ~12 |
2126
+ | Premier check après login | **60 secondes** |
2127
+ | Checks suivants | **toutes les 2 minutes** |
2128
+ | Requêtes par heure | ~30 |
1940
2129
 
1941
2130
  ### Comportement réseau
1942
2131
 
1943
2132
  | Situation | Action |
1944
2133
  |-----------|--------|
1945
- | ✅ 200 OK | Met à jour `user_infos` en localStorage |
1946
- | ❌ 401 Unauthorized | Déconnecte l'utilisateur (token expiré/révoqué) |
2134
+ | ✅ 200 `status: 'connected'` | Met à jour `user_infos` en localStorage |
2135
+ | ❌ 401 `status: 'disconnected'` | Révoque l'IAM + déconnecte le frontend |
1947
2136
  | ⚠️ Erreur réseau / timeout / 500 | **Aucune action** — la session est conservée |
1948
2137
  | 📴 Appareil hors ligne | Le check échoue silencieusement, session conservée |
1949
2138
 
2139
+ ### Revocation automatique sur 401
2140
+
2141
+ Quand le SaaS retourne un 401 (token révoqué par un admin, session expirée, etc.) :
2142
+
2143
+ 1. **Appel IAM** — Le package appelle `POST /api/iam/disconnect` avec `sanctum_token` + `app_access_token_ref` (fire-and-forget, 5s timeout) pour révoquer l'`AppAccessToken` correspondant
2144
+ 2. **Nettoyage localStorage** — Les 6 clés de session sont supprimées
2145
+ 3. **Réinitialisation de l'état** — L'interface revient à l'écran de connexion
2146
+
1950
2147
  > **Philosophie** : Ne déconnecter que sur un rejet explicite du serveur (401). Jamais sur un problème réseau.
1951
2148
 
1952
- ### Callbacks
2149
+ ### Format de réponse attendu du SaaS
1953
2150
 
1954
- ```ts
1955
- useTokenHealthCheck({
1956
- enabled: true, // Activer/désactiver
1957
- onUserUpdate: (user) => {
1958
- // Appelé quand user_infos sont rafraîchies
1959
- },
1960
- onSessionExpired: () => {
1961
- // Appelé sur 401 — rediriger vers login
1962
- navigate('/auth/sso');
1963
- },
1964
- });
2151
+ ```json
2152
+ // Connecté (200)
2153
+ {
2154
+ "status": "connected",
2155
+ "user": { "name": "...", "email": "...", "avatar": "..." }
2156
+ }
2157
+
2158
+ // Déconnecté (401)
2159
+ {
2160
+ "status": "disconnected",
2161
+ "message": "Utilisateur non connecté"
2162
+ }
1965
2163
  ```
1966
2164
 
2165
+ > Voir [BACKEND_INTEGRATION.md — Section 2.3](./BACKEND_INTEGRATION.md) pour l'implémentation PHP complète.
2166
+
1967
2167
  ---
1968
2168
 
1969
2169
  ## Sécurité
@@ -2035,6 +2235,7 @@ if (config('services.iam.debug')) {
2035
2235
  - `useMobilePassword` — Hook récupération mot de passe
2036
2236
  - `useMobileRegistration` — Hook inscription
2037
2237
  - `useTokenHealthCheck` — Hook vérification périodique du token
2238
+ - `useLogout` — Hook déconnexion sécurisée avec callbacks
2038
2239
 
2039
2240
  ### Provider
2040
2241
  - `NativeSSOProvider` — Provider React pour configuration centralisée
@@ -2045,6 +2246,7 @@ if (config('services.iam.debug')) {
2045
2246
  - `mobilePasswordService` — Service mot de passe
2046
2247
  - `setNativeAuthConfig` — Configuration manuelle des URLs
2047
2248
  - `iamAccountService` — Service APIs IAM Account (link-phone, link-email, refresh-user-info, update-avatar, reset-avatar)
2249
+ - `logout` — Déconnexion complète (double révocation SaaS + IAM + nettoyage localStorage)
2048
2250
  - `getAuthToken` — Récupérer le token depuis localStorage
2049
2251
  - `getAuthUser` — Récupérer l'utilisateur depuis localStorage
2050
2252
  - `getAccountType` — Récupérer le type de compte depuis localStorage
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Hook React pour la déconnexion sécurisée.
3
+ *
4
+ * Wrape `logout()` avec gestion d'état (loading, error)
5
+ * et callbacks (onSuccess, onError) pour une intégration UX simple.
6
+ *
7
+ * @example
8
+ * ```tsx
9
+ * import { useLogout } from '@ollaid/native-sso';
10
+ *
11
+ * const LogoutButton = () => {
12
+ * const { logout, loading, error } = useLogout({
13
+ * onSuccess: () => navigate('/auth/login'),
14
+ * onError: (err) => toast.error(err.message),
15
+ * });
16
+ *
17
+ * return (
18
+ * <button onClick={logout} disabled={loading}>
19
+ * {loading ? 'Déconnexion...' : 'Se déconnecter'}
20
+ * </button>
21
+ * );
22
+ * };
23
+ * ```
24
+ *
25
+ * @version 1.0.0
26
+ */
27
+ export interface UseLogoutOptions {
28
+ /** Callback appelé après une déconnexion réussie (redirection, toast, etc.) */
29
+ onSuccess?: () => void;
30
+ /** Callback appelé en cas d'erreur (notification, log, etc.) */
31
+ onError?: (error: Error) => void;
32
+ }
33
+ export interface UseLogoutReturn {
34
+ /** Déclenche la déconnexion complète (double revocation SaaS + IAM) */
35
+ logout: () => Promise<void>;
36
+ /** `true` pendant l'appel de déconnexion */
37
+ loading: boolean;
38
+ /** Message d'erreur si la déconnexion a échoué, `null` sinon */
39
+ error: string | null;
40
+ }
41
+ export declare const useLogout: (options?: UseLogoutOptions) => UseLogoutReturn;
@@ -1,12 +1,16 @@
1
1
  /**
2
- * Hook de vérification périodique du token Sanctum
2
+ * Hook de vérification périodique du token Sanctum (Auth Check)
3
3
  *
4
- * - Premier check 2 min après login
5
- * - Checks suivants toutes les 5 min
4
+ * Le package consulte l'endpoint POST /api/native/check-token du SaaS
5
+ * pour vérifier si l'utilisateur est toujours connecté.
6
+ *
7
+ * - Premier check 60s après login
8
+ * - Checks suivants toutes les 2 min
9
+ * - Si status === 'connected' → met à jour user_infos en localStorage
10
+ * - Si 401 → révoque l'IAM (POST /iam/disconnect) + nettoie le frontend
6
11
  * - Ne déconnecte PAS si offline ou serveur inaccessible
7
- * - Déconnecte UNIQUEMENT si le backend retourne 401 (token invalide)
8
12
  *
9
- * @version 1.0.0
13
+ * @version 2.0.0
10
14
  */
11
15
  import type { UserInfos } from '../types/native';
12
16
  export interface UseTokenHealthCheckOptions {
@@ -14,6 +18,8 @@ export interface UseTokenHealthCheckOptions {
14
18
  enabled: boolean;
15
19
  /** SaaS API base URL */
16
20
  saasApiUrl: string;
21
+ /** IAM API base URL (for revocation on 401) */
22
+ iamApiUrl: string;
17
23
  /** Called when the backend explicitly invalidates the token (401) */
18
24
  onTokenInvalid: () => void;
19
25
  /** Called when fresh user_infos are received */