@ollaid/native-sso 2.5.0 → 2.7.0

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.
Files changed (40) hide show
  1. package/dist/components/AvatarCropModal.d.ts +1 -1
  2. package/dist/components/DebugPanel.d.ts +1 -1
  3. package/dist/components/LoginModal.d.ts +1 -1
  4. package/dist/components/NativeSSOPage.d.ts +4 -2
  5. package/dist/components/OnboardingModal.d.ts +1 -1
  6. package/dist/components/PasswordRecoveryModal.d.ts +1 -1
  7. package/dist/components/SignupModal.d.ts +1 -1
  8. package/dist/components/ui.d.ts +1 -1
  9. package/dist/hooks/useLogout.d.ts +1 -1
  10. package/dist/hooks/useMobilePassword.d.ts +1 -1
  11. package/dist/hooks/useMobileRegistration.d.ts +2 -2
  12. package/dist/hooks/useNativeAuth.d.ts +8 -5
  13. package/dist/hooks/useTokenHealthCheck.d.ts +1 -1
  14. package/dist/index-Bpixveaz.js +489 -0
  15. package/dist/index-Bpixveaz.js.map +1 -0
  16. package/dist/index-DDOXM37y.cjs +488 -0
  17. package/dist/index-DDOXM37y.cjs.map +1 -0
  18. package/dist/index.cjs +7118 -184
  19. package/dist/index.cjs.map +1 -1
  20. package/dist/index.d.ts +4 -3
  21. package/dist/index.js +7118 -184
  22. package/dist/index.js.map +1 -1
  23. package/dist/provider.d.ts +4 -1
  24. package/dist/services/api.d.ts +52 -10
  25. package/dist/services/debugLogger.d.ts +1 -1
  26. package/dist/services/iamAccount.d.ts +1 -1
  27. package/dist/services/mobilePassword.d.ts +1 -1
  28. package/dist/services/mobileRegistration.d.ts +1 -1
  29. package/dist/services/nativeAuth.d.ts +1 -1
  30. package/dist/services/profile.d.ts +1 -1
  31. package/dist/services/profileChange.d.ts +1 -1
  32. package/dist/services/profileMedia.d.ts +1 -1
  33. package/dist/types/mobile.d.ts +1 -1
  34. package/dist/types/native.d.ts +1 -1
  35. package/dist/web-BQDVoI6q.cjs +146 -0
  36. package/dist/web-BQDVoI6q.cjs.map +1 -0
  37. package/dist/web-DPmAPlXS.js +146 -0
  38. package/dist/web-DPmAPlXS.js.map +1 -0
  39. package/package.json +10 -3
  40. package/README.md +0 -2503
package/README.md DELETED
@@ -1,2503 +0,0 @@
1
- # @ollaid/native-sso
2
-
3
- Package NPM Frontend-First pour l'authentification Native SSO Ollaid.
4
- **Un `npm install` et une route — c'est tout.**
5
-
6
- ---
7
-
8
- ## Table des matières
9
-
10
- 1. [Installation](#installation)
11
- 2. [Intégration rapide (3 étapes)](#intégration-rapide-3-étapes)
12
- 3. [Props de NativeSSOPage](#props-de-nativessopage)
13
- 4. [Usage avancé (composants individuels)](#usage-avancé-composants-individuels)
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)
31
- 22. [Webhooks & Health Check (Backend)](#webhooks--health-check-backend)
32
-
33
- ---
34
-
35
- ## Installation
36
-
37
- ```bash
38
- npm install @ollaid/native-sso
39
- ```
40
-
41
- ---
42
-
43
- ## Intégration rapide (3 étapes)
44
-
45
- ### 1. Installer le package
46
-
47
- ```bash
48
- npm install @ollaid/native-sso
49
- ```
50
-
51
- ### 2. Ajouter la route dans `App.tsx`
52
-
53
- ```tsx
54
- import { NativeSSOPage } from '@ollaid/native-sso';
55
- import { Route, Routes, useNavigate } from 'react-router-dom';
56
-
57
- function App() {
58
- const navigate = useNavigate();
59
-
60
- return (
61
- <Routes>
62
- <Route
63
- path="/auth/native-sso"
64
- element={
65
- <NativeSSOPage
66
- saasApiUrl="https://mon-saas.com/api"
67
- iamApiUrl="https://identityam.ollaid.com/api"
68
- accountType="user"
69
- onLoginSuccess={(token, user) => {
70
- console.log('Connecté !', user.name);
71
- navigate('/dashboard');
72
- }}
73
- onLogout={() => navigate('/auth/native-sso')}
74
- />
75
- }
76
- />
77
- {/* ... autres routes */}
78
- </Routes>
79
- );
80
- }
81
- ```
82
-
83
- ### 3. C'est tout ✅
84
-
85
- La page `/auth/native-sso` gère automatiquement :
86
- - ✅ Connexion par email (mot de passe + OTP)
87
- - ✅ Connexion par téléphone (SMS OTP)
88
- - ✅ Connexion par code d'accès
89
- - ✅ Inscription complète (email ou téléphone uniquement 🇸🇳)
90
- - ✅ Récupération de mot de passe
91
- - ✅ Grant access (inscription auto à une nouvelle app)
92
- - ✅ 2FA (TOTP)
93
- - ✅ Session persistée en localStorage
94
- - ✅ Branding Ollaid SSO
95
-
96
- ---
97
-
98
- ## Props de `NativeSSOPage`
99
-
100
- | Prop | Type | Requis | Description |
101
- |------|------|--------|-------------|
102
- | `saasApiUrl` | `string` | ✅ | URL du backend SaaS (ex: `https://mon-saas.com/api`) |
103
- | `iamApiUrl` | `string` | ✅ | URL du backend IAM (ex: `https://identityam.ollaid.com/api`) |
104
- | `accountType` | `'user' \| 'client'` | ❌ | Type de compte à persister dans localStorage (défaut: `'user'`). Utile si vous avez plusieurs pages SSO avec des rôles différents. |
105
- | `configPrefix` | `string` | ❌ | **Multi-tenant** : préfixe de configuration IAM côté backend (défaut: `'iam'`). Permet à un même backend SaaS de gérer N applications IAM. Voir [Multi-Tenant](#multi-tenant-plusieurs-applications-sur-le-même-backend). |
106
- | `onLoginSuccess` | `(token: string, user: UserInfos) => void` | ❌ | Callback après connexion réussie |
107
- | `onLogout` | `() => void` | ❌ | Callback après déconnexion |
108
- | `title` | `string` | ❌ | Titre personnalisé (défaut: "Un compte pour toutes vos applications") |
109
- | `description` | `string` | ❌ | Description personnalisée |
110
- | `logoUrl` | `string` | ❌ | URL du logo (remplace le slider) |
111
- | `hideFooter` | `boolean` | ❌ | Masquer "Propulsé par iam.ollaid.com" |
112
- | `onOnboardingComplete` | `(data: { name?: string; image_url?: string; ccphone?: string; phone?: string; email?: string }) => void` | ❌ | Callback après complétion de l'onboarding |
113
- | `redirectAfterLogin` | `string` | ❌ | Route vers laquelle rediriger après connexion réussie (ex: `/client/dashboard`). Utilise `window.location.href`. Compatible avec ou sans react-router. |
114
- | `redirectAfterLogout` | `string` | ❌ | Route vers laquelle rediriger après déconnexion (ex: `/auth/client`). Utilise `window.location.href`. |
115
-
116
- > **Note :** Le mode `debug` est contrôlé **uniquement** par le backend via la variable d'environnement `IAM_DEBUG` dans le `.env` du SaaS. Il n'y a plus de prop `debug` à passer au composant. Le `DebugPanel` est **réactif** : il apparaît automatiquement après le chargement des credentials si `debug: true` est retourné par le backend.
117
- > Quand le `DebugPanel` est visible, il expose aussi des boutons de test pour ouvrir `Connexion`, `Inscription`, `Infos profile` et un reset du rappel profil.
118
-
119
- ### Redirections automatiques (optionnel)
120
-
121
- > **Ces props sont entièrement optionnels.** Si vous ne les définissez pas,
122
- > c'est votre backend SaaS qui gère la redirection après l'exchange — il connaît
123
- > déjà le `accountType` et peut rediriger l'utilisateur vers la bonne page après
124
- > avoir sauvegardé le token dans le localStorage.
125
-
126
- Utilisez ces props uniquement si vous souhaitez que le **composant lui-même**
127
- déclenche la redirection côté frontend :
128
-
129
- ```tsx
130
- <NativeSSOPage
131
- saasApiUrl="https://votre-saas.com/api"
132
- iamApiUrl="https://identityam.ollaid.com/api"
133
- configPrefix="iam_client"
134
- accountType="client"
135
- redirectAfterLogin="/client/dashboard"
136
- redirectAfterLogout="/auth/client"
137
- />
138
- ```
139
-
140
- **Sans ces props** (usage minimal, le SaaS gère la redirection) :
141
-
142
- ```tsx
143
- <NativeSSOPage
144
- saasApiUrl="https://votre-saas.com/api"
145
- iamApiUrl="https://identityam.ollaid.com/api"
146
- configPrefix="iam_client"
147
- accountType="client"
148
- onLoginSuccess={(token, user) => { /* votre logique */ }}
149
- />
150
- ```
151
-
152
- **Comportement :**
153
- - Si `redirectAfterLogin` est défini → redirection via `window.location.href` après connexion
154
- - Si `onLoginSuccess` est aussi défini → le callback est appelé **avant** la redirection
155
- - Si **aucun** prop de redirection n'est défini → aucune redirection automatique, comportement inchangé
156
-
157
- ---
158
-
159
- ## Déconnexion externe (sans le composant)
160
-
161
- 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.
162
-
163
- > ⚠️ **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.
164
-
165
- ### Utilisation
166
-
167
- ```tsx
168
- import { logout } from '@ollaid/native-sso';
169
-
170
- const handleLogout = async () => {
171
- await logout(); // ✅ Double revocation (SaaS + IAM) + nettoyage localStorage
172
- navigate('/auth/login');
173
- };
174
- ```
175
-
176
- ### Que fait `logout()` ?
177
-
178
- 1. **Révoque le token SaaS** — `POST /api/native/logout` (supprime le Sanctum token)
179
- 2. **Révoque la session IAM** — `POST /api/iam/disconnect` (avec `sanctum_token` + `app_access_token_ref`)
180
- 3. **Nettoie le localStorage** — supprime les clés de session du package
181
-
182
- Les appels réseau sont en `Promise.allSettled` (best-effort) : même si le serveur est injoignable, le localStorage est **toujours** nettoyé.
183
-
184
- ### Clés localStorage nettoyées
185
-
186
- | Clé | Description |
187
- |-----|-------------|
188
- | `auth_token` | Token Sanctum actif |
189
- | `token` | Token legacy |
190
- | `user` | Objet utilisateur (avec `iam_reference`, `alias_reference`) |
191
- | `account_type` | Type de compte (`user` ou `client`) |
192
- | `alias_reference` | Référence de l'alias de connexion |
193
- | `app_access_token_ref` | Référence de l'`AppAccessToken` IAM (pour revocation optimisée) |
194
- | `refresh_token` | Refresh token SaaS (si activé) |
195
- | `token_expires_at` | Expiration du token Sanctum (si fournie) |
196
- | `refresh_expires_at` | Expiration du refresh token (si fournie) |
197
-
198
- ### ⛔ `clearAuthToken()` est déprécié
199
-
200
- `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.
201
-
202
- ```tsx
203
- // ❌ NE PAS FAIRE — sessions orphelines sur l'IAM
204
- import { clearAuthToken } from '@ollaid/native-sso';
205
- clearAuthToken();
206
-
207
- // ✅ FAIRE — double revocation garantie
208
- import { logout } from '@ollaid/native-sso';
209
- await logout();
210
- ```
211
-
212
- > **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.
213
-
214
- ---
215
-
216
- ## Multi-Tenant (plusieurs applications sur le même backend)
217
-
218
- Le package supporte N applications IAM sur le même backend SaaS via le prop `configPrefix`. C'est dynamique : vous pouvez ajouter autant d'applications que nécessaire sans modifier le code du package.
219
-
220
- ### Principe
221
-
222
- Le frontend envoie un header `X-IAM-Config-Prefix` dans tous les appels au SaaS. Le backend utilise ce préfixe pour résoudre dynamiquement le bon bloc de configuration.
223
-
224
- ```
225
- Frontend (configPrefix="iam_vendor")
226
- → GET /api/native/config [Header: X-IAM-Config-Prefix: iam_vendor]
227
-
228
- Backend SaaS:
229
- $prefix = $request->header('X-IAM-Config-Prefix', 'iam');
230
- $appKey = config("services.{$prefix}.app_key"); // ← résolution dynamique
231
- ```
232
-
233
- ### Côté Frontend
234
-
235
- ```tsx
236
- {/* Page login principale */}
237
- <NativeSSOPage
238
- saasApiUrl="https://votre-saas.com/api"
239
- iamApiUrl="https://identityam.ollaid.com/api"
240
- configPrefix="iam"
241
- accountType="user"
242
- redirectAfterLogin="/dashboard"
243
- redirectAfterLogout="/auth/native-sso"
244
- />
245
-
246
- {/* Page login espace vendeur */}
247
- <NativeSSOPage
248
- saasApiUrl="https://votre-saas.com/api"
249
- iamApiUrl="https://identityam.ollaid.com/api"
250
- configPrefix="iam_vendor"
251
- accountType="client"
252
- redirectAfterLogin="/vendor/dashboard"
253
- redirectAfterLogout="/auth/vendor"
254
- />
255
-
256
- {/* Page login admin — même backend, app IAM différente */}
257
- <NativeSSOPage
258
- saasApiUrl="https://votre-saas.com/api"
259
- iamApiUrl="https://identityam.ollaid.com/api"
260
- configPrefix="iam_admin"
261
- accountType="user"
262
- redirectAfterLogin="/admin/dashboard"
263
- redirectAfterLogout="/auth/admin"
264
- />
265
- ```
266
-
267
- ### Côté Backend SaaS — `.env`
268
-
269
- Le préfixe `.env` correspond au `configPrefix` en UPPER_CASE. Ajoutez autant de blocs que nécessaire :
270
-
271
- ```env
272
- # ===== Serveur IAM (partagé) =====
273
- IAM_API_URL=https://identityam.ollaid.com/api
274
- IAM_AUTH_URL=https://iam.ollaid.com
275
-
276
- # ===== Préfixe "iam" (application principale) =====
277
- IAM_APP_KEY=oiam_ak_xxx
278
- IAM_PUBLIC_KEY=oiam_pk_xxx
279
- IAM_SECRET_KEY=oiam_sk_xxx
280
- IAM_WEBHOOK_SECRET=oiam_whsec_xxx
281
- IAM_DEBUG=true
282
-
283
- # ===== Préfixe "iam_vendor" (espace vendeur/shop) =====
284
- IAM_VENDOR_APP_KEY=oiam_ak_yyy
285
- IAM_VENDOR_PUBLIC_KEY=oiam_pk_yyy
286
- IAM_VENDOR_SECRET_KEY=oiam_sk_yyy
287
- IAM_VENDOR_WEBHOOK_SECRET=oiam_whsec_yyy
288
- IAM_VENDOR_DEBUG=false
289
-
290
- # ===== Préfixe "iam_client" (espace client) =====
291
- IAM_CLIENT_APP_KEY=oiam_ak_zzz
292
- IAM_CLIENT_SECRET_KEY=oiam_sk_zzz
293
- IAM_CLIENT_DEBUG=true
294
-
295
- # ===== Préfixe "iam_admin" (back-office) =====
296
- IAM_ADMIN_APP_KEY=oiam_ak_aaa
297
- IAM_ADMIN_SECRET_KEY=oiam_sk_aaa
298
- IAM_ADMIN_DEBUG=true
299
- ```
300
-
301
- ### Côté Backend SaaS — `config/services.php`
302
-
303
- ```php
304
- return [
305
- 'iam' => [
306
- 'api_url' => env('IAM_API_URL', 'https://identityam.ollaid.com/api'),
307
- 'app_key' => env('IAM_APP_KEY'),
308
- 'public_key' => env('IAM_PUBLIC_KEY'),
309
- 'secret_key' => env('IAM_SECRET_KEY'),
310
- 'debug' => env('IAM_DEBUG', false),
311
- ],
312
- 'iam_vendor' => [
313
- 'api_url' => env('IAM_API_URL'),
314
- 'app_key' => env('IAM_VENDOR_APP_KEY'),
315
- 'public_key' => env('IAM_VENDOR_PUBLIC_KEY'),
316
- 'secret_key' => env('IAM_VENDOR_SECRET_KEY'),
317
- 'debug' => env('IAM_VENDOR_DEBUG', false),
318
- ],
319
- 'iam_client' => [
320
- 'api_url' => env('IAM_API_URL'),
321
- 'app_key' => env('IAM_CLIENT_APP_KEY'),
322
- 'secret_key' => env('IAM_CLIENT_SECRET_KEY'),
323
- 'debug' => env('IAM_CLIENT_DEBUG', false),
324
- ],
325
- 'iam_admin' => [
326
- 'api_url' => env('IAM_API_URL'),
327
- 'app_key' => env('IAM_ADMIN_APP_KEY'),
328
- 'secret_key' => env('IAM_ADMIN_SECRET_KEY'),
329
- 'debug' => env('IAM_ADMIN_DEBUG', false),
330
- ],
331
- // Ajoutez d'autres blocs selon vos besoins...
332
- ];
333
- ```
334
-
335
- ### Côté Backend SaaS — Controller multi-tenant
336
-
337
- Tous les controllers Native (`config`, `exchange`, `check-token`, `refresh`, `logout`) doivent lire le header :
338
-
339
- ```php
340
- class NativeConfigController extends Controller
341
- {
342
- public function getConfig(Request $request): JsonResponse
343
- {
344
- // Multi-tenant : résolution dynamique du préfixe
345
- $prefix = $request->header('X-IAM-Config-Prefix', 'iam');
346
-
347
- // Sécurité : valider que le préfixe commence par "iam"
348
- if (!str_starts_with($prefix, 'iam')) {
349
- return response()->json(['success' => false, 'message' => 'Invalid config prefix'], 400);
350
- }
351
-
352
- $appKey = config("services.{$prefix}.app_key");
353
- $secretKey = config("services.{$prefix}.secret_key");
354
- $debug = (bool) config("services.{$prefix}.debug", false);
355
-
356
- if (!$appKey || !$secretKey) {
357
- return response()->json([
358
- 'success' => false,
359
- 'message' => "Configuration '{$prefix}' non trouvée",
360
- ], 404);
361
- }
362
-
363
- // Chiffrement Opaque Token (AES-256-CBC)
364
- $payload = json_encode(['secret_key' => $secretKey, 'ts' => time()]);
365
- $key = hash('sha256', $secretKey, true);
366
- $iv = random_bytes(16);
367
- $encrypted = openssl_encrypt($payload, 'AES-256-CBC', $key, OPENSSL_RAW_DATA, $iv);
368
- $encryptedCredentials = base64_encode($iv . '::' . base64_encode($encrypted));
369
-
370
- return response()->json([
371
- 'success' => true,
372
- 'app_key' => $appKey,
373
- 'encrypted_credentials' => $encryptedCredentials,
374
- 'iam_api_url' => config("services.{$prefix}.api_url", 'https://identityam.ollaid.com/api'),
375
- 'credentials_ttl' => 300,
376
- 'debug' => $debug,
377
- ]);
378
- }
379
- }
380
- ```
381
-
382
- > **⚠️ Important** : Appliquez la même logique `X-IAM-Config-Prefix` dans `exchange`, `check-token`, `refresh` et `logout`.
383
-
384
- Le package envoie aussi :
385
- - `X-Device-Id` (stable par appareil / webview)
386
- - `X-Session-UUID` (UUID stable par instance, pour différencier plusieurs sessions sur un même device)
387
-
388
- ---
389
-
390
- ## Debug Mode — Troubleshooting
391
-
392
- Le debug est **piloté par le backend** : le package n'a aucun prop `debug`.
393
-
394
- ### Comment ça marche
395
-
396
- 1. Le backend lit `IAM_DEBUG` (ou `IAM_VENDOR_DEBUG`, etc.) depuis `.env`
397
- 2. `GET /api/native/config` retourne `"debug": true`
398
- 3. Le package active les logs console + le `DebugPanel`
399
- 4. Le `DebugPanel` est **réactif** : il apparaît automatiquement après le chargement des credentials
400
-
401
- ### Checklist de troubleshooting
402
-
403
- | Problème | Solution |
404
- |----------|----------|
405
- | `DebugPanel` n'apparaît pas | Vérifier que `GET /api/native/config` retourne `"debug": true` dans la réponse JSON |
406
- | La valeur est toujours `false` | Vérifier le `.env` : pas de typo ! (`IAM_DEBUG` et non `IAM_DEBUGL`) |
407
- | Changement non pris en compte | Lancer `php artisan config:clear && php artisan config:cache` |
408
- | Debug actif en production | Mettre `IAM_DEBUG=false` dans le `.env` de production |
409
- | Debug marche pour main mais pas vendor | Vérifier `IAM_VENDOR_DEBUG=true` dans `.env` + `config/services.php` |
410
-
411
- ### ⚠️ Erreur fréquente : typo dans `.env`
412
-
413
- ```env
414
- # ❌ MAUVAIS (typo "DEBUGL" avec un L en trop)
415
- IAM_DEBUGL=true
416
-
417
- # ✅ CORRECT
418
- IAM_DEBUG=true
419
- ```
420
-
421
- Après correction, n'oubliez pas :
422
- ```bash
423
- php artisan config:clear
424
- php artisan config:cache
425
- ```
426
-
427
- ---
428
-
429
- ## Usage avancé (composants individuels)
430
-
431
- Pour ceux qui veulent plus de contrôle :
432
-
433
- ```tsx
434
- import {
435
- NativeSSOProvider,
436
- LoginModal,
437
- SignupModal,
438
- useNativeAuth,
439
- } from '@ollaid/native-sso';
440
-
441
- function MyCustomAuth() {
442
- const [showLogin, setShowLogin] = useState(false);
443
-
444
- return (
445
- <>
446
- <button onClick={() => setShowLogin(true)}>Se connecter</button>
447
-
448
- <LoginModal
449
- open={showLogin}
450
- onOpenChange={setShowLogin}
451
- onSwitchToSignup={() => {}}
452
- onLoginSuccess={(token, user) => console.log('OK', user)}
453
- saasApiUrl="https://mon-saas.com/api"
454
- iamApiUrl="https://identityam.ollaid.com/api"
455
- defaultAccountType="user"
456
- />
457
- </>
458
- );
459
- }
460
- ```
461
-
462
- ---
463
-
464
- ## Props des composants individuels
465
-
466
- ### `SignupModal`
467
-
468
- | Prop | Type | Requis | Description |
469
- |------|------|--------|-------------|
470
- | `open` | `boolean` | ✅ | Contrôle l'ouverture du modal |
471
- | `onOpenChange` | `(open: boolean) => void` | ✅ | Callback de changement d'état |
472
- | `onSwitchToLogin` | `() => void` | ✅ | Callback pour basculer vers le login |
473
- | `onSignupSuccess` | `(token: string, user: UserInfos) => void` | ✅ | Callback après inscription réussie |
474
- | `saasApiUrl` | `string` | ✅ | URL du backend SaaS |
475
- | `iamApiUrl` | `string` | ✅ | URL du backend IAM |
476
- | `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'`. |
477
- | `onSwitchToLoginWithPhone` | `(phone: string) => void` | ❌ | Callback lors d'un conflit phone +221 — permet de basculer vers `LoginModal` avec le numéro pré-rempli |
478
-
479
- ### `LoginModal`
480
-
481
- | Prop | Type | Requis | Description |
482
- |------|------|--------|-------------|
483
- | `open` | `boolean` | ✅ | Contrôle l'ouverture du modal |
484
- | `onOpenChange` | `(open: boolean) => void` | ✅ | Callback de changement d'état |
485
- | `onSwitchToSignup` | `() => void` | ✅ | Callback pour basculer vers l'inscription |
486
- | `onLoginSuccess` | `(token: string, user: UserInfos) => void` | ❌ | Callback après connexion réussie |
487
- | `saasApiUrl` | `string` | ✅ | URL du backend SaaS |
488
- | `iamApiUrl` | `string` | ✅ | URL du backend IAM |
489
- | `loading` | `boolean` | ❌ | État de chargement externe |
490
- | `showSwitchToSignup` | `boolean` | ❌ | Afficher le lien "Pas de compte ? S'inscrire" (défaut: `true`) |
491
- | `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'`. |
492
- | `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. |
493
-
494
- ---
495
-
496
- ## Gestion des conflits d'inscription
497
-
498
- 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`).
499
-
500
- ### Comportement automatique
501
-
502
- 1. L'utilisateur remplit le formulaire d'inscription et soumet
503
- 2. Le backend détecte un conflit (`email_exists` ou `phone_exists`) et retourne un objet `conflict`
504
- 3. Le `SignupModal` affiche la `ConflictView` avec les options disponibles
505
-
506
- ### Affichage intelligent des identifiants
507
-
508
- - **L'identifiant saisi par l'utilisateur** (email ou téléphone) est affiché **en clair** pour confirmer sa saisie
509
- - **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`)
510
-
511
- ### Options proposées
512
-
513
- Selon le type de conflit et les capacités du compte existant, la vue propose :
514
-
515
- | Option | Condition | Action |
516
- |--------|-----------|--------|
517
- | Se connecter | `conflict.options.can_login === true` | Bascule vers `LoginModal` via `onSwitchToLogin` |
518
- | Se connecter par téléphone | Conflit phone + numéro +221 | Bascule vers `LoginModal` avec le numéro pré-rempli via `onSwitchToLoginWithPhone(phone)` |
519
- | Récupérer par email | `conflict.options.can_recover_by_email === true` | Ouvre le `PasswordRecoveryModal` |
520
- | Récupérer par SMS | `conflict.options.can_recover_by_sms === true` | Ouvre le `PasswordRecoveryModal` |
521
- | Modifier les informations | Toujours disponible | Retour au formulaire pour changer l'identifiant |
522
-
523
- ### Exemple d'intégration avec hand-off téléphone
524
-
525
- ```tsx
526
- function AuthPage() {
527
- const [showSignup, setShowSignup] = useState(false);
528
- const [showLogin, setShowLogin] = useState(false);
529
- const [loginPhone, setLoginPhone] = useState<string | undefined>();
530
-
531
- return (
532
- <>
533
- <SignupModal
534
- open={showSignup}
535
- onOpenChange={setShowSignup}
536
- onSwitchToLogin={() => { setShowSignup(false); setShowLogin(true); }}
537
- onSwitchToLoginWithPhone={(phone) => {
538
- setLoginPhone(phone);
539
- setShowSignup(false);
540
- setShowLogin(true);
541
- }}
542
- onSignupSuccess={(token, user) => navigate('/dashboard')}
543
- saasApiUrl="https://mon-saas.com/api"
544
- iamApiUrl="https://identityam.ollaid.com/api"
545
- />
546
-
547
- <LoginModal
548
- open={showLogin}
549
- onOpenChange={setShowLogin}
550
- onSwitchToSignup={() => { setShowLogin(false); setShowSignup(true); }}
551
- initialPhone={loginPhone}
552
- saasApiUrl="https://mon-saas.com/api"
553
- iamApiUrl="https://identityam.ollaid.com/api"
554
- />
555
- </>
556
- );
557
- }
558
- ```
559
-
560
- ### Type `RegistrationConflict`
561
-
562
- L'objet retourné par le backend lors d'un conflit :
563
-
564
- ```typescript
565
- interface RegistrationConflict {
566
- type: 'email' | 'phone';
567
- masked_identifier: string;
568
- options: {
569
- can_login: boolean;
570
- can_recover_by_email: boolean;
571
- can_recover_by_sms: boolean;
572
- masked_email?: string;
573
- masked_phone?: string;
574
- };
575
- }
576
- ```
577
-
578
- ---
579
-
580
- ## Hook `useMobileRegistration`
581
-
582
- Hook React pour gérer le flow d'inscription mobile en 3 étapes : `init` → `verify-otp` → `complete`.
583
-
584
- ### Import
585
-
586
- ```tsx
587
- import { useMobileRegistration } from '@ollaid/native-sso';
588
- ```
589
-
590
- ### Propriétés retournées
591
-
592
- #### État
593
-
594
- | Propriété | Type | Description |
595
- |-----------|------|-------------|
596
- | `processToken` | `string \| null` | Token de processus d'inscription en cours |
597
- | `status` | `'idle' \| 'pending_otp' \| 'pending_password' \| 'pending_registration' \| 'completed'` | Étape actuelle du flow |
598
- | `formData` | `Partial<MobileRegistrationFormData>` | Données du formulaire stockées |
599
- | `loading` | `boolean` | Indique si une opération est en cours |
600
- | `error` | `string \| null` | Message d'erreur (français, user-friendly) |
601
- | `conflict` | `RegistrationConflict \| null` | Objet de conflit si l'identifiant existe déjà |
602
- | `isCompleted` | `boolean` | `true` si l'inscription est terminée |
603
- | `hasConflict` | `boolean` | `true` si un conflit est en cours |
604
-
605
- #### Type de compte (phone-only)
606
-
607
- | Propriété | Type | Description |
608
- |-----------|------|-------------|
609
- | `accountType` | `'email' \| 'phone-only'` | Type de compte sélectionné |
610
- | `setAccountType` | `(type: AccountType) => void` | Change le type de compte |
611
- | `isPhoneOnly` | `boolean` | `true` si `accountType === 'phone-only'` |
612
-
613
- #### Méthodes
614
-
615
- | Méthode | Signature | Description |
616
- |---------|-----------|-------------|
617
- | `updateFormData` | `(data: Partial<MobileRegistrationFormData>) => void` | Met à jour les données du formulaire |
618
- | `initRegistration` | `(data) => Promise<{ success, otp_code_dev?, otp_method?, otp_sent_to? }>` | Initialise l'inscription (envoi OTP). Peut retourner un conflit. |
619
- | `verifyOtp` | `(otpCode: string) => Promise<{ success, completed?, callback_token? }>` | Vérifie le code OTP |
620
- | `completeRegistration` | `(password: string) => Promise<{ success, callback_token? }>` | Finalise avec mot de passe (type `email`) |
621
- | `completePhoneOnlyRegistration` | `() => Promise<{ success, callback_token? }>` | Finalise sans mot de passe (type `phone-only`, Sénégal 🇸🇳) |
622
- | `resendOtp` | `() => Promise<{ success, cooldown?, otp_code_dev? }>` | Renvoie le code OTP |
623
- | `reset` | `() => void` | Réinitialise tout le hook |
624
- | `clearError` | `() => void` | Efface l'erreur et le conflit |
625
-
626
- ---
627
-
628
- ## Backend SaaS — Endpoints requis
629
-
630
- Le backend SaaS (Laravel) doit exposer **5 endpoints** : `config`, `exchange`, `check-token`, `refresh`, `logout`.
631
- Voici les spécifications exactes :
632
-
633
- ### `GET /api/native/config`
634
-
635
- Retourne un **token opaque chiffré** (`encrypted_credentials`) contenant les credentials IAM.
636
- ⚠️ **Les clés `app_key` et `secret_key` ne sont JAMAIS exposées au frontend.**
637
-
638
- **Headers requis :** aucun
639
-
640
- **Réponse succès (200) :**
641
- ```json
642
- {
643
- "success": true,
644
- "app_key": "oiam_ak_xxxxxxxxxxxxxxxxxxxxxxxx",
645
- "encrypted_credentials": "base64_encoded_iv_plus_ciphertext...",
646
- "debug": true // optionnel — active le DebugPanel et les logs console côté frontend
647
- }
648
- ```
649
-
650
- **Principe :**
651
- Le SaaS chiffre `secret_key + timestamp` en AES-256-CBC avec `OPENSSL_RAW_DATA`, en utilisant la `secret_key` elle-même comme clé de chiffrement (SHA-256 → 32 bytes).
652
- Le frontend transporte le blob opaque + l'`app_key` en clair (non sensible) vers l'IAM.
653
- L'IAM retrouve la `secret_key` via l'`app_key` dans sa table `applications`, puis déchiffre le blob.
654
-
655
- **Implémentation Laravel :**
656
- ```php
657
- // routes/api.php
658
- Route::get('/native/config', function () {
659
- $appKey = config('services.iam.app_key');
660
- $secretKey = config('services.iam.secret_key');
661
- $iamApiUrl = config('services.iam.api_url');
662
-
663
- // Clé AES = SHA-256 de la secret_key (32 bytes binaires)
664
- $encryptionKey = hash('sha256', $secretKey, true);
665
- $iv = random_bytes(16);
666
-
667
- $payload = json_encode([
668
- 'secret_key' => $secretKey,
669
- 'ts' => time(),
670
- ]);
671
-
672
- $encrypted = openssl_encrypt($payload, 'AES-256-CBC', $encryptionKey, OPENSSL_RAW_DATA, $iv);
673
-
674
- return response()->json([
675
- 'success' => true,
676
- 'app_key' => $appKey, // En clair (non sensible, sert d'identifiant)
677
- 'encrypted_credentials' => base64_encode($iv . '::' . base64_encode($encrypted)),
678
- 'iam_api_url' => $iamApiUrl,
679
- 'credentials_ttl' => 300,
680
- 'debug' => (bool) config('services.iam.debug'),
681
- ]);
682
- });
683
- ```
684
-
685
- **Décryptage côté IAM :**
686
- ```php
687
- // Dans le endpoint /iam/native/encrypt
688
- // 1. Retrouver la secret_key via l'app_key
689
- $app = Application::where('app_key', $request->app_key)->first();
690
- if (!$app) {
691
- return response()->json(['success' => false, 'message' => 'Application inconnue'], 401);
692
- }
693
-
694
- // 2. Déchiffrer avec la secret_key de l'application
695
- $encryptionKey = hash('sha256', $app->secret_key, true);
696
- $decoded = base64_decode($request->encrypted_credentials);
697
- [$iv, $ciphertextB64] = explode('::', $decoded, 2);
698
-
699
- $payload = json_decode(
700
- openssl_decrypt(base64_decode($ciphertextB64), 'AES-256-CBC', $encryptionKey, OPENSSL_RAW_DATA, $iv),
701
- true
702
- );
703
-
704
- // 3. Vérifier le timestamp (anti-replay, max 5 min)
705
- if (!$payload || time() - $payload['ts'] > 300) {
706
- return response()->json(['success' => false, 'message' => 'Credentials expirés ou invalides'], 401);
707
- }
708
-
709
- $secretKey = $payload['secret_key'];
710
- // ... valider et continuer le flux
711
- ```
712
-
713
- ---
714
-
715
- ### `POST /api/native/exchange`
716
-
717
- > ⚠️ **IMPORTANT — Route PUBLIQUE obligatoire**
718
- > Cette route **NE DOIT PAS** être derrière le middleware `auth:sanctum`.
719
- > Si vous la placez dans un groupe `Route::middleware('auth:sanctum')`, le frontend recevra une **erreur 401 Unauthenticated** systématique car l'utilisateur n'a pas encore de token Sanctum à ce stade du flux.
720
- > Assurez-vous que `/api/native/config` et `/api/native/exchange` sont **en dehors** de tout groupe authentifié.
721
-
722
- Échange le `callback_token` reçu de l'IAM contre un token Sanctum local.
723
-
724
- **Headers requis :** `Content-Type: application/json`
725
-
726
- **Body de la requête :**
727
- ```json
728
- {
729
- "callback_token": "eyJhbGciOiJIUzI1NiIs..."
730
- }
731
- ```
732
-
733
- **Logique backend :**
734
- 1. Recevoir le `callback_token`
735
- 2. Appeler l'IAM `POST /iam/auth/decrypt` avec les credentials dans les headers (`X-IAM-App-Key`, `X-IAM-Secret-Key`) et le token dans le body
736
- 3. L'IAM retourne les `user_infos` (9 champs standardisés) + `app_access_token_ref`
737
- 4. Créer ou mettre à jour l'utilisateur local
738
- 5. Générer un token Sanctum
739
- 6. Retourner le token + user
740
-
741
- **Réponse succès (200) :**
742
- ```json
743
- {
744
- "success": true,
745
- "token": "1|abc123def456ghi789...",
746
- "expires_at": "2026-04-23T03:12:32.000000Z",
747
- "app_access_token_ref": "aat_ref_XXXXXXXX",
748
- "user": {
749
- "id": 1,
750
- "reference": "USR-XXXXXXXX",
751
- "alias_reference": "ALI-XXXXXXXX",
752
- "name": "John Doe",
753
- "email": "john@example.com",
754
- "phone": "+221771234567",
755
- "ccphone": "+221",
756
- "image_url": "https://identityam.ollaid.com/storage/avatars/xxx.jpg",
757
- "email_verified": true,
758
- "phone_verified": true
759
- }
760
- }
761
- ```
762
-
763
- **Implémentation Laravel :**
764
- ```php
765
- // routes/api.php
766
- Route::post('/native/exchange', function (Request $request) {
767
- $iamApiUrl = config('services.iam.api_url');
768
-
769
- // Appel IAM pour décrypter — credentials dans les HEADERS
770
- $response = Http::withHeaders([
771
- 'X-IAM-App-Key' => config('services.iam.app_key'),
772
- 'X-IAM-Secret-Key' => config('services.iam.secret_key'),
773
- ])->post("{$iamApiUrl}/iam/auth/decrypt", [
774
- 'token' => $request->callback_token,
775
- ]);
776
-
777
- if (!$response->successful() || !$response->json('success')) {
778
- return response()->json([
779
- 'success' => false,
780
- 'error' => 'Token invalide ou expiré',
781
- 'error_type' => 'invalid_token',
782
- ], 401);
783
- }
784
-
785
- $data = $response->json();
786
- $userInfos = $data['user_infos'];
787
- $appAccessTokenRef = $data['app_access_token_ref'] ?? null;
788
-
789
- // Créer ou mettre à jour l'utilisateur local
790
- $user = User::updateOrCreate(
791
- ['reference' => $userInfos['reference']],
792
- [
793
- 'name' => $userInfos['name'],
794
- 'email' => $userInfos['email'],
795
- 'phone' => $userInfos['phone'] ?? null,
796
- 'ccphone' => $userInfos['ccphone'] ?? null,
797
- 'image' => $userInfos['image_url'] ?? null,
798
- 'alias_reference' => $userInfos['alias_reference'],
799
- 'user_infos' => json_encode($userInfos),
800
- 'password' => bcrypt(Str::random(32)),
801
- ]
802
- );
803
-
804
- // Générer un token Sanctum
805
- $token = $user->createToken('native-sso')->plainTextToken;
806
-
807
- return response()->json([
808
- 'success' => true,
809
- 'token' => $token,
810
- 'expires_at' => now()->addDays(30)->toISOString(),
811
- 'app_access_token_ref' => $appAccessTokenRef,
812
- 'user' => [
813
- 'id' => $user->id,
814
- 'reference' => $user->reference,
815
- 'alias_reference' => $user->alias_reference,
816
- 'name' => $user->name,
817
- 'email' => $user->email,
818
- 'phone' => $user->phone,
819
- 'ccphone' => $user->ccphone,
820
- 'image_url' => $user->image,
821
- 'email_verified' => $userInfos['email_verification'] === 'verified',
822
- 'phone_verified' => $userInfos['phone_verification'] === 'verified',
823
- ],
824
- ]);
825
- });
826
- ```
827
-
828
- ---
829
-
830
- ### `POST /api/native/check-token`
831
-
832
- 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).
833
-
834
- **Headers requis :** `Authorization: Bearer {token}`
835
-
836
- **Réponse succès (200) :**
837
- ```json
838
- {
839
- "success": true,
840
- "status": "connected",
841
- "user": {
842
- "name": "John Doe",
843
- "email": "john@example.com",
844
- "ccphone": "+221",
845
- "phone": "771234567",
846
- "image_url": "https://...",
847
- "town": "Dakar",
848
- "country": "SN",
849
- "auth_2fa": false
850
- }
851
- }
852
- ```
853
-
854
- **Si token invalide/expiré :** Sanctum retourne automatiquement 401.
855
-
856
- **Implémentation Laravel :**
857
- ```php
858
- // routes/api.php
859
- Route::post('/native/check-token', function (Request $request) {
860
- $user = $request->user();
861
-
862
- return response()->json([
863
- 'success' => true,
864
- 'status' => 'connected',
865
- 'user' => [
866
- 'name' => $user->name,
867
- 'email' => $user->email,
868
- 'ccphone' => $user->ccphone,
869
- 'phone' => $user->phone,
870
- 'image_url' => $user->image,
871
- 'town' => $user->town ?? null,
872
- 'country' => $user->country ?? null,
873
- 'auth_2fa' => (bool) ($user->auth_2fa ?? false),
874
- ],
875
- ]);
876
- })->middleware('auth:sanctum');
877
- ```
878
-
879
- > **Règle de déconnexion :** Le package ne déconnecte l'utilisateur **que** sur un **HTTP 401 explicite**. Tout HTTP 200 (quelle que soit la structure du body) confirme la session. Erreur réseau, timeout, 5xx, offline → session conservée, jamais de déconnexion inutile.
880
-
881
- ---
882
-
883
- ### `POST /api/native/refresh` (OBLIGATOIRE pour la stabilité)
884
-
885
- Renouvelle la session **sans déconnecter**. Le package l'utilise :
886
- - en **proactif** (avant expiration, si `expires_at` est fourni)
887
- - en **récupération** quand `check-token` retourne `401` (tente un refresh avant logout)
888
-
889
- **Body :**
890
- ```json
891
- { "refresh_token": "rt_..." }
892
- ```
893
-
894
- **Recommandation (stabilité maximale) :** ne pas changer le token Sanctum.
895
- Le refresh doit **prolonger `expires_at`** du token existant (le client garde son token actuel).
896
-
897
- **Réponse succès (200) :**
898
- ```json
899
- {
900
- "success": true,
901
- "expires_at": "2026-06-13T12:00:00+00:00",
902
- "refresh_token": "rt_new_...",
903
- "refresh_expires_at": "2026-08-13T12:00:00+00:00",
904
- "user": { "name": "John Doe" }
905
- }
906
- ```
907
-
908
- **Réponse refresh invalide (401) :**
909
- ```json
910
- {
911
- "success": false,
912
- "error_type": "invalid_refresh",
913
- "message": "Refresh token invalide ou expiré"
914
- }
915
- ```
916
-
917
- > Si le refresh est invalide (`401 invalid_refresh`) : c'est une révocation explicite, la déconnexion est autorisée.
918
- > Si offline/timeout/5xx : ne jamais déconnecter.
919
-
920
- ---
921
-
922
- ### `POST /api/native/logout` (single-session)
923
-
924
- Invalide **uniquement** le token Sanctum courant (`currentAccessToken()->delete()`). Les autres sessions actives de l'utilisateur ne sont pas affectées.
925
-
926
- **Headers requis :** `Authorization: Bearer {token}`
927
-
928
- **Réponse succès (200) :**
929
- ```json
930
- {
931
- "success": true,
932
- "message": "Déconnexion réussie"
933
- }
934
- ```
935
-
936
- **Implémentation Laravel :**
937
- ```php
938
- // routes/api.php
939
- Route::post('/native/logout', function (Request $request) {
940
- // Supprime UNIQUEMENT le token courant (pas les autres sessions)
941
- $request->user()->currentAccessToken()->delete();
942
-
943
- return response()->json([
944
- 'success' => true,
945
- 'message' => 'Déconnexion réussie',
946
- ]);
947
- })->middleware('auth:sanctum');
948
- ```
949
-
950
- ---
951
-
952
- ## Controller Laravel Complet (copier-coller)
953
-
954
- Voici un `NativeAuthController.php` complet regroupant les 5 endpoints. Copiez-le dans `app/Http/Controllers/Api/NativeAuthController.php` :
955
-
956
- ```php
957
- <?php
958
-
959
- namespace App\Http\Controllers\Api;
960
-
961
- use App\Http\Controllers\Controller;
962
- use App\Models\User;
963
- use Illuminate\Http\JsonResponse;
964
- use Illuminate\Http\Request;
965
- use Illuminate\Support\Facades\Http;
966
- use Illuminate\Support\Facades\Log;
967
- use Illuminate\Support\Str;
968
-
969
- class NativeAuthController extends Controller
970
- {
971
- // ════════════════════════════════════════
972
- // GET /api/native/config
973
- // ════════════════════════════════════════
974
-
975
- /**
976
- * Retourne les credentials IAM chiffrés (opaque token).
977
- * Le frontend ne voit JAMAIS app_key ni secret_key en clair.
978
- */
979
- public function config(): JsonResponse
980
- {
981
- $appKey = config('services.iam.app_key');
982
- $secretKey = config('services.iam.secret_key');
983
-
984
- if (empty($appKey) || empty($secretKey)) {
985
- Log::error('[NativeSSO] Config manquante : IAM_APP_KEY ou IAM_SECRET_KEY');
986
- return response()->json([
987
- 'success' => false,
988
- 'error' => 'Configuration SSO incomplète',
989
- 'error_type' => 'config_missing',
990
- ], 500);
991
- }
992
-
993
- // Clé AES = SHA-256 de la secret_key (32 bytes binaires)
994
- $aesKey = hash('sha256', $secretKey, true);
995
- $iv = random_bytes(16);
996
- $payload = json_encode([
997
- 'secret_key' => $secretKey,
998
- 'ts' => time(),
999
- ]);
1000
-
1001
- $encrypted = openssl_encrypt($payload, 'aes-256-cbc', $aesKey, OPENSSL_RAW_DATA, $iv);
1002
- $encryptedCredentials = base64_encode($iv . '::' . base64_encode($encrypted));
1003
-
1004
- return response()->json([
1005
- 'success' => true,
1006
- 'app_key' => $appKey, // En clair (non sensible, sert d'identifiant pour l'IAM)
1007
- 'encrypted_credentials' => $encryptedCredentials,
1008
- 'iam_api_url' => config('services.iam.api_url', 'https://identityam.ollaid.com/api'),
1009
- 'credentials_ttl' => 300,
1010
- 'debug' => (bool) config('services.iam.debug'), // Contrôlé par IAM_DEBUG dans .env
1011
- ]);
1012
- }
1013
-
1014
- // ════════════════════════════════════════
1015
- // POST /api/native/exchange
1016
- // ════════════════════════════════════════
1017
-
1018
- /**
1019
- * Échange le callback_token IAM contre un token Sanctum local.
1020
- *
1021
- * 1. Reçoit callback_token du frontend
1022
- * 2. Appelle IAM /iam/auth/decrypt pour décrypter
1023
- * 3. Crée ou met à jour l'utilisateur local
1024
- * 4. Génère un token Sanctum
1025
- */
1026
- public function exchange(Request $request): JsonResponse
1027
- {
1028
- $callbackToken = $request->input('callback_token');
1029
-
1030
- if (empty($callbackToken)) {
1031
- return response()->json([
1032
- 'success' => false,
1033
- 'error' => 'callback_token est requis',
1034
- 'error_type' => 'missing_token',
1035
- ], 400);
1036
- }
1037
-
1038
- try {
1039
- $response = Http::timeout(30)
1040
- ->withHeaders([
1041
- 'Content-Type' => 'application/json',
1042
- 'Accept' => 'application/json',
1043
- 'X-IAM-App-Key' => config('services.iam.app_key'),
1044
- 'X-IAM-Secret-Key' => config('services.iam.secret_key'),
1045
- ])
1046
- ->post(config('services.iam.api_url') . '/iam/auth/decrypt', [
1047
- 'token' => $callbackToken,
1048
- ]);
1049
-
1050
- if (!$response->successful() || !$response->json('success')) {
1051
- $error = $response->json();
1052
- Log::warning('[NativeSSO] Échec décryptage callback_token', [
1053
- 'status' => $response->status(),
1054
- 'error' => $error['message'] ?? 'Inconnu',
1055
- ]);
1056
-
1057
- return response()->json([
1058
- 'success' => false,
1059
- 'error' => $error['message'] ?? 'Token invalide ou expiré',
1060
- 'error_type' => 'invalid_token',
1061
- ], 401);
1062
- }
1063
-
1064
- $data = $response->json('data', $response->json());
1065
- $iamReference = $data['iam_reference'] ?? null;
1066
- $aliasReference = $data['alias_reference'] ?? null;
1067
- $userInfos = $data['user_infos'] ?? [];
1068
-
1069
- if (!$iamReference || empty($userInfos)) {
1070
- return response()->json([
1071
- 'success' => false,
1072
- 'error' => 'Données utilisateur incomplètes depuis l\'IAM',
1073
- 'error_type' => 'decrypt_failed',
1074
- ], 422);
1075
- }
1076
-
1077
- // Créer ou mettre à jour l'utilisateur local
1078
- $user = User::where('reference', $iamReference)->first();
1079
- if (!$user && $aliasReference) {
1080
- $user = User::where('alias_reference', $aliasReference)->first();
1081
- }
1082
-
1083
- $userData = [
1084
- 'name' => $userInfos['name'] ?? null,
1085
- 'email' => $userInfos['email'] ?? null,
1086
- 'phone' => $userInfos['phone'] ?? null,
1087
- 'ccphone' => $userInfos['ccphone'] ?? null,
1088
- 'image' => $userInfos['image_url'] ?? null,
1089
- 'alias_reference' => $aliasReference,
1090
- 'user_infos' => json_encode($userInfos),
1091
- ];
1092
-
1093
- if ($user) {
1094
- $user->update($userData);
1095
- } else {
1096
- $user = User::create(array_merge($userData, [
1097
- 'reference' => $iamReference,
1098
- 'password' => bcrypt(Str::random(32)),
1099
- ]));
1100
- }
1101
-
1102
- // Générer un token Sanctum (expiration 30 jours)
1103
- $expiresAt = now()->addDays(30);
1104
- $token = $user->createToken('native-sso', ['*'], $expiresAt);
1105
-
1106
- // Récupérer app_access_token_ref depuis la racine de la réponse IAM
1107
- $appAccessTokenRef = $data['app_access_token_ref'] ?? null;
1108
-
1109
- // Stocker la ref dans le token Sanctum pour le webhook de révocation
1110
- if ($appAccessTokenRef) {
1111
- $token->accessToken->forceFill([
1112
- 'app_access_token_ref' => $appAccessTokenRef,
1113
- ])->save();
1114
- }
1115
-
1116
- return response()->json([
1117
- 'success' => true,
1118
- 'token' => $token->plainTextToken,
1119
- 'expires_at' => $expiresAt->toIso8601String(),
1120
- 'app_access_token_ref' => $appAccessTokenRef,
1121
- 'user' => [
1122
- 'id' => $user->id,
1123
- 'reference' => $iamReference,
1124
- 'alias_reference' => $aliasReference,
1125
- 'name' => $userInfos['name'] ?? null,
1126
- 'email' => $userInfos['email'] ?? null,
1127
- 'phone' => isset($userInfos['phone'])
1128
- ? ($userInfos['ccphone'] ?? '') . $userInfos['phone']
1129
- : null,
1130
- 'ccphone' => $userInfos['ccphone'] ?? null,
1131
- 'image_url' => $userInfos['image_url'] ?? null,
1132
- 'email_verified' => ($userInfos['email_verification'] ?? '') === 'verified',
1133
- 'phone_verified' => ($userInfos['phone_verification'] ?? '') === 'verified',
1134
- ],
1135
- ]);
1136
-
1137
- } catch (\Illuminate\Http\Client\ConnectionException $e) {
1138
- Log::error('[NativeSSO] Timeout connexion IAM', ['error' => $e->getMessage()]);
1139
- return response()->json([
1140
- 'success' => false,
1141
- 'error' => 'Impossible de contacter le serveur d\'authentification',
1142
- 'error_type' => 'exchange_error',
1143
- ], 503);
1144
-
1145
- } catch (\Exception $e) {
1146
- Log::error('[NativeSSO] Erreur exchange', ['error' => $e->getMessage()]);
1147
- $details = [];
1148
- if (config('services.iam.debug')) {
1149
- $details = [
1150
- 'debug_error' => $e->getMessage(),
1151
- 'debug_file' => basename($e->getFile()) . ':' . $e->getLine(),
1152
- ];
1153
- }
1154
- return response()->json([
1155
- 'success' => false,
1156
- 'error' => 'Erreur lors de l\'authentification',
1157
- 'error_type' => 'exchange_error',
1158
- ...$details,
1159
- ], 500);
1160
- }
1161
- }
1162
-
1163
- // ════════════════════════════════════════
1164
- // POST /api/native/check-token
1165
- // ════════════════════════════════════════
1166
-
1167
- /**
1168
- * Vérifie la validité du token Sanctum et retourne les user_infos fraîches.
1169
- * Route protégée par auth:sanctum.
1170
- */
1171
- public function checkToken(Request $request): JsonResponse
1172
- {
1173
- $user = $request->user();
1174
-
1175
- return response()->json([
1176
- 'success' => true,
1177
- 'status' => 'connected',
1178
- 'message' => 'Utilisateur connecté',
1179
- 'user' => [
1180
- 'name' => $user->name,
1181
- 'email' => $user->email,
1182
- 'ccphone' => $user->ccphone,
1183
- 'phone' => $user->phone,
1184
- 'image_url' => $user->image,
1185
- 'town' => $user->town ?? null,
1186
- 'country' => $user->country ?? null,
1187
- 'auth_2fa' => (bool) ($user->auth_2fa ?? false),
1188
- ],
1189
- ]);
1190
- }
1191
-
1192
- // ════════════════════════════════════════
1193
- // POST /api/native/logout (déconnexion synchronisée)
1194
- // ════════════════════════════════════════
1195
-
1196
- /**
1197
- * Révoque le token Sanctum courant (single-session)
1198
- * ET notifie l'IAM pour révoquer l'AppAccessToken lié.
1199
- * Route protégée par auth:sanctum.
1200
- */
1201
- public function logout(Request $request): JsonResponse
1202
- {
1203
- try {
1204
- $user = $request->user();
1205
-
1206
- if ($user) {
1207
- $sanctumTokenPlain = $request->bearerToken();
1208
- $currentToken = $user->currentAccessToken();
1209
- $appAccessTokenRef = $currentToken?->app_access_token_ref ?? null;
1210
-
1211
- // Supprimer le token APRÈS avoir lu la ref
1212
- $currentToken?->delete();
1213
-
1214
- // Notifier l'IAM (fire-and-forget, timeout 5s)
1215
- if ($sanctumTokenPlain || $appAccessTokenRef) {
1216
- $iamPrefix = $request->attributes->get('iam_prefix', 'iam');
1217
- $iamApiUrl = config("services.{$iamPrefix}.api_url", 'https://identityam.ollaid.com/api');
1218
- try {
1219
- Http::timeout(5)->post("{$iamApiUrl}/iam/disconnect", array_filter([
1220
- 'sanctum_token' => $sanctumTokenPlain,
1221
- 'app_access_token_ref' => $appAccessTokenRef,
1222
- ]));
1223
- } catch (\Exception $e) {
1224
- Log::warning("[NativeSSO] IAM disconnect failed (non-blocking)", ['error' => $e->getMessage()]);
1225
- }
1226
- }
1227
- }
1228
-
1229
- return response()->json(['success' => true, 'message' => 'Déconnexion réussie']);
1230
- } catch (\Exception $e) {
1231
- return response()->json(['success' => true, 'message' => 'Déconnexion effectuée']);
1232
- }
1233
- }
1234
- }
1235
- ```
1236
-
1237
- ### Routes `routes/api.php`
1238
-
1239
- > ⚠️ **Attention au placement des routes !**
1240
- > `config` et `exchange` doivent être **publiques** (pas de middleware auth).
1241
- > `check-token` et `logout` doivent être **protégées** par `auth:sanctum`.
1242
- > Si `exchange` est derrière `auth:sanctum`, le frontend recevra un **401 Unauthenticated**.
1243
-
1244
- ```php
1245
- use App\Http\Controllers\Api\NativeAuthController;
1246
-
1247
- // ✅ Routes PUBLIQUES (pas d'auth requise — l'utilisateur n'a pas encore de token)
1248
- Route::get('/native/config', [NativeAuthController::class, 'config']);
1249
- Route::post('/native/exchange', [NativeAuthController::class, 'exchange']);
1250
-
1251
- // 🔒 Routes PROTÉGÉES (auth Sanctum requise — l'utilisateur a un token)
1252
- Route::middleware('auth:sanctum')->group(function () {
1253
- Route::post('/native/check-token', [NativeAuthController::class, 'checkToken']);
1254
- Route::post('/native/logout', [NativeAuthController::class, 'logout']);
1255
- });
1256
- ```
1257
-
1258
- ### Middleware `ForceJsonResponse` (recommandé)
1259
-
1260
- Pour éviter que Laravel retourne du HTML au lieu de JSON en cas d'erreur :
1261
-
1262
- ```php
1263
- // app/Http/Middleware/ForceJsonResponse.php
1264
- namespace App\Http\Middleware;
1265
-
1266
- use Closure;
1267
- use Illuminate\Http\Request;
1268
-
1269
- class ForceJsonResponse
1270
- {
1271
- public function handle(Request $request, Closure $next)
1272
- {
1273
- $request->headers->set('Accept', 'application/json');
1274
- return $next($request);
1275
- }
1276
- }
1277
- ```
1278
-
1279
- Appliquez-le sur les routes API dans `bootstrap/app.php` (Laravel 11+) :
1280
- ```php
1281
- ->withMiddleware(function (Middleware $middleware) {
1282
- $middleware->api(prepend: [
1283
- \App\Http\Middleware\ForceJsonResponse::class,
1284
- ]);
1285
- })
1286
- ```
1287
-
1288
- Ou dans `app/Http/Kernel.php` (Laravel 10 et avant) :
1289
- ```php
1290
- 'api' => [
1291
- \App\Http\Middleware\ForceJsonResponse::class,
1292
- // ... autres middlewares
1293
- ],
1294
- ```
1295
-
1296
- ---
1297
-
1298
- ## Réponses d'erreur
1299
-
1300
- Tous les endpoints retournent le même format d'erreur :
1301
-
1302
- ```json
1303
- {
1304
- "success": false,
1305
- "error": "Description lisible de l'erreur",
1306
- "error_type": "code_erreur"
1307
- }
1308
- ```
1309
-
1310
- ### Codes d'erreur par endpoint
1311
-
1312
- #### `/api/native/config`
1313
-
1314
- | Code HTTP | `error_type` | Description |
1315
- |-----------|-------------|-------------|
1316
- | 500 | `config_missing` | `IAM_APP_KEY` ou `IAM_SECRET_KEY` non configuré dans le `.env` |
1317
-
1318
- #### `/api/native/exchange`
1319
-
1320
- | Code HTTP | `error_type` | Description |
1321
- |-----------|-------------|-------------|
1322
- | 400 | `missing_token` | `callback_token` absent du body |
1323
- | 401 | `invalid_token` | Token expiré, invalide ou déjà utilisé |
1324
- | 422 | `decrypt_failed` | Erreur lors de l'appel à l'IAM `/iam/auth/decrypt` |
1325
- | 500 | `exchange_error` | Erreur interne lors de la création du user/token |
1326
-
1327
- #### `/api/native/logout`
1328
-
1329
- | Code HTTP | `error_type` | Description |
1330
- |-----------|-------------|-------------|
1331
- | 401 | `unauthenticated` | Token Bearer manquant ou invalide |
1332
-
1333
- ---
1334
-
1335
- ## Configuration .env Laravel
1336
-
1337
- Ajoutez ces variables dans le fichier `.env` du backend SaaS :
1338
-
1339
- ```env
1340
- # Credentials IAM (récupérés depuis le dashboard iam.ollaid.com)
1341
- IAM_APP_KEY=oiam_ak_xxxxxxxxxxxxxxxxxxxxxxxx
1342
- IAM_SECRET_KEY=oiam_sk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
1343
- IAM_API_URL=https://identityam.ollaid.com/api
1344
-
1345
- # Mode debug — contrôle le DebugPanel et les logs côté frontend
1346
- # true : active le DebugPanel + logs console + détails d'erreur dans les réponses JSON
1347
- # false : mode production (aucun détail d'erreur exposé, pas de DebugPanel)
1348
- # ⚠️ Si absent ou null → considéré comme false (mode production)
1349
- IAM_DEBUG=false
1350
- ```
1351
-
1352
- > **Note :** Pas de clé partagée supplémentaire. Le chiffrement AES-256-CBC utilise directement un hash SHA-256 de la `secret_key` comme clé de chiffrement. L'IAM retrouve la `secret_key` via l'`app_key` dans sa table `applications`.
1353
-
1354
- Et dans `config/services.php` :
1355
-
1356
- ```php
1357
- 'iam' => [
1358
- 'app_key' => env('IAM_APP_KEY'),
1359
- 'secret_key' => env('IAM_SECRET_KEY'),
1360
- 'api_url' => env('IAM_API_URL', 'https://identityam.ollaid.com/api'),
1361
- 'debug' => env('IAM_DEBUG', false), // Active le debug côté frontend (false si absent)
1362
- ],
1363
- ```
1364
-
1365
- ---
1366
-
1367
- ## Migration Laravel
1368
-
1369
- ### Colonnes requises sur la table `users`
1370
-
1371
- Si votre table `users` n'a pas encore ces colonnes, ajoutez-les :
1372
-
1373
- ```bash
1374
- php artisan make:migration add_iam_columns_to_users_table
1375
- ```
1376
-
1377
- ```php
1378
- public function up(): void
1379
- {
1380
- Schema::table('users', function (Blueprint $table) {
1381
- $table->string('reference')->unique()->nullable()->after('id');
1382
- $table->string('alias_reference')->nullable()->after('reference');
1383
- $table->string('ccphone')->nullable();
1384
- $table->string('phone')->nullable();
1385
- $table->string('image')->nullable();
1386
- $table->json('user_infos')->nullable();
1387
- $table->string('phone_verification')->default('pending')->nullable();
1388
- $table->string('email_verification')->default('pending')->nullable();
1389
- });
1390
- }
1391
- ```
1392
-
1393
- > **Note :** Le champ `reference` est l'identifiant unique IAM de l'utilisateur. Le champ `alias_reference` identifie l'utilisateur dans le contexte d'une application spécifique.
1394
-
1395
- ### Colonnes recommandées sur `personal_access_tokens` (Sanctum)
1396
-
1397
- Pour la révocation synchronisée et la stabilité de session :
1398
- - `app_access_token_ref` (révocation IAM ciblée)
1399
- - `refresh_token_hash` + `refresh_expires_at` (refresh token)
1400
-
1401
- Ces migrations sont détaillées dans `BACKEND_INTEGRATION.md` du package.
1402
-
1403
- ---
1404
-
1405
- ## Flux d'authentification
1406
-
1407
- ```
1408
- ┌──────────────────┐ ┌──────────────┐ ┌──────────────┐
1409
- │ @ollaid/native │ │ IAM API │ │ SaaS API │
1410
- │ -sso (Frontend) │ │ (Ollaid) │ │ (Laravel) │
1411
- └────────┬─────────┘ └──────┬───────┘ └──────┬───────┘
1412
- │ │ │
1413
- │ 1. GET /api/native/config │
1414
- │───────────────────────────────────────────►│
1415
- │◄──────────────── encrypted_credentials ────│
1416
- │ │ │
1417
- │ 2. POST /iam/native/encrypt │
1418
- │─────────────────────►│ │
1419
- │◄── encrypted_credentials │
1420
- │ │ │
1421
- │ 3. POST /iam/native/init │
1422
- │─────────────────────►│ │
1423
- │◄── status + session │ │
1424
- │ │ │
1425
- │ 4. POST /iam/native/validate │
1426
- │─────────────────────►│ │
1427
- │◄── callback_token │ │
1428
- │ │ │
1429
- │ 5. POST /api/native/exchange │
1430
- │───────────────────────────────────────────►│
1431
- │ │ decrypt via IAM │
1432
- │ │◄────────────────────│
1433
- │ │────────────────────►│
1434
- │◄──────────────── sanctum token + user ─────│
1435
- │ │ │
1436
- │ ✅ Connecté ! │ │
1437
- ```
1438
-
1439
- ### Détail des étapes :
1440
-
1441
- 1. **Config** — Le frontend récupère `encrypted_credentials` (blob opaque chiffré AES-256-CBC) depuis le backend SaaS. Les `app_key` et `secret_key` ne sont **jamais** exposées au frontend.
1442
- 2. **Encrypt** — Le frontend envoie le blob `encrypted_credentials` à l'IAM, qui le déchiffre côté serveur pour valider les credentials
1443
- 3. **Init** — L'IAM vérifie le compte et retourne le statut (`pending_password`, `pending_otp`, `needs_access`, etc.)
1444
- 4. **Validate** — Le frontend envoie le mot de passe/OTP, l'IAM retourne un `callback_token`
1445
- 5. **Exchange** — Le frontend envoie le `callback_token` au backend SaaS, qui le décrypte via l'IAM et crée une session Sanctum
1446
-
1447
- > **Important :** Le package gère les étapes 1-5 automatiquement. Le backend SaaS doit implémenter **5 endpoints** (`config`, `exchange`, `check-token`, `refresh`, `logout`).
1448
-
1449
- ---
1450
-
1451
- ## APIs IAM Account (Server-to-Server)
1452
-
1453
- > ⚠️ **ATTENTION** : Ces APIs utilisent la `secret_key` de votre application IAM. Elles doivent être appelées **exclusivement depuis votre backend** (server-to-server). Ne jamais exposer la `secret_key` côté frontend.
1454
-
1455
- Le package expose un service `iamAccountService` pour interagir avec les APIs IAM de gestion de compte :
1456
-
1457
- ```ts
1458
- import { iamAccountService } from '@ollaid/native-sso';
1459
- ```
1460
-
1461
- ---
1462
-
1463
- ### `POST /api/iam/link-phone`
1464
-
1465
- Associe un numéro de téléphone à un compte utilisateur existant dans l'IAM.
1466
-
1467
- **Cas d'usage :** Un utilisateur inscrit par email souhaite ajouter son numéro de téléphone, ou le SaaS collecte le numéro après inscription.
1468
-
1469
- **Paramètres requis :**
1470
-
1471
- ```json
1472
- {
1473
- "app_key": "oiam_ak_xxxxxxxxxxxxxxxxxxxxxxxx",
1474
- "secret_key": "oiam_sk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
1475
- "iam_reference": "USR-XXXXXXXX",
1476
- "ccphone": "+221",
1477
- "phone": "771234567"
1478
- }
1479
- ```
1480
-
1481
- | Champ | Type | Description |
1482
- |-------|------|-------------|
1483
- | `app_key` | `string` | Clé publique de l'application IAM |
1484
- | `secret_key` | `string` | Clé secrète de l'application IAM |
1485
- | `iam_reference` | `string` | Référence unique de l'utilisateur dans l'IAM (`USR-XXXXXXXX`) |
1486
- | `ccphone` | `string` | Indicatif téléphonique (ex: `+221`) |
1487
- | `phone` | `string` | Numéro de téléphone sans indicatif (ex: `771234567`) |
1488
-
1489
- **Réponse succès (200) :**
1490
-
1491
- ```json
1492
- {
1493
- "success": true,
1494
- "message": "Numéro de téléphone lié avec succès",
1495
- "user_reference": "USR-XXXXXXXX",
1496
- "user_infos": {
1497
- "name": "John Doe",
1498
- "email": "john@example.com",
1499
- "ccphone": "+221",
1500
- "phone": "771234567",
1501
- "address": null,
1502
- "town": "Dakar",
1503
- "country": "SN",
1504
- "image_url": "https://identityam.ollaid.com/storage/avatars/xxx.jpg",
1505
- "auth_2fa": false
1506
- }
1507
- }
1508
- ```
1509
-
1510
- **Codes d'erreur :**
1511
-
1512
- | Code HTTP | `error_type` | Description |
1513
- |-----------|-------------|-------------|
1514
- | 401 | `invalid_credentials` | `app_key` ou `secret_key` invalide |
1515
- | 404 | `user_not_found` | `iam_reference` introuvable |
1516
- | 409 | `phone_already_linked` | Ce numéro est déjà associé à un autre compte |
1517
- | 422 | `validation_error` | Champs manquants ou format invalide |
1518
-
1519
- **Exemple Node.js (backend) :**
1520
-
1521
- ```js
1522
- import { iamAccountService } from '@ollaid/native-sso';
1523
-
1524
- const result = await iamAccountService.linkPhone({
1525
- app_key: process.env.IAM_APP_KEY,
1526
- secret_key: process.env.IAM_SECRET_KEY,
1527
- iam_reference: 'USR-XXXXXXXX',
1528
- ccphone: '+221',
1529
- phone: '771234567',
1530
- });
1531
-
1532
- if (result.success) {
1533
- // Mettre à jour l'utilisateur local avec result.user_infos
1534
- console.log('Téléphone lié :', result.user_infos.phone);
1535
- }
1536
- ```
1537
-
1538
- ---
1539
-
1540
- ### `POST /api/iam/link-email`
1541
-
1542
- Associe une adresse email à un compte utilisateur existant dans l'IAM.
1543
-
1544
- **Cas d'usage :** Un utilisateur inscrit par téléphone (phone-only) souhaite ajouter son email.
1545
-
1546
- **Paramètres requis :**
1547
-
1548
- ```json
1549
- {
1550
- "app_key": "oiam_ak_xxxxxxxxxxxxxxxxxxxxxxxx",
1551
- "secret_key": "oiam_sk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
1552
- "iam_reference": "USR-XXXXXXXX",
1553
- "email": "john@example.com"
1554
- }
1555
- ```
1556
-
1557
- | Champ | Type | Description |
1558
- |-------|------|-------------|
1559
- | `app_key` | `string` | Clé publique de l'application IAM |
1560
- | `secret_key` | `string` | Clé secrète de l'application IAM |
1561
- | `iam_reference` | `string` | Référence unique de l'utilisateur dans l'IAM |
1562
- | `email` | `string` | Adresse email à associer |
1563
-
1564
- **Réponse succès (200) :**
1565
-
1566
- ```json
1567
- {
1568
- "success": true,
1569
- "message": "Adresse email liée avec succès",
1570
- "user_reference": "USR-XXXXXXXX",
1571
- "user_infos": {
1572
- "name": "John Doe",
1573
- "email": "john@example.com",
1574
- "ccphone": "+221",
1575
- "phone": "771234567",
1576
- "address": null,
1577
- "town": "Dakar",
1578
- "country": "SN",
1579
- "image_url": "https://identityam.ollaid.com/storage/avatars/xxx.jpg",
1580
- "auth_2fa": false
1581
- }
1582
- }
1583
- ```
1584
-
1585
- **Codes d'erreur :**
1586
-
1587
- | Code HTTP | `error_type` | Description |
1588
- |-----------|-------------|-------------|
1589
- | 401 | `invalid_credentials` | `app_key` ou `secret_key` invalide |
1590
- | 404 | `user_not_found` | `iam_reference` introuvable |
1591
- | 409 | `email_already_linked` | Cet email est déjà associé à un autre compte |
1592
- | 422 | `validation_error` | Champs manquants ou format invalide |
1593
-
1594
- **Exemple Node.js (backend) :**
1595
-
1596
- ```js
1597
- import { iamAccountService } from '@ollaid/native-sso';
1598
-
1599
- const result = await iamAccountService.linkEmail({
1600
- app_key: process.env.IAM_APP_KEY,
1601
- secret_key: process.env.IAM_SECRET_KEY,
1602
- iam_reference: 'USR-XXXXXXXX',
1603
- email: 'john@example.com',
1604
- });
1605
-
1606
- if (result.success) {
1607
- console.log('Email lié :', result.user_infos.email);
1608
- }
1609
- ```
1610
-
1611
- ---
1612
-
1613
- ### `POST /api/iam/refresh-user-info` (Single)
1614
-
1615
- Récupère les informations à jour d'un utilisateur depuis l'IAM.
1616
-
1617
- **Cas d'usage :** Synchroniser les données utilisateur (nom, avatar, etc.) après une modification côté IAM, ou récupérer les infos complètes pour un utilisateur inscrit par téléphone.
1618
-
1619
- **Paramètres requis :**
1620
-
1621
- ```json
1622
- {
1623
- "secret_key": "oiam_sk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
1624
- "alias_reference": "ALI-XXXXXXXX"
1625
- }
1626
- ```
1627
-
1628
- | Champ | Type | Description |
1629
- |-------|------|-------------|
1630
- | `secret_key` | `string` | Clé secrète de l'application IAM |
1631
- | `alias_reference` | `string` | Référence de l'alias utilisateur dans le contexte de votre app (`ALI-XXXXXXXX`) |
1632
-
1633
- **Réponse succès (200) :**
1634
-
1635
- ```json
1636
- {
1637
- "success": true,
1638
- "message": "Informations utilisateur récupérées",
1639
- "user_reference": "USR-XXXXXXXX",
1640
- "alias_reference": "ALI-XXXXXXXX",
1641
- "login_type": "email",
1642
- "user_infos": {
1643
- "name": "John Doe",
1644
- "email": "john@example.com",
1645
- "ccphone": "+221",
1646
- "phone": "771234567",
1647
- "address": null,
1648
- "town": "Dakar",
1649
- "country": "SN",
1650
- "image_url": "https://identityam.ollaid.com/storage/avatars/xxx.jpg",
1651
- "auth_2fa": false
1652
- }
1653
- }
1654
- ```
1655
-
1656
- **Codes d'erreur :**
1657
-
1658
- | Code HTTP | `error_type` | Description |
1659
- |-----------|-------------|-------------|
1660
- | 401 | `invalid_credentials` | `secret_key` invalide |
1661
- | 404 | `alias_not_found` | `alias_reference` introuvable |
1662
-
1663
- **Exemple Node.js :**
1664
-
1665
- ```js
1666
- import { iamAccountService } from '@ollaid/native-sso';
1667
-
1668
- const result = await iamAccountService.refreshUserInfo({
1669
- secret_key: process.env.IAM_SECRET_KEY,
1670
- alias_reference: 'ALI-XXXXXXXX',
1671
- });
1672
-
1673
- if (result.success) {
1674
- // Mettre à jour le profil local
1675
- await updateLocalUser(result.alias_reference, result.user_infos);
1676
- }
1677
- ```
1678
-
1679
- ---
1680
-
1681
- ### `POST /api/iam/refresh-user-info` (Bulk)
1682
-
1683
- Synchronise les informations de **plusieurs utilisateurs** en un seul appel (maximum **100 références** par requête).
1684
-
1685
- **Cas d'usage :** Synchronisation batch quotidienne, ou mise à jour groupée après un import.
1686
-
1687
- **Paramètres requis :**
1688
-
1689
- ```json
1690
- {
1691
- "secret_key": "oiam_sk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
1692
- "alias_references": [
1693
- "ALI-AAAAAAAA",
1694
- "ALI-BBBBBBBB",
1695
- "ALI-CCCCCCCC"
1696
- ]
1697
- }
1698
- ```
1699
-
1700
- | Champ | Type | Description |
1701
- |-------|------|-------------|
1702
- | `secret_key` | `string` | Clé secrète de l'application IAM |
1703
- | `alias_references` | `string[]` | Liste des alias à synchroniser (max 100) |
1704
-
1705
- **Réponse succès (200) :**
1706
-
1707
- ```json
1708
- {
1709
- "success": true,
1710
- "message": "Synchronisation terminée",
1711
- "total_requested": 3,
1712
- "total_found": 2,
1713
- "total_errors": 1,
1714
- "data": [
1715
- {
1716
- "user_reference": "USR-AAAAAAAA",
1717
- "alias_reference": "ALI-AAAAAAAA",
1718
- "login_type": "email",
1719
- "user_infos": { "name": "Alice", "email": "alice@example.com", "..." : "..." }
1720
- },
1721
- {
1722
- "user_reference": "USR-BBBBBBBB",
1723
- "alias_reference": "ALI-BBBBBBBB",
1724
- "login_type": "phone",
1725
- "user_infos": { "name": "Bob", "phone": "771234567", "..." : "..." }
1726
- }
1727
- ],
1728
- "errors": [
1729
- {
1730
- "alias_reference": "ALI-CCCCCCCC",
1731
- "error": "Alias introuvable"
1732
- }
1733
- ]
1734
- }
1735
- ```
1736
-
1737
- **Codes d'erreur :**
1738
-
1739
- | Code HTTP | `error_type` | Description |
1740
- |-----------|-------------|-------------|
1741
- | 401 | `invalid_credentials` | `secret_key` invalide |
1742
- | 422 | `too_many_references` | Plus de 100 références envoyées |
1743
- | 422 | `validation_error` | `alias_references` manquant ou invalide |
1744
-
1745
- **Exemple Node.js :**
1746
-
1747
- ```js
1748
- import { iamAccountService } from '@ollaid/native-sso';
1749
-
1750
- const result = await iamAccountService.refreshUserInfoBulk({
1751
- secret_key: process.env.IAM_SECRET_KEY,
1752
- alias_references: ['ALI-AAAAAAAA', 'ALI-BBBBBBBB', 'ALI-CCCCCCCC'],
1753
- });
1754
-
1755
- console.log(`Synchronisés: ${result.total_found}/${result.total_requested}`);
1756
-
1757
- // Traiter les succès
1758
- for (const item of result.data ?? []) {
1759
- await updateLocalUser(item.alias_reference, item.user_infos);
1760
- }
1761
-
1762
- // Logger les erreurs
1763
- for (const err of result.errors ?? []) {
1764
- console.warn(`Erreur pour ${err.alias_reference}: ${err.error}`);
1765
- }
1766
- ```
1767
-
1768
- ---
1769
-
1770
- ## Avatar par application
1771
-
1772
- Le système d'avatar permet à chaque utilisateur d'avoir une **image de profil différente par application**. Un champ `avatar` est ajouté sur la table `app_accesses`.
1773
-
1774
- ### Cascade de résolution `image_url`
1775
-
1776
- Dans toutes les réponses `user_infos`, le champ `image_url` est résolu selon cette priorité :
1777
-
1778
- 1. **`app_access.avatar`** — Avatar spécifique à l'application (si défini et valide)
1779
- 2. **`user.image`** — Image globale du profil IAM (si définie et valide)
1780
- 3. **`no-image.png`** — Fallback par défaut
1781
-
1782
- ### Migration requise
1783
-
1784
- ```sql
1785
- ALTER TABLE app_accesses ADD COLUMN avatar VARCHAR(255) NULL AFTER status;
1786
- ```
1787
-
1788
- Ou via Laravel :
1789
-
1790
- ```php
1791
- Schema::table('app_accesses', function (Blueprint $table) {
1792
- $table->string('avatar')->nullable()->after('status');
1793
- });
1794
- ```
1795
-
1796
- ### Pré-remplissage
1797
-
1798
- Lorsqu'un `AppAccess` est créé (inscription, grant-access), le champ `avatar` est automatiquement pré-rempli avec l'image globale du user (`user.image`).
1799
-
1800
- ### `POST /api/iam/update-avatar`
1801
-
1802
- Met à jour l'avatar d'un utilisateur **pour une application spécifique**.
1803
-
1804
- > ⚠️ **Server-to-Server uniquement** — Requiert `app_key` + `secret_key`.
1805
-
1806
- **Paramètres requis :**
1807
-
1808
- ```json
1809
- {
1810
- "app_key": "oiam_ak_xxxxxxxxxxxxxxxxxxxxxxxx",
1811
- "secret_key": "oiam_sk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
1812
- "alias_reference": "ALI-XXXXXXXX",
1813
- "avatar_url": "https://example.com/avatars/user123.jpg"
1814
- }
1815
- ```
1816
-
1817
- | Champ | Type | Description |
1818
- |-------|------|-------------|
1819
- | `app_key` | `string` | Clé publique de l'application IAM |
1820
- | `secret_key` | `string` | Clé secrète de l'application IAM |
1821
- | `alias_reference` | `string` | Référence de l'alias utilisateur |
1822
- | `avatar_url` | `string` | URL de la nouvelle image avatar |
1823
-
1824
- **Réponse succès (200) :**
1825
-
1826
- ```json
1827
- {
1828
- "success": true,
1829
- "message": "Avatar mis à jour",
1830
- "user_reference": "USR-XXXXXXXX",
1831
- "alias_reference": "ALI-XXXXXXXX",
1832
- "user_infos": {
1833
- "name": "John Doe",
1834
- "email": "john@example.com",
1835
- "ccphone": "+221",
1836
- "phone": "771234567",
1837
- "address": null,
1838
- "town": "Dakar",
1839
- "country": "SN",
1840
- "image_url": "https://example.com/avatars/user123.jpg",
1841
- "auth_2fa": false
1842
- }
1843
- }
1844
- ```
1845
-
1846
- **Codes d'erreur :**
1847
-
1848
- | Code HTTP | Description |
1849
- |-----------|-------------|
1850
- | 403 | `app_key` ou `secret_key` invalide |
1851
- | 404 | `alias_reference` introuvable ou pas d'accès à cette app |
1852
- | 422 | Champs manquants ou format invalide |
1853
-
1854
- **Exemple Node.js (backend) :**
1855
-
1856
- ```js
1857
- import { iamAccountService } from '@ollaid/native-sso';
1858
-
1859
- const result = await iamAccountService.updateAvatar({
1860
- app_key: process.env.IAM_APP_KEY,
1861
- secret_key: process.env.IAM_SECRET_KEY,
1862
- alias_reference: 'ALI-XXXXXXXX',
1863
- avatar_url: 'https://example.com/avatars/user123.jpg',
1864
- });
1865
-
1866
- if (result.success) {
1867
- console.log('Avatar mis à jour :', result.user_infos.image_url);
1868
- }
1869
- ```
1870
-
1871
- ### `POST /api/iam/reset-avatar`
1872
-
1873
- Réinitialise l'avatar d'un utilisateur pour une application, le remettant à `null`. La cascade retombera sur `user.image` ou `no-image.png`.
1874
-
1875
- > ⚠️ **Server-to-Server uniquement** — Requiert `app_key` + `secret_key`.
1876
-
1877
- **Paramètres requis :**
1878
-
1879
- ```json
1880
- {
1881
- "app_key": "oiam_ak_xxxxxxxxxxxxxxxxxxxxxxxx",
1882
- "secret_key": "oiam_sk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
1883
- "alias_reference": "ALI-XXXXXXXX"
1884
- }
1885
- ```
1886
-
1887
- | Champ | Type | Description |
1888
- |-------|------|-------------|
1889
- | `app_key` | `string` | Clé publique de l'application IAM |
1890
- | `secret_key` | `string` | Clé secrète de l'application IAM |
1891
- | `alias_reference` | `string` | Référence de l'alias utilisateur |
1892
-
1893
- **Réponse succès (200) :**
1894
-
1895
- ```json
1896
- {
1897
- "success": true,
1898
- "message": "Avatar réinitialisé",
1899
- "user_reference": "USR-XXXXXXXX",
1900
- "alias_reference": "ALI-XXXXXXXX",
1901
- "user_infos": {
1902
- "name": "John Doe",
1903
- "email": "john@example.com",
1904
- "image_url": "https://iam.example.com/storage/users/image.jpg"
1905
- }
1906
- }
1907
- ```
1908
-
1909
- **Codes d'erreur :**
1910
-
1911
- | Code HTTP | Description |
1912
- |-----------|-------------|
1913
- | 403 | `app_key` ou `secret_key` invalide |
1914
- | 404 | `alias_reference` introuvable ou pas d'accès à cette app |
1915
- | 422 | Champs manquants |
1916
-
1917
- **Exemple Node.js (backend) :**
1918
-
1919
- ```js
1920
- import { iamAccountService } from '@ollaid/native-sso';
1921
-
1922
- const result = await iamAccountService.resetAvatar({
1923
- app_key: process.env.IAM_APP_KEY,
1924
- secret_key: process.env.IAM_SECRET_KEY,
1925
- alias_reference: 'ALI-XXXXXXXX',
1926
- });
1927
-
1928
- if (result.success) {
1929
- console.log('Avatar réinitialisé, image actuelle :', result.user_infos.image_url);
1930
- }
1931
- ```
1932
-
1933
- ---
1934
-
1935
- ### Format `user_infos` (9 champs standardisés)
1936
-
1937
- Toutes les APIs IAM retournent le même objet `user_infos` avec exactement **9 champs** :
1938
-
1939
- | Champ | Type | Description |
1940
- |-------|------|-------------|
1941
- | `name` | `string` | Nom complet de l'utilisateur |
1942
- | `email` | `string \| null` | Adresse email (null si compte phone-only) |
1943
- | `ccphone` | `string \| null` | Indicatif téléphonique (ex: `+221`) |
1944
- | `phone` | `string \| null` | Numéro de téléphone sans indicatif |
1945
- | `address` | `string \| null` | Adresse postale |
1946
- | `town` | `string \| null` | Ville |
1947
- | `country` | `string \| null` | Code pays ISO (ex: `SN`, `FR`) |
1948
- | `image_url` | `string \| null` | URL de l'avatar (résolu via la cascade app_access → user → fallback) |
1949
- | `auth_2fa` | `boolean` | Indique si la 2FA est activée |
1950
-
1951
- > **Note :** Le champ `image_url` utilise désormais la cascade d'avatar. Il reflète l'avatar spécifique à l'application si défini, sinon l'image globale du user, sinon le fallback `no-image.png`.
1952
-
1953
- ---
1954
-
1955
- ## Session & localStorage
1956
-
1957
- Le package utilise **9 clés** dans `localStorage` pour persister la session et le suivi du profil :
1958
-
1959
- | Clé | Contenu | Source | Valeurs possibles |
1960
- |-----|---------|--------|-------------------|
1961
- | `token` | Token Sanctum (bearer) | Réponse de `/api/native/exchange` | Chaîne `"1\|abc123..."` |
1962
- | `auth_token` | Copie du token (compatibilité) | Même source que `token` | Idem |
1963
- | `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":"...",...}` |
1964
- | `account_type` | Type de compte | Déterminé lors du `exchange` selon le mode d'inscription | `"user"` (défaut) ou `"client"` (inscription phone-only) |
1965
- | `alias_reference` | Référence alias utilisée lors de la connexion | Réponse de `/api/native/exchange` (champ `user.alias_reference`) | `"ALI-XXXXXXXX"` |
1966
- | `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"` |
1967
- | `sso_image_last_status` | Statut du dernier onboarding profil | Mis à jour par `OnboardingModal` | `"true"` ou `"false"` |
1968
- | `sso_image_last_check` | Timestamp du dernier contrôle profil | Mis à jour par `OnboardingModal` / sync profil | Millisecondes Unix |
1969
- | `sso_image_recheck_at` | Timestamp de rappel après snooze | Mis à jour quand l'utilisateur passe l'onboarding | Millisecondes Unix |
1970
-
1971
- > **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`.
1972
- > **Note :** `sso_image_last_status`, `sso_image_last_check` et `sso_image_recheck_at` vivent dans le `localStorage` du SaaS et servent au rappel d'onboarding du profil.
1973
-
1974
- #### Champs enrichis dans l'objet `user`
1975
-
1976
- L'objet `user` stocké en localStorage contient deux champs ajoutés automatiquement par le package :
1977
- - `iam_reference` : la référence IAM de l'utilisateur (`USR-XXXXXXXX`), extraite de `user.reference`
1978
- - `alias_reference` : la référence de l'alias utilisé lors de la connexion (`ALI-XXXXXXXX`), extraite de `user.alias_reference`
1979
-
1980
- ### Nettoyage
1981
-
1982
- Lors du `logout()`, les 6 clés de session sont supprimées via `clearAuthToken()`.
1983
- Les clés de rappel profil (`sso_image_*`) ne sont pas effacées, afin de conserver le comportement de relance après snooze.
1984
-
1985
- ### Accès programmatique
1986
-
1987
- ```ts
1988
- import { getAuthToken, getAuthUser, getAccountType } from '@ollaid/native-sso';
1989
-
1990
- const token = getAuthToken(); // string | null
1991
- const user = getAuthUser<UserInfos>(); // UserInfos | null — contient iam_reference et alias_reference
1992
- const type = getAccountType(); // string | null
1993
- const aliasRef = localStorage.getItem('alias_reference'); // string | null
1994
- ```
1995
-
1996
- ---
1997
-
1998
- ## Déconnexion synchronisée
1999
-
2000
- 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.
2001
-
2002
- ### Architecture — Double Revocation
2003
-
2004
- ```
2005
- ┌─────────────────┐
2006
- │ Package │──────────────────────────────────┐
2007
- │ logout() │ │
2008
- └──────┬──────────┘ │
2009
- │ ① POST /native/logout │ ② POST /iam/disconnect
2010
- │ (Sanctum token) │ (sanctum_token + app_access_token_ref)
2011
- ▼ ▼
2012
- ┌─────────────────┐ ┌─────────────────┐
2013
- │ SaaS API │──③ fire-and-forget──────▶│ IAM API │
2014
- │ │ POST /iam/disconnect │ │
2015
- └─────────────────┘ └─────────────────┘
2016
- ```
2017
-
2018
- **Trois niveaux de sécurité :**
2019
-
2020
- | Scénario | Résultat |
2021
- |----------|----------|
2022
- | ✅ Tout fonctionne | SaaS + IAM révoquent tous les deux |
2023
- | ⚠️ SaaS injoignable | L'IAM révoque quand même via l'appel direct ② |
2024
- | ⚠️ IAM injoignable | Le SaaS révoque via ① puis retente via ③ |
2025
- | ❌ Les deux injoignables | localStorage nettoyé, le health check détectera le 401 plus tard |
2026
-
2027
- ### Comportement automatique
2028
-
2029
- Quand `useNativeAuth.logout()` est appelé (ou que l'utilisateur clique "Se déconnecter" dans `NativeSSOPage`) :
2030
-
2031
- 1. **Appels parallèles** (via `Promise.allSettled`, jamais bloquants) :
2032
- - `POST /api/native/logout` → SaaS supprime le Sanctum token + notifie l'IAM (fire-and-forget)
2033
- - `POST /api/iam/disconnect` → IAM révoque directement l'`AppAccessToken` via `sanctum_token` + `app_access_token_ref` (lookup optimisé)
2034
- 2. **Nettoyage local garanti** : les 6 clés localStorage sont supprimées (`token`, `auth_token`, `user`, `account_type`, `alias_reference`, `app_access_token_ref`)
2035
- 3. **Aucun blocage** : même si les deux appels échouent (offline, timeout), la déconnexion locale est instantanée
2036
-
2037
- > **Fiabilité** : `Promise.allSettled` + `.catch()` sur chaque appel. L'appel IAM a un timeout court (5s) pour ne jamais ralentir l'UX.
2038
-
2039
- ### API IAM de revocation — `POST /api/iam/disconnect`
2040
-
2041
- Le package contacte directement l'IAM pour révoquer l'`AppAccessToken` lié à la session.
2042
-
2043
- | Paramètre | Type | Description |
2044
- |-----------|------|-------------|
2045
- | `sanctum_token` | `string` | Le token Sanctum stocké localement, utilisé pour identifier l'`AppAccessToken` à révoquer |
2046
- | `app_access_token_ref` | `string` | (recommandé) Référence directe de l'`AppAccessToken` IAM — lookup instantané par PK |
2047
-
2048
- 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.
2049
-
2050
- ### Déconnexion externe (hors package)
2051
-
2052
- Si votre application gère une déconnexion **en dehors** de `NativeSSOPage` (ex : backoffice, bouton custom) :
2053
-
2054
- > ⚠️ **OBLIGATOIRE** : utilisez `logout()` — jamais `clearAuthToken()` seul.
2055
-
2056
- ```ts
2057
- import { logout } from '@ollaid/native-sso';
2058
-
2059
- // Double revocation complète (SaaS + IAM) + nettoyage localStorage
2060
- await logout();
2061
-
2062
- // Rediriger vers la page de connexion
2063
- navigate('/auth/login');
2064
- ```
2065
-
2066
- `logout()` effectue automatiquement :
2067
- 1. `POST /api/native/logout` → révoque le Sanctum token
2068
- 2. `POST /api/iam/disconnect` → révoque l'AppAccessToken IAM (avec `sanctum_token` + `app_access_token_ref`)
2069
- 3. `clearAuthToken()` → nettoie les 6 clés localStorage
2070
-
2071
- ### Hook `useLogout()` (recommandé pour React)
2072
-
2073
- Pour une intégration React avec gestion d'état (loading, error) et callbacks :
2074
-
2075
- ```tsx
2076
- import { useLogout } from '@ollaid/native-sso';
2077
- import { useNavigate } from 'react-router-dom';
2078
-
2079
- const LogoutButton = () => {
2080
- const navigate = useNavigate();
2081
- const { logout, loading, error } = useLogout({
2082
- onSuccess: () => navigate('/auth/login'),
2083
- onError: (err) => console.error('Logout failed:', err.message),
2084
- });
2085
-
2086
- return (
2087
- <>
2088
- <button onClick={logout} disabled={loading}>
2089
- {loading ? 'Déconnexion...' : 'Se déconnecter'}
2090
- </button>
2091
- {error && <p className="text-red-500">{error}</p>}
2092
- </>
2093
- );
2094
- };
2095
- ```
2096
-
2097
- | Propriété | Type | Description |
2098
- |-----------|------|-------------|
2099
- | **Options** | | |
2100
- | `onSuccess` | `() => void` | Appelé après déconnexion réussie (redirection, toast, etc.) |
2101
- | `onError` | `(error: Error) => void` | Appelé en cas d'échec |
2102
- | **Retour** | | |
2103
- | `logout` | `() => Promise<void>` | Déclenche la double revocation complète |
2104
- | `loading` | `boolean` | `true` pendant l'appel |
2105
- | `error` | `string \| null` | Message d'erreur ou `null` |
2106
-
2107
- ### Détection automatique des sessions révoquées
2108
-
2109
- 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.
2110
-
2111
- > **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.
2112
-
2113
- ### Prérequis backend
2114
-
2115
- 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))
2116
- 2. L'IAM **doit** exposer `POST /api/iam/disconnect` acceptant `{ sanctum_token, app_access_token_ref }` pour la revocation directe par le package
2117
-
2118
- ---
2119
-
2120
- Modal post-connexion qui invite l'utilisateur à compléter les informations manquantes de son profil, ou à éditer ses informations complètes depuis un SaaS.
2121
-
2122
- ### Props
2123
-
2124
- | Prop | Type | Requis | Description |
2125
- |------|------|--------|-------------|
2126
- | `open` | `boolean` | ✅ | Contrôle l'ouverture de la modal |
2127
- | `onOpenChange` | `(open: boolean) => void` | ✅ | Callback changement d'état |
2128
- | `user` | `NativeUser` | ✅ | Objet utilisateur courant (pour détecter les champs manquants) |
2129
- | `variant` | `'missing' \| 'edit'` | ❌ | `missing` affiche uniquement les champs absents, `edit` affiche les champs de base en mode édition complet |
2130
- | `onComplete` | `(data) => void` | ✅ | Callback avec les données saisies |
2131
- | `onSkip` | `() => void` | ✅ | Callback si l'utilisateur passe l'étape |
2132
-
2133
- ### Champs affichés
2134
-
2135
- La modal gère **deux modes** :
2136
-
2137
- - **Mode `missing`** : affiche uniquement les champs manquants
2138
- - **Nom complet** — si `user.name` est vide
2139
- - **Photo de profil** — si `user.image_url` est vide (max 2 Mo, JPG/PNG)
2140
- - **Numéro de téléphone** — si `user.phone` est vide
2141
- - **Adresse email** — si `user.email` est vide
2142
- - **Mode `edit`** : affiche les champs de base éditables même si le profil est déjà complet
2143
- - Nom complet
2144
- - Photo de profil
2145
- - Numéro de téléphone en lecture seule avec bouton `Changer téléphone`
2146
- - Adresse email en lecture seule avec bouton `Changer email`
2147
-
2148
- ### Callback `onComplete`
2149
-
2150
- ```ts
2151
- onComplete: (data: {
2152
- name?: string; // Nom complet (si fourni ou modifié)
2153
- image_url?: string; // Base64 de la photo (si ajoutée)
2154
- ccphone?: string; // Indicatif (si téléphone ajouté)
2155
- phone?: string; // Numéro (si téléphone ajouté)
2156
- email?: string; // Email (si renseigné)
2157
- }) => void;
2158
- ```
2159
-
2160
- ### Condition de soumission
2161
-
2162
- L'utilisateur **doit** :
2163
- 1. Fournir un nom si le nom est affiché
2164
- 2. Fournir une photo si elle est affichée
2165
- 3. Fournir un téléphone valide si le champ est affiché
2166
- 4. Fournir un email valide si le champ est affiché
2167
-
2168
- Après connexion, la page autonome attend 5 minutes avant d'ouvrir cette modal si le profil est incomplet.
2169
- Si l'utilisateur la ferme sans compléter, le package enregistre un snooze de 8 heures (`sso_image_last_status=false`, `sso_image_last_check=now`, `sso_image_recheck_at=now+8h`).
2170
- Un sync profil best-effort relit ensuite l'état utilisateur toutes les 30 minutes et remet `sso_image_last_status=true` dès que le profil est complet.
2171
-
2172
- ### Mode automatique
2173
-
2174
- Le mode automatique est piloté par `NativeSSOPage` :
2175
- - la modal s'ouvre après le délai de 5 minutes si des infos sont manquantes,
2176
- - elle reste sur un écran de chargement tant que l'hydratation IAM n'a pas renvoyé le profil,
2177
- - elle affiche ensuite uniquement les champs manquants,
2178
- - si l'utilisateur ferme la modal, le rappel est repoussé de 8 heures.
2179
-
2180
- ### Mode manuel depuis un SaaS
2181
-
2182
- Quand vous ouvrez la modal depuis votre SaaS via un bouton "Infos profil" ou "Modifier mon profil", utilisez `variant="edit"`.
2183
-
2184
- Exemple :
2185
-
2186
- ```tsx
2187
- import { useState } from 'react';
2188
- import { OnboardingModal } from '@ollaid/native-sso';
2189
-
2190
- export function ProfileButton({ user }: { user: NativeUser }) {
2191
- const [open, setOpen] = useState(false);
2192
-
2193
- return (
2194
- <>
2195
- <button type="button" onClick={() => setOpen(true)}>
2196
- Infos profil
2197
- </button>
2198
-
2199
- <OnboardingModal
2200
- open={open}
2201
- onOpenChange={setOpen}
2202
- onDismiss={() => setOpen(false)}
2203
- user={user}
2204
- variant="edit"
2205
- profileHydrating={false}
2206
- onComplete={(data) => {
2207
- // Mettre à jour votre cache local / localStorage avec data.user_infos
2208
- setOpen(false);
2209
- }}
2210
- onSkip={() => setOpen(false)}
2211
- />
2212
- </>
2213
- );
2214
- }
2215
- ```
2216
-
2217
- Dans ce mode, le changement de téléphone/email ouvre un sous-flux OTP dans la même modal:
2218
- 1. saisie du nouveau téléphone/email,
2219
- 2. envoi du premier OTP sur le contact actuel,
2220
- 3. vérification du premier OTP,
2221
- 4. envoi d'un second OTP sur le nouveau contact,
2222
- 5. confirmation finale et mise à jour du profil.
2223
-
2224
- Les écrans de changement restent en `static backdrop` côté package: un clic hors modal ne ferme pas le flux.
2225
-
2226
- ### Exemple
2227
-
2228
- ```tsx
2229
- import { OnboardingModal } from '@ollaid/native-sso';
2230
-
2231
- <OnboardingModal
2232
- open={showOnboarding}
2233
- onOpenChange={setShowOnboarding}
2234
- user={currentUser}
2235
- variant="edit"
2236
- onComplete={async (data) => {
2237
- // Envoyer les données au backend pour mise à jour
2238
- await api.updateProfile(data);
2239
- setShowOnboarding(false);
2240
- }}
2241
- onSkip={() => setShowOnboarding(false)}
2242
- />
2243
- ```
2244
-
2245
- ---
2246
-
2247
- ## useTokenHealthCheck
2248
-
2249
- Hook qui vérifie périodiquement la validité du token Sanctum via `POST /api/native/check-token` du SaaS.
2250
-
2251
- **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)).
2252
-
2253
- ### Timing
2254
-
2255
- | Événement | Délai |
2256
- |-----------|-------|
2257
- | Premier check après login | **60 secondes** |
2258
- | Checks suivants | **toutes les 2 minutes** |
2259
- | Requêtes par heure | ~30 |
2260
-
2261
- ### Comportement réseau
2262
-
2263
- | Situation | Action |
2264
- |-----------|--------|
2265
- | ✅ 200 `status: 'connected'` | Met à jour `user_infos` en localStorage |
2266
- | ❌ 401 `status: 'disconnected'` | Révoque l'IAM + déconnecte le frontend |
2267
- | ⚠️ Erreur réseau / timeout / 500 | **Aucune action** — la session est conservée |
2268
- | 📴 Appareil hors ligne | Le check échoue silencieusement, session conservée |
2269
-
2270
- ### Revocation automatique sur 401
2271
-
2272
- Quand le SaaS retourne un 401 (token révoqué par un admin, session expirée, etc.) :
2273
-
2274
- 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
2275
- 2. **Nettoyage localStorage** — Les 6 clés de session sont supprimées
2276
- 3. **Réinitialisation de l'état** — L'interface revient à l'écran de connexion
2277
-
2278
- > **Philosophie** : Ne déconnecter que sur un rejet explicite du serveur (401). Jamais sur un problème réseau.
2279
-
2280
- ### Format de réponse attendu du SaaS
2281
-
2282
- ```json
2283
- // Connecté (200)
2284
- {
2285
- "status": "connected",
2286
- "user": { "name": "...", "email": "...", "avatar": "..." }
2287
- }
2288
-
2289
- // Déconnecté (401)
2290
- {
2291
- "status": "disconnected",
2292
- "message": "Utilisateur non connecté"
2293
- }
2294
- ```
2295
-
2296
- > Voir [BACKEND_INTEGRATION.md — Section 2.3](./BACKEND_INTEGRATION.md) pour l'implémentation PHP complète.
2297
-
2298
- ---
2299
-
2300
- ## Sécurité
2301
-
2302
- ### Credentials protégés par chiffrement — Opaque Token
2303
-
2304
- L'architecture utilise un **token opaque** (`encrypted_credentials`) pour protéger les clés API :
2305
-
2306
- 1. Le backend SaaS chiffre `app_key + secret_key + timestamp` en AES-256-CBC avec `SHA-256(secret_key)` comme clé
2307
- 2. Le frontend reçoit un **blob opaque** + l'`app_key` en clair (non sensible)
2308
- 3. Le frontend transporte le blob + `app_key` vers l'IAM
2309
- 4. L'IAM retrouve la `secret_key` via l'`app_key` dans sa table `applications`, déchiffre le blob et valide
2310
-
2311
- **Les `secret_key` ne sont JAMAIS visibles** dans les DevTools (onglet Network). Seuls le blob chiffré et l'`app_key` apparaissent. Le blob est inutilisable sans la `secret_key` correspondante.
2312
-
2313
- ### APIs S2S — Backend uniquement
2314
-
2315
- Le service `iamAccountService` (link-phone, link-email, refresh-user-info, update-avatar, reset-avatar) utilise la `secret_key` pour des opérations de gestion de compte.
2316
-
2317
- > ⚠️ **Ces APIs ne doivent JAMAIS être appelées depuis le frontend.** Utilisez-les uniquement depuis votre backend Node.js, PHP (Laravel), ou tout autre serveur.
2318
-
2319
- ```ts
2320
- // ✅ CORRECT — dans un contrôleur Node.js / route API backend
2321
- import { iamAccountService } from '@ollaid/native-sso';
2322
- await iamAccountService.linkPhone({ ... });
2323
-
2324
- // ❌ INTERDIT — dans un composant React ou du code frontend
2325
- // La secret_key serait visible dans le navigateur
2326
- ```
2327
-
2328
- ### Champs de debug en production
2329
-
2330
- Les types `NativeExchangeResponse` incluent `debug_error` et `debug_file` optionnels. Ces champs sont utiles en développement mais **ne doivent jamais être retournés en production** par le backend SaaS, car ils exposent des chemins serveur et des messages d'erreur internes.
2331
-
2332
- ```php
2333
- // ✅ Laravel — ne retourner les champs debug que si IAM_DEBUG=true
2334
- if (config('services.iam.debug')) {
2335
- $response['debug_error'] = $exception->getMessage();
2336
- $response['debug_file'] = $exception->getFile();
2337
- }
2338
- // ⚠️ Si IAM_DEBUG est absent/null → considéré comme false (production)
2339
- ```
2340
-
2341
- ### Bonnes pratiques
2342
-
2343
- - ✅ Le token Sanctum est stocké en `localStorage` (standard pour SPA)
2344
- - ✅ Les credentials IAM sont gardés **en mémoire uniquement** (jamais persistés)
2345
- - ✅ Le device ID est un identifiant aléatoire (pas de fingerprinting invasif)
2346
- - ✅ Le logout est single-session (ne déconnecte pas les autres appareils)
2347
- - ✅ Le health check ne déconnecte que sur 401 explicite
2348
- - ✅ Les photos sont validées à 2 Mo max côté client
2349
- - ✅ Les lectures `localStorage` sont protégées par try/catch
2350
-
2351
- ---
2352
-
2353
- ## Exports
2354
-
2355
- ### Composants
2356
- - `NativeSSOPage` — Page complète autonome (recommandé)
2357
- - `LoginModal` — Modal de connexion
2358
- - `SignupModal` — Modal d'inscription
2359
- - `PasswordRecoveryModal` — Modal de récupération
2360
- - `OTPInput` — Input OTP 6 chiffres
2361
- - `PhoneInput` — Input téléphone avec indicatif
2362
- - `AppsLogoSlider` — Slider logos applications
2363
-
2364
- ### Hooks
2365
- - `useNativeAuth` — Hook principal d'authentification
2366
- - `useMobilePassword` — Hook récupération mot de passe
2367
- - `useMobileRegistration` — Hook inscription
2368
- - `useTokenHealthCheck` — Hook vérification périodique du token
2369
- - `useLogout` — Hook déconnexion sécurisée avec callbacks
2370
-
2371
- ### Provider
2372
- - `NativeSSOProvider` — Provider React pour configuration centralisée
2373
- - `useNativeSSOConfig` — Hook pour accéder à la config
2374
-
2375
- ### Services
2376
- - `nativeAuthService` — Service d'authentification
2377
- - `mobilePasswordService` — Service mot de passe
2378
- - `setNativeAuthConfig` — Configuration manuelle des URLs
2379
- - `iamAccountService` — Service APIs IAM Account (link-phone, link-email, refresh-user-info, update-avatar, reset-avatar)
2380
- - `logout` — Déconnexion complète (double révocation SaaS + IAM + nettoyage localStorage)
2381
- - `getAuthToken` — Récupérer le token depuis localStorage
2382
- - `getAuthUser` — Récupérer l'utilisateur depuis localStorage
2383
- - `getAccountType` — Récupérer le type de compte depuis localStorage
2384
- - `getDeviceId` — Récupérer/générer le `X-Device-Id` (persisté)
2385
- - `getSessionUuid` — Récupérer/générer le `X-Session-UUID` (persisté)
2386
- - `STORAGE_KEYS` — Constantes des clés localStorage utilisées par le package
2387
-
2388
- ### Types
2389
- - `UserInfos`, `NativeAuthState`, `NativeAuthStatus`, `NativeCredentials`, etc.
2390
- - `AccountType` — `'email' | 'phone-only'`
2391
- - `MobilePasswordState`, `MobilePasswordStatus` — Types pour la récupération de mot de passe
2392
- - `MobileRegistrationFormData` — Données du formulaire d'inscription
2393
- - `RegistrationConflict` — Objet de conflit d'inscription (type interne, voir [Gestion des conflits](#gestion-des-conflits-dinscription))
2394
- - `LinkPhoneRequest`, `LinkPhoneResponse` — Types pour l'API link-phone
2395
- - `LinkEmailRequest`, `LinkEmailResponse` — Types pour l'API link-email
2396
- - `RefreshUserInfoSingleRequest`, `RefreshUserInfoSingleResponse` — Types pour refresh single
2397
- - `RefreshUserInfoBulkRequest`, `RefreshUserInfoBulkResponse` — Types pour refresh bulk
2398
- - `UpdateAvatarRequest`, `UpdateAvatarResponse` — Types pour l'API update-avatar
2399
- - `ResetAvatarRequest`, `ResetAvatarResponse` — Types pour l'API reset-avatar
2400
-
2401
- ---
2402
-
2403
- ## Publication & Installation npm
2404
-
2405
- ### Prérequis
2406
-
2407
- - **Node.js** ≥ 18 et **npm** ≥ 9
2408
- - Un compte [npmjs.com](https://www.npmjs.com/) connecté : `npm login`
2409
- - Être membre de l'organisation **@ollaid** sur npm
2410
-
2411
- ### Build
2412
-
2413
- ```bash
2414
- cd packages/ollaid-native-sso
2415
- npm run build
2416
- ```
2417
-
2418
- ### Versioning
2419
-
2420
- Avant chaque publication, incrémenter la version selon [semver](https://semver.org/) :
2421
-
2422
- ```bash
2423
- # Correction de bug (1.0.0 → 1.0.1)
2424
- npm version patch
2425
-
2426
- # Nouvelle fonctionnalité rétro-compatible (1.0.1 → 1.1.0)
2427
- npm version minor
2428
-
2429
- # Breaking change (1.1.0 → 2.0.0)
2430
- npm version major
2431
- ```
2432
-
2433
- ### Publication
2434
-
2435
- Les scoped packages (`@ollaid/*`) sont **privés par défaut** sur npm.
2436
- Le flag `--access public` est **obligatoire** pour publier en accès libre :
2437
-
2438
- ```bash
2439
- npm publish --access public
2440
- ```
2441
-
2442
- > **Astuce** : Pour ne plus avoir à passer le flag à chaque fois, ajoutez dans le `package.json` du package :
2443
- > ```json
2444
- > "publishConfig": {
2445
- > "access": "public"
2446
- > }
2447
- > ```
2448
-
2449
- ### Installation côté client
2450
-
2451
- Dans le projet qui consomme le package :
2452
-
2453
- ```bash
2454
- npm install @ollaid/native-sso
2455
- ```
2456
-
2457
- ### Vérification
2458
-
2459
- ```bash
2460
- # Voir les infos du package publié
2461
- npm info @ollaid/native-sso
2462
-
2463
- # Voir toutes les versions publiées
2464
- npm info @ollaid/native-sso versions
2465
- ```
2466
-
2467
- ### Mise à jour
2468
-
2469
- ```bash
2470
- # Mettre à jour vers la dernière version
2471
- npm update @ollaid/native-sso
2472
-
2473
- # Ou forcer une version spécifique
2474
- npm install @ollaid/native-sso@1.0.0
2475
- ```
2476
-
2477
- ### Workflow complet de publication
2478
-
2479
- ```bash
2480
- cd packages/ollaid-native-sso
2481
- npm run build # 1. Build
2482
- npm version patch # 2. Incrémenter la version
2483
- npm publish --access public # 3. Publier sur npm
2484
- ```
2485
-
2486
- ---
2487
-
2488
- ## Licence
2489
-
2490
- Propriétaire — Ollaid © 2026
2491
-
2492
- ---
2493
-
2494
- ## Webhooks & Health Check (Backend)
2495
-
2496
- Pour garantir la fiabilité et la synchronisation en temps réel (ex: révocation instantanée d'une session bannie), votre backend doit implémenter deux briques supplémentaires.
2497
-
2498
- Consultez le guide détaillé : [**WEBHOOKS_HEALTH.md**](./WEBHOOKS_HEALTH.md)
2499
-
2500
- | Brique | Rôle |
2501
- |--------|------|
2502
- | **Health Check** | Permet à l'IAM de vérifier que votre SaaS est "Healthy" et bien configuré. |
2503
- | **Webhooks** | Permet à l'IAM de notifier votre SaaS d'événements critiques (suspension, révocation). |