@ollaid/native-sso 2.6.0 → 2.7.1

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