@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 +427 -66
- package/dist/components/LoginModal.d.ts +3 -1
- package/dist/components/PhoneInput.d.ts +1 -1
- package/dist/components/SignupModal.d.ts +3 -1
- package/dist/components/ui.d.ts +10 -0
- package/dist/hooks/useLogout.d.ts +41 -0
- package/dist/hooks/useMobileRegistration.d.ts +1 -1
- package/dist/hooks/useTokenHealthCheck.d.ts +11 -5
- package/dist/index.cjs +711 -407
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +711 -407
- package/dist/index.js.map +1 -1
- package/dist/services/api.d.ts +26 -0
- package/dist/services/nativeAuth.d.ts +1 -1
- package/dist/types/native.d.ts +5 -3
- package/dist/utils/countries.d.ts +1 -0
- package/package.json +1 -1
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. [
|
|
15
|
-
6. [
|
|
16
|
-
7. [
|
|
17
|
-
8. [
|
|
18
|
-
9. [
|
|
19
|
-
10. [
|
|
20
|
-
11. [
|
|
21
|
-
12. [
|
|
22
|
-
13. [
|
|
23
|
-
14. [
|
|
24
|
-
15. [
|
|
25
|
-
16. [
|
|
26
|
-
17. [
|
|
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
|
|
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 {
|
|
166
|
+
import { logout } from '@ollaid/native-sso';
|
|
161
167
|
|
|
162
168
|
const handleLogout = async () => {
|
|
163
|
-
await
|
|
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
|
-
###
|
|
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
|
-
|
|
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'
|
|
603
|
-
'token'
|
|
604
|
-
'expires_at'
|
|
605
|
-
'
|
|
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
|
|
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
|
-
"
|
|
633
|
-
"
|
|
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
|
-
'
|
|
657
|
-
'
|
|
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'
|
|
861
|
-
'token'
|
|
862
|
-
'expires_at'
|
|
863
|
-
'
|
|
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
|
-
'
|
|
919
|
-
'
|
|
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
|
|
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
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
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 **
|
|
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
|
|
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
|
-
##
|
|
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 | **
|
|
1770
|
-
| Checks suivants | **toutes les
|
|
1771
|
-
| Requêtes par heure | ~
|
|
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
|
|
1778
|
-
| ❌ 401
|
|
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
|
-
###
|
|
2136
|
+
### Format de réponse attendu du SaaS
|
|
1785
2137
|
|
|
1786
|
-
```
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
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
|