@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/dist/components/AvatarCropModal.d.ts +1 -1
- package/dist/components/DebugPanel.d.ts +1 -1
- package/dist/components/LoginModal.d.ts +1 -1
- package/dist/components/NativeSSOPage.d.ts +1 -1
- package/dist/components/OnboardingModal.d.ts +1 -1
- package/dist/components/PasswordRecoveryModal.d.ts +1 -1
- package/dist/components/SignupModal.d.ts +1 -1
- package/dist/components/ui.d.ts +1 -1
- package/dist/hooks/useLogout.d.ts +1 -1
- package/dist/hooks/useMobilePassword.d.ts +1 -1
- package/dist/hooks/useMobileRegistration.d.ts +1 -1
- package/dist/hooks/useNativeAuth.d.ts +1 -1
- package/dist/hooks/useTokenHealthCheck.d.ts +1 -1
- package/dist/index.cjs +84 -52
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +84 -52
- package/dist/index.js.map +1 -1
- package/dist/provider.d.ts +1 -1
- package/dist/services/api.d.ts +1 -1
- package/dist/services/debugLogger.d.ts +1 -1
- package/dist/services/iamAccount.d.ts +1 -1
- package/dist/services/mobilePassword.d.ts +1 -1
- package/dist/services/mobileRegistration.d.ts +1 -1
- package/dist/services/nativeAuth.d.ts +1 -1
- package/dist/services/profile.d.ts +1 -1
- package/dist/services/profileChange.d.ts +1 -1
- package/dist/services/profileMedia.d.ts +1 -1
- package/dist/types/mobile.d.ts +1 -1
- package/dist/types/native.d.ts +1 -1
- package/package.json +1 -1
- package/README.md +0 -2689
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). |
|