@ollaid/native-sso 1.0.7 → 2.1.2

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
@@ -11,19 +11,23 @@ Package NPM Frontend-First pour l'authentification Native SSO Ollaid.
11
11
  2. [Intégration rapide (3 étapes)](#intégration-rapide-3-étapes)
12
12
  3. [Props de NativeSSOPage](#props-de-nativessopage)
13
13
  4. [Usage avancé (composants individuels)](#usage-avancé-composants-individuels)
14
- 5. [Backend SaaS Endpoints requis](#backend-saas--endpoints-requis)
15
- 6. [APIs IAM Account (Server-to-Server)](#apis-iam-account-server-to-server)
16
- 7. [Avatar par application](#avatar-par-application)
17
- 8. [Réponses d'erreur](#réponses-derreur)
18
- 9. [Configuration .env Laravel](#configuration-env-laravel)
19
- 10. [Migration Laravel](#migration-laravel)
20
- 11. [Flux d'authentification](#flux-dauthentification)
21
- 12. [Session & localStorage](#session--localstorage)
22
- 13. [OnboardingModal](#onboardingmodal)
23
- 14. [useTokenHealthCheck](#usetokenhealthcheck)
24
- 15. [Sécurité](#sécurité)
25
- 16. [Exports](#exports)
26
- 17. [Publication & Installation npm](#publication--installation-npm)
14
+ 5. [Props des composants individuels](#props-des-composants-individuels)
15
+ 6. [Gestion des conflits d'inscription](#gestion-des-conflits-dinscription)
16
+ 7. [Hook useMobileRegistration](#hook-usemobileregistration)
17
+ 8. [Backend SaaS — Endpoints requis](#backend-saas--endpoints-requis)
18
+ 9. [APIs IAM Account (Server-to-Server)](#apis-iam-account-server-to-server)
19
+ 10. [Avatar par application](#avatar-par-application)
20
+ 11. [Réponses d'erreur](#réponses-derreur)
21
+ 12. [Configuration .env Laravel](#configuration-env-laravel)
22
+ 13. [Migration Laravel](#migration-laravel)
23
+ 14. [Flux d'authentification](#flux-dauthentification)
24
+ 15. [Session & localStorage](#session--localstorage)
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)
27
31
 
28
32
  ---
29
33
 
@@ -152,21 +156,30 @@ déclenche la redirection côté frontend :
152
156
 
153
157
  ## Déconnexion externe (sans le composant)
154
158
 
155
- 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.
156
162
 
157
163
  ### Utilisation
158
164
 
159
165
  ```tsx
160
- import { clearAuthToken } from '@ollaid/native-sso';
166
+ import { logout } from '@ollaid/native-sso';
161
167
 
162
168
  const handleLogout = async () => {
163
- await revokeBackendToken(); // votre logique SaaS (révocation Sanctum, etc.)
164
- clearAuthToken(); // nettoie les 5 clés localStorage du package
169
+ await logout(); // Double revocation (SaaS + IAM) + nettoyage localStorage
165
170
  navigate('/auth/login');
166
171
  };
167
172
  ```
168
173
 
169
- ### 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
170
183
 
171
184
  | Clé | Description |
172
185
  |-----|-------------|
@@ -175,8 +188,23 @@ const handleLogout = async () => {
175
188
  | `user` | Objet utilisateur (avec `iam_reference`, `alias_reference`) |
176
189
  | `account_type` | Type de compte (`user` ou `client`) |
177
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) |
178
192
 
179
- > **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.
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
+ ```
206
+
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.
180
208
 
181
209
  ---
182
210
 
@@ -414,6 +442,7 @@ function MyCustomAuth() {
414
442
  onLoginSuccess={(token, user) => console.log('OK', user)}
415
443
  saasApiUrl="https://mon-saas.com/api"
416
444
  iamApiUrl="https://identityam.ollaid.com/api"
445
+ defaultAccountType="user"
417
446
  />
418
447
  </>
419
448
  );
@@ -422,6 +451,170 @@ function MyCustomAuth() {
422
451
 
423
452
  ---
424
453
 
454
+ ## Props des composants individuels
455
+
456
+ ### `SignupModal`
457
+
458
+ | Prop | Type | Requis | Description |
459
+ |------|------|--------|-------------|
460
+ | `open` | `boolean` | ✅ | Contrôle l'ouverture du modal |
461
+ | `onOpenChange` | `(open: boolean) => void` | ✅ | Callback de changement d'état |
462
+ | `onSwitchToLogin` | `() => void` | ✅ | Callback pour basculer vers le login |
463
+ | `onSignupSuccess` | `(token: string, user: UserInfos) => void` | ✅ | Callback après inscription réussie |
464
+ | `saasApiUrl` | `string` | ✅ | URL du backend SaaS |
465
+ | `iamApiUrl` | `string` | ✅ | URL du backend IAM |
466
+ | `defaultAccountType` | `'user' \| 'client'` | ❌ | Hérité de `NativeSSOPage.accountType`. Type de compte à persister dans localStorage. Si non défini, la valeur par défaut est `'user'`. |
467
+ | `onSwitchToLoginWithPhone` | `(phone: string) => void` | ❌ | Callback lors d'un conflit phone +221 — permet de basculer vers `LoginModal` avec le numéro pré-rempli |
468
+
469
+ ### `LoginModal`
470
+
471
+ | Prop | Type | Requis | Description |
472
+ |------|------|--------|-------------|
473
+ | `open` | `boolean` | ✅ | Contrôle l'ouverture du modal |
474
+ | `onOpenChange` | `(open: boolean) => void` | ✅ | Callback de changement d'état |
475
+ | `onSwitchToSignup` | `() => void` | ✅ | Callback pour basculer vers l'inscription |
476
+ | `onLoginSuccess` | `(token: string, user: UserInfos) => void` | ❌ | Callback après connexion réussie |
477
+ | `saasApiUrl` | `string` | ✅ | URL du backend SaaS |
478
+ | `iamApiUrl` | `string` | ✅ | URL du backend IAM |
479
+ | `loading` | `boolean` | ❌ | État de chargement externe |
480
+ | `showSwitchToSignup` | `boolean` | ❌ | Afficher le lien "Pas de compte ? S'inscrire" (défaut: `true`) |
481
+ | `defaultAccountType` | `'user' \| 'client'` | ❌ | Hérité de `NativeSSOPage.accountType`. Type de compte à persister dans localStorage. Si non défini, la valeur par défaut est `'user'`. |
482
+ | `initialPhone` | `string` | ❌ | Pré-remplit le numéro de téléphone et va directement à l'étape `phone-input`. Utilisé par le hand-off depuis `SignupModal` lors d'un conflit. |
483
+
484
+ ---
485
+
486
+ ## Gestion des conflits d'inscription
487
+
488
+ Lorsqu'un utilisateur tente de s'inscrire avec un email ou un téléphone déjà associé à un compte existant, le backend retourne un **409 Conflict**. Le `SignupModal` gère automatiquement ce cas avec une vue dédiée (`ConflictView`).
489
+
490
+ ### Comportement automatique
491
+
492
+ 1. L'utilisateur remplit le formulaire d'inscription et soumet
493
+ 2. Le backend détecte un conflit (`email_exists` ou `phone_exists`) et retourne un objet `conflict`
494
+ 3. Le `SignupModal` affiche la `ConflictView` avec les options disponibles
495
+
496
+ ### Affichage intelligent des identifiants
497
+
498
+ - **L'identifiant saisi par l'utilisateur** (email ou téléphone) est affiché **en clair** pour confirmer sa saisie
499
+ - **L'identifiant lié au compte existant** (ex: l'email masqué associé à un numéro déjà enregistré) reste **masqué** pour la confidentialité (ex: `j***@gmail.com`)
500
+
501
+ ### Options proposées
502
+
503
+ Selon le type de conflit et les capacités du compte existant, la vue propose :
504
+
505
+ | Option | Condition | Action |
506
+ |--------|-----------|--------|
507
+ | Se connecter | `conflict.options.can_login === true` | Bascule vers `LoginModal` via `onSwitchToLogin` |
508
+ | Se connecter par téléphone | Conflit phone + numéro +221 | Bascule vers `LoginModal` avec le numéro pré-rempli via `onSwitchToLoginWithPhone(phone)` |
509
+ | Récupérer par email | `conflict.options.can_recover_by_email === true` | Ouvre le `PasswordRecoveryModal` |
510
+ | Récupérer par SMS | `conflict.options.can_recover_by_sms === true` | Ouvre le `PasswordRecoveryModal` |
511
+ | Modifier les informations | Toujours disponible | Retour au formulaire pour changer l'identifiant |
512
+
513
+ ### Exemple d'intégration avec hand-off téléphone
514
+
515
+ ```tsx
516
+ function AuthPage() {
517
+ const [showSignup, setShowSignup] = useState(false);
518
+ const [showLogin, setShowLogin] = useState(false);
519
+ const [loginPhone, setLoginPhone] = useState<string | undefined>();
520
+
521
+ return (
522
+ <>
523
+ <SignupModal
524
+ open={showSignup}
525
+ onOpenChange={setShowSignup}
526
+ onSwitchToLogin={() => { setShowSignup(false); setShowLogin(true); }}
527
+ onSwitchToLoginWithPhone={(phone) => {
528
+ setLoginPhone(phone);
529
+ setShowSignup(false);
530
+ setShowLogin(true);
531
+ }}
532
+ onSignupSuccess={(token, user) => navigate('/dashboard')}
533
+ saasApiUrl="https://mon-saas.com/api"
534
+ iamApiUrl="https://identityam.ollaid.com/api"
535
+ />
536
+
537
+ <LoginModal
538
+ open={showLogin}
539
+ onOpenChange={setShowLogin}
540
+ onSwitchToSignup={() => { setShowLogin(false); setShowSignup(true); }}
541
+ initialPhone={loginPhone}
542
+ saasApiUrl="https://mon-saas.com/api"
543
+ iamApiUrl="https://identityam.ollaid.com/api"
544
+ />
545
+ </>
546
+ );
547
+ }
548
+ ```
549
+
550
+ ### Type `RegistrationConflict`
551
+
552
+ L'objet retourné par le backend lors d'un conflit :
553
+
554
+ ```typescript
555
+ interface RegistrationConflict {
556
+ type: 'email' | 'phone';
557
+ masked_identifier: string;
558
+ options: {
559
+ can_login: boolean;
560
+ can_recover_by_email: boolean;
561
+ can_recover_by_sms: boolean;
562
+ masked_email?: string;
563
+ masked_phone?: string;
564
+ };
565
+ }
566
+ ```
567
+
568
+ ---
569
+
570
+ ## Hook `useMobileRegistration`
571
+
572
+ Hook React pour gérer le flow d'inscription mobile en 3 étapes : `init` → `verify-otp` → `complete`.
573
+
574
+ ### Import
575
+
576
+ ```tsx
577
+ import { useMobileRegistration } from '@ollaid/native-sso';
578
+ ```
579
+
580
+ ### Propriétés retournées
581
+
582
+ #### État
583
+
584
+ | Propriété | Type | Description |
585
+ |-----------|------|-------------|
586
+ | `processToken` | `string \| null` | Token de processus d'inscription en cours |
587
+ | `status` | `'idle' \| 'pending_otp' \| 'pending_password' \| 'pending_registration' \| 'completed'` | Étape actuelle du flow |
588
+ | `formData` | `Partial<MobileRegistrationFormData>` | Données du formulaire stockées |
589
+ | `loading` | `boolean` | Indique si une opération est en cours |
590
+ | `error` | `string \| null` | Message d'erreur (français, user-friendly) |
591
+ | `conflict` | `RegistrationConflict \| null` | Objet de conflit si l'identifiant existe déjà |
592
+ | `isCompleted` | `boolean` | `true` si l'inscription est terminée |
593
+ | `hasConflict` | `boolean` | `true` si un conflit est en cours |
594
+
595
+ #### Type de compte (phone-only)
596
+
597
+ | Propriété | Type | Description |
598
+ |-----------|------|-------------|
599
+ | `accountType` | `'email' \| 'phone-only'` | Type de compte sélectionné |
600
+ | `setAccountType` | `(type: AccountType) => void` | Change le type de compte |
601
+ | `isPhoneOnly` | `boolean` | `true` si `accountType === 'phone-only'` |
602
+
603
+ #### Méthodes
604
+
605
+ | Méthode | Signature | Description |
606
+ |---------|-----------|-------------|
607
+ | `updateFormData` | `(data: Partial<MobileRegistrationFormData>) => void` | Met à jour les données du formulaire |
608
+ | `initRegistration` | `(data) => Promise<{ success, otp_code_dev?, otp_method?, otp_sent_to? }>` | Initialise l'inscription (envoi OTP). Peut retourner un conflit. |
609
+ | `verifyOtp` | `(otpCode: string) => Promise<{ success, completed?, callback_token? }>` | Vérifie le code OTP |
610
+ | `completeRegistration` | `(password: string) => Promise<{ success, callback_token? }>` | Finalise avec mot de passe (type `email`) |
611
+ | `completePhoneOnlyRegistration` | `() => Promise<{ success, callback_token? }>` | Finalise sans mot de passe (type `phone-only`, Sénégal 🇸🇳) |
612
+ | `resendOtp` | `() => Promise<{ success, cooldown?, otp_code_dev? }>` | Renvoie le code OTP |
613
+ | `reset` | `() => void` | Réinitialise tout le hook |
614
+ | `clearError` | `() => void` | Efface l'erreur et le conflit |
615
+
616
+ ---
617
+
425
618
  ## Backend SaaS — Endpoints requis
426
619
 
427
620
  Le backend SaaS (Laravel) doit exposer **4 endpoints**. Voici les spécifications exactes :
@@ -539,6 +732,7 @@ $secretKey = $payload['secret_key'];
539
732
  "success": true,
540
733
  "token": "1|abc123def456ghi789...",
541
734
  "expires_at": "2026-04-23T03:12:32.000000Z",
735
+ "app_access_token_ref": "aat_ref_XXXXXXXX",
542
736
  "user": {
543
737
  "id": 1,
544
738
  "reference": "USR-XXXXXXXX",
@@ -598,11 +792,15 @@ Route::post('/native/exchange', function (Request $request) {
598
792
  // Générer un token Sanctum
599
793
  $token = $user->createToken('native-sso')->plainTextToken;
600
794
 
795
+ // Récupérer app_access_token_ref depuis la réponse IAM
796
+ $appAccessTokenRef = $response->json('user_infos.app_access_token_ref');
797
+
601
798
  return response()->json([
602
- 'success' => true,
603
- 'token' => $token,
604
- 'expires_at' => now()->addDays(30)->toISOString(),
605
- 'user' => [
799
+ 'success' => true,
800
+ 'token' => $token,
801
+ 'expires_at' => now()->addDays(30)->toISOString(),
802
+ 'app_access_token_ref' => $appAccessTokenRef,
803
+ 'user' => [
606
804
  'id' => $user->id,
607
805
  'reference' => $user->reference,
608
806
  'alias_reference' => $user->alias_reference,
@@ -622,16 +820,15 @@ Route::post('/native/exchange', function (Request $request) {
622
820
 
623
821
  ### `POST /api/native/check-token`
624
822
 
625
- 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).
823
+ 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).
626
824
 
627
825
  **Headers requis :** `Authorization: Bearer {token}`
628
826
 
629
827
  **Réponse succès (200) :**
630
828
  ```json
631
829
  {
632
- "success": true,
633
- "valid": true,
634
- "user_infos": {
830
+ "status": "connected",
831
+ "user": {
635
832
  "name": "John Doe",
636
833
  "email": "john@example.com",
637
834
  "ccphone": "+221",
@@ -653,9 +850,8 @@ Route::post('/native/check-token', function (Request $request) {
653
850
  $user = $request->user();
654
851
 
655
852
  return response()->json([
656
- 'success' => true,
657
- 'valid' => true,
658
- 'user_infos' => [
853
+ 'status' => 'connected',
854
+ 'user' => [
659
855
  'name' => $user->name,
660
856
  'email' => $user->email,
661
857
  'ccphone' => $user->ccphone,
@@ -856,11 +1052,15 @@ class NativeAuthController extends Controller
856
1052
  $expiresAt = now()->addDays(30);
857
1053
  $token = $user->createToken('native-sso', ['*'], $expiresAt);
858
1054
 
1055
+ // Récupérer app_access_token_ref depuis la réponse IAM
1056
+ $appAccessTokenRef = $userInfos['app_access_token_ref'] ?? null;
1057
+
859
1058
  return response()->json([
860
- 'success' => true,
861
- 'token' => $token->plainTextToken,
862
- 'expires_at' => $expiresAt->toIso8601String(),
863
- 'user' => [
1059
+ 'success' => true,
1060
+ 'token' => $token->plainTextToken,
1061
+ 'expires_at' => $expiresAt->toIso8601String(),
1062
+ 'app_access_token_ref' => $appAccessTokenRef,
1063
+ 'user' => [
864
1064
  'id' => $user->id,
865
1065
  'reference' => $iamReference,
866
1066
  'alias_reference' => $aliasReference,
@@ -915,9 +1115,8 @@ class NativeAuthController extends Controller
915
1115
  $user = $request->user();
916
1116
 
917
1117
  return response()->json([
918
- 'success' => true,
919
- 'valid' => true,
920
- 'user_infos' => [
1118
+ 'status' => 'connected',
1119
+ 'user' => [
921
1120
  'name' => $user->name,
922
1121
  'email' => $user->email,
923
1122
  'ccphone' => $user->ccphone,
@@ -931,21 +1130,43 @@ class NativeAuthController extends Controller
931
1130
  }
932
1131
 
933
1132
  // ════════════════════════════════════════
934
- // POST /api/native/logout
1133
+ // POST /api/native/logout (déconnexion synchronisée)
935
1134
  // ════════════════════════════════════════
936
1135
 
937
1136
  /**
938
- * Révoque UNIQUEMENT le token Sanctum courant (single-session).
1137
+ * Révoque le token Sanctum courant (single-session)
1138
+ * ET notifie l'IAM pour révoquer l'AppAccessToken lié.
939
1139
  * Route protégée par auth:sanctum.
940
1140
  */
941
1141
  public function logout(Request $request): JsonResponse
942
1142
  {
943
- $request->user()->currentAccessToken()->delete();
944
-
945
- return response()->json([
946
- 'success' => true,
947
- 'message' => 'Déconnexion réussie',
948
- ]);
1143
+ try {
1144
+ $user = $request->user();
1145
+
1146
+ if ($user) {
1147
+ $sanctumTokenPlain = $request->bearerToken();
1148
+ $user->currentAccessToken()?->delete();
1149
+
1150
+ // Notifier l'IAM (fire-and-forget, timeout 5s)
1151
+ if ($sanctumTokenPlain) {
1152
+ $iamPrefix = $request->attributes->get('iam_prefix', 'iam');
1153
+ $iamApiUrl = config("services.{$iamPrefix}.api_url", 'https://identityam.ollaid.com/api');
1154
+ try {
1155
+ $appAccessTokenRef = $user->currentAccessToken()->app_access_token_ref ?? null;
1156
+ Http::timeout(5)->post("{$iamApiUrl}/iam/disconnect", array_filter([
1157
+ 'sanctum_token' => $sanctumTokenPlain,
1158
+ 'app_access_token_ref' => $appAccessTokenRef,
1159
+ ]));
1160
+ } catch (\Exception $e) {
1161
+ Log::warning("[NativeSSO] IAM disconnect failed (non-blocking)", ['error' => $e->getMessage()]);
1162
+ }
1163
+ }
1164
+ }
1165
+
1166
+ return response()->json(['success' => true, 'message' => 'Déconnexion réussie']);
1167
+ } catch (\Exception $e) {
1168
+ return response()->json(['success' => true, 'message' => 'Déconnexion effectuée']);
1169
+ }
949
1170
  }
950
1171
  }
951
1172
  ```
@@ -1662,7 +1883,7 @@ Toutes les APIs IAM retournent le même objet `user_infos` avec exactement **9 c
1662
1883
 
1663
1884
  ## Session & localStorage
1664
1885
 
1665
- Le package utilise **5 clés** dans `localStorage` pour persister la session :
1886
+ Le package utilise **6 clés** dans `localStorage` pour persister la session :
1666
1887
 
1667
1888
  | Clé | Contenu | Source | Valeurs possibles |
1668
1889
  |-----|---------|--------|-------------------|
@@ -1671,6 +1892,7 @@ Le package utilise **5 clés** dans `localStorage` pour persister la session :
1671
1892
  | `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":"...",...}` |
1672
1893
  | `account_type` | Type de compte | Déterminé lors du `exchange` selon le mode d'inscription | `"user"` (défaut) ou `"client"` (inscription phone-only) |
1673
1894
  | `alias_reference` | Référence alias utilisée lors de la connexion | Réponse de `/api/native/exchange` (champ `user.alias_reference`) | `"ALI-XXXXXXXX"` |
1895
+ | `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"` |
1674
1896
 
1675
1897
  > **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`.
1676
1898
 
@@ -1682,7 +1904,7 @@ L'objet `user` stocké en localStorage contient deux champs ajoutés automatique
1682
1904
 
1683
1905
  ### Nettoyage
1684
1906
 
1685
- Lors du `logout()`, les 5 clés sont supprimées via `clearAuthToken()`.
1907
+ Lors du `logout()`, les 6 clés sont supprimées via `clearAuthToken()`.
1686
1908
 
1687
1909
  ### Accès programmatique
1688
1910
 
@@ -1697,7 +1919,127 @@ const aliasRef = localStorage.getItem('alias_reference'); // string | null
1697
1919
 
1698
1920
  ---
1699
1921
 
1700
- ## OnboardingModal
1922
+ ## Déconnexion synchronisée
1923
+
1924
+ 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.
1925
+
1926
+ ### Architecture — Double Revocation
1927
+
1928
+ ```
1929
+ ┌─────────────────┐
1930
+ │ Package │──────────────────────────────────┐
1931
+ │ logout() │ │
1932
+ └──────┬──────────┘ │
1933
+ │ ① POST /native/logout │ ② POST /iam/disconnect
1934
+ │ (Sanctum token) │ (sanctum_token + app_access_token_ref)
1935
+ ▼ ▼
1936
+ ┌─────────────────┐ ┌─────────────────┐
1937
+ │ SaaS API │──③ fire-and-forget──────▶│ IAM API │
1938
+ │ │ POST /iam/disconnect │ │
1939
+ └─────────────────┘ └─────────────────┘
1940
+ ```
1941
+
1942
+ **Trois niveaux de sécurité :**
1943
+
1944
+ | Scénario | Résultat |
1945
+ |----------|----------|
1946
+ | ✅ Tout fonctionne | SaaS + IAM révoquent tous les deux |
1947
+ | ⚠️ SaaS injoignable | L'IAM révoque quand même via l'appel direct ② |
1948
+ | ⚠️ IAM injoignable | Le SaaS révoque via ① puis retente via ③ |
1949
+ | ❌ Les deux injoignables | localStorage nettoyé, le health check détectera le 401 plus tard |
1950
+
1951
+ ### Comportement automatique
1952
+
1953
+ Quand `useNativeAuth.logout()` est appelé (ou que l'utilisateur clique "Se déconnecter" dans `NativeSSOPage`) :
1954
+
1955
+ 1. **Appels parallèles** (via `Promise.allSettled`, jamais bloquants) :
1956
+ - `POST /api/native/logout` → SaaS supprime le Sanctum token + notifie l'IAM (fire-and-forget)
1957
+ - `POST /api/iam/disconnect` → IAM révoque directement l'`AppAccessToken` via `sanctum_token` + `app_access_token_ref` (lookup optimisé)
1958
+ 2. **Nettoyage local garanti** : les 6 clés localStorage sont supprimées (`token`, `auth_token`, `user`, `account_type`, `alias_reference`, `app_access_token_ref`)
1959
+ 3. **Aucun blocage** : même si les deux appels échouent (offline, timeout), la déconnexion locale est instantanée
1960
+
1961
+ > **Fiabilité** : `Promise.allSettled` + `.catch()` sur chaque appel. L'appel IAM a un timeout court (5s) pour ne jamais ralentir l'UX.
1962
+
1963
+ ### API IAM de revocation — `POST /api/iam/disconnect`
1964
+
1965
+ Le package contacte directement l'IAM pour révoquer l'`AppAccessToken` lié à la session.
1966
+
1967
+ | Paramètre | Type | Description |
1968
+ |-----------|------|-------------|
1969
+ | `sanctum_token` | `string` | Le token Sanctum stocké localement, utilisé pour identifier l'`AppAccessToken` à révoquer |
1970
+ | `app_access_token_ref` | `string` | (recommandé) Référence directe de l'`AppAccessToken` IAM — lookup instantané par PK |
1971
+
1972
+ 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.
1973
+
1974
+ ### Déconnexion externe (hors package)
1975
+
1976
+ Si votre application gère une déconnexion **en dehors** de `NativeSSOPage` (ex : backoffice, bouton custom) :
1977
+
1978
+ > ⚠️ **OBLIGATOIRE** : utilisez `logout()` — jamais `clearAuthToken()` seul.
1979
+
1980
+ ```ts
1981
+ import { logout } from '@ollaid/native-sso';
1982
+
1983
+ // Double revocation complète (SaaS + IAM) + nettoyage localStorage
1984
+ await logout();
1985
+
1986
+ // Rediriger vers la page de connexion
1987
+ navigate('/auth/login');
1988
+ ```
1989
+
1990
+ `logout()` effectue automatiquement :
1991
+ 1. `POST /api/native/logout` → révoque le Sanctum token
1992
+ 2. `POST /api/iam/disconnect` → révoque l'AppAccessToken IAM (avec `sanctum_token` + `app_access_token_ref`)
1993
+ 3. `clearAuthToken()` → nettoie les 6 clés localStorage
1994
+
1995
+ ### Hook `useLogout()` (recommandé pour React)
1996
+
1997
+ Pour une intégration React avec gestion d'état (loading, error) et callbacks :
1998
+
1999
+ ```tsx
2000
+ import { useLogout } from '@ollaid/native-sso';
2001
+ import { useNavigate } from 'react-router-dom';
2002
+
2003
+ const LogoutButton = () => {
2004
+ const navigate = useNavigate();
2005
+ const { logout, loading, error } = useLogout({
2006
+ onSuccess: () => navigate('/auth/login'),
2007
+ onError: (err) => console.error('Logout failed:', err.message),
2008
+ });
2009
+
2010
+ return (
2011
+ <>
2012
+ <button onClick={logout} disabled={loading}>
2013
+ {loading ? 'Déconnexion...' : 'Se déconnecter'}
2014
+ </button>
2015
+ {error && <p className="text-red-500">{error}</p>}
2016
+ </>
2017
+ );
2018
+ };
2019
+ ```
2020
+
2021
+ | Propriété | Type | Description |
2022
+ |-----------|------|-------------|
2023
+ | **Options** | | |
2024
+ | `onSuccess` | `() => void` | Appelé après déconnexion réussie (redirection, toast, etc.) |
2025
+ | `onError` | `(error: Error) => void` | Appelé en cas d'échec |
2026
+ | **Retour** | | |
2027
+ | `logout` | `() => Promise<void>` | Déclenche la double revocation complète |
2028
+ | `loading` | `boolean` | `true` pendant l'appel |
2029
+ | `error` | `string \| null` | Message d'erreur ou `null` |
2030
+
2031
+ ### Détection automatique des sessions révoquées
2032
+
2033
+ 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.
2034
+
2035
+ > **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.
2036
+
2037
+ ### Prérequis backend
2038
+
2039
+ 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))
2040
+ 2. L'IAM **doit** exposer `POST /api/iam/disconnect` acceptant `{ sanctum_token, app_access_token_ref }` pour la revocation directe par le package
2041
+
2042
+ ---
1701
2043
 
1702
2044
  Modal post-connexion qui invite l'utilisateur à compléter les informations manquantes de son profil.
1703
2045
 
@@ -1760,42 +2102,55 @@ import { OnboardingModal } from '@ollaid/native-sso';
1760
2102
 
1761
2103
  ## useTokenHealthCheck
1762
2104
 
1763
- Hook qui vérifie périodiquement la validité du token Sanctum via `POST /api/native/check-token`.
2105
+ Hook qui vérifie périodiquement la validité du token Sanctum via `POST /api/native/check-token` du SaaS.
2106
+
2107
+ **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)).
1764
2108
 
1765
2109
  ### Timing
1766
2110
 
1767
2111
  | Événement | Délai |
1768
2112
  |-----------|-------|
1769
- | Premier check après login | **2 minutes** |
1770
- | Checks suivants | **toutes les 5 minutes** |
1771
- | Requêtes par heure | ~12 |
2113
+ | Premier check après login | **60 secondes** |
2114
+ | Checks suivants | **toutes les 2 minutes** |
2115
+ | Requêtes par heure | ~30 |
1772
2116
 
1773
2117
  ### Comportement réseau
1774
2118
 
1775
2119
  | Situation | Action |
1776
2120
  |-----------|--------|
1777
- | ✅ 200 OK | Met à jour `user_infos` en localStorage |
1778
- | ❌ 401 Unauthorized | Déconnecte l'utilisateur (token expiré/révoqué) |
2121
+ | ✅ 200 `status: 'connected'` | Met à jour `user_infos` en localStorage |
2122
+ | ❌ 401 `status: 'disconnected'` | Révoque l'IAM + déconnecte le frontend |
1779
2123
  | ⚠️ Erreur réseau / timeout / 500 | **Aucune action** — la session est conservée |
1780
2124
  | 📴 Appareil hors ligne | Le check échoue silencieusement, session conservée |
1781
2125
 
2126
+ ### Revocation automatique sur 401
2127
+
2128
+ Quand le SaaS retourne un 401 (token révoqué par un admin, session expirée, etc.) :
2129
+
2130
+ 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
2131
+ 2. **Nettoyage localStorage** — Les 6 clés de session sont supprimées
2132
+ 3. **Réinitialisation de l'état** — L'interface revient à l'écran de connexion
2133
+
1782
2134
  > **Philosophie** : Ne déconnecter que sur un rejet explicite du serveur (401). Jamais sur un problème réseau.
1783
2135
 
1784
- ### Callbacks
2136
+ ### Format de réponse attendu du SaaS
1785
2137
 
1786
- ```ts
1787
- useTokenHealthCheck({
1788
- enabled: true, // Activer/désactiver
1789
- onUserUpdate: (user) => {
1790
- // Appelé quand user_infos sont rafraîchies
1791
- },
1792
- onSessionExpired: () => {
1793
- // Appelé sur 401 — rediriger vers login
1794
- navigate('/auth/sso');
1795
- },
1796
- });
2138
+ ```json
2139
+ // Connecté (200)
2140
+ {
2141
+ "status": "connected",
2142
+ "user": { "name": "...", "email": "...", "avatar": "..." }
2143
+ }
2144
+
2145
+ // Déconnecté (401)
2146
+ {
2147
+ "status": "disconnected",
2148
+ "message": "Utilisateur non connecté"
2149
+ }
1797
2150
  ```
1798
2151
 
2152
+ > Voir [BACKEND_INTEGRATION.md — Section 2.3](./BACKEND_INTEGRATION.md) pour l'implémentation PHP complète.
2153
+
1799
2154
  ---
1800
2155
 
1801
2156
  ## Sécurité
@@ -1867,6 +2222,7 @@ if (config('services.iam.debug')) {
1867
2222
  - `useMobilePassword` — Hook récupération mot de passe
1868
2223
  - `useMobileRegistration` — Hook inscription
1869
2224
  - `useTokenHealthCheck` — Hook vérification périodique du token
2225
+ - `useLogout` — Hook déconnexion sécurisée avec callbacks
1870
2226
 
1871
2227
  ### Provider
1872
2228
  - `NativeSSOProvider` — Provider React pour configuration centralisée
@@ -1877,12 +2233,17 @@ if (config('services.iam.debug')) {
1877
2233
  - `mobilePasswordService` — Service mot de passe
1878
2234
  - `setNativeAuthConfig` — Configuration manuelle des URLs
1879
2235
  - `iamAccountService` — Service APIs IAM Account (link-phone, link-email, refresh-user-info, update-avatar, reset-avatar)
2236
+ - `logout` — Déconnexion complète (double révocation SaaS + IAM + nettoyage localStorage)
1880
2237
  - `getAuthToken` — Récupérer le token depuis localStorage
1881
2238
  - `getAuthUser` — Récupérer l'utilisateur depuis localStorage
1882
2239
  - `getAccountType` — Récupérer le type de compte depuis localStorage
1883
2240
 
1884
2241
  ### Types
1885
2242
  - `UserInfos`, `NativeAuthState`, `NativeAuthStatus`, `NativeCredentials`, etc.
2243
+ - `AccountType` — `'email' | 'phone-only'`
2244
+ - `MobilePasswordState`, `MobilePasswordStatus` — Types pour la récupération de mot de passe
2245
+ - `MobileRegistrationFormData` — Données du formulaire d'inscription
2246
+ - `RegistrationConflict` — Objet de conflit d'inscription (type interne, voir [Gestion des conflits](#gestion-des-conflits-dinscription))
1886
2247
  - `LinkPhoneRequest`, `LinkPhoneResponse` — Types pour l'API link-phone
1887
2248
  - `LinkEmailRequest`, `LinkEmailResponse` — Types pour l'API link-email
1888
2249
  - `RefreshUserInfoSingleRequest`, `RefreshUserInfoSingleResponse` — Types pour refresh single