@ollaid/native-sso 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1682 -0
- package/dist/components/AppsLogoSlider.d.ts +9 -0
- package/dist/components/DebugPanel.d.ts +12 -0
- package/dist/components/LoginModal.d.ts +20 -0
- package/dist/components/NativeSSOPage.d.ts +35 -0
- package/dist/components/OTPInput.d.ts +13 -0
- package/dist/components/OnboardingModal.d.ts +23 -0
- package/dist/components/PasswordRecoveryModal.d.ts +17 -0
- package/dist/components/PhoneInput.d.ts +15 -0
- package/dist/components/SignupModal.d.ts +18 -0
- package/dist/components/ui.d.ts +70 -0
- package/dist/hooks/useMobilePassword.d.ts +94 -0
- package/dist/hooks/useMobileRegistration.d.ts +148 -0
- package/dist/hooks/useNativeAuth.d.ts +262 -0
- package/dist/hooks/useTokenHealthCheck.d.ts +25 -0
- package/dist/index.cjs +4493 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +40 -0
- package/dist/index.js +4493 -0
- package/dist/index.js.map +1 -0
- package/dist/provider.d.ts +38 -0
- package/dist/services/api.d.ts +49 -0
- package/dist/services/debugLogger.d.ts +23 -0
- package/dist/services/iamAccount.d.ts +45 -0
- package/dist/services/mobilePassword.d.ts +19 -0
- package/dist/services/mobileRegistration.d.ts +31 -0
- package/dist/services/nativeAuth.d.ts +55 -0
- package/dist/types/mobile.d.ts +128 -0
- package/dist/types/native.d.ts +289 -0
- package/package.json +50 -0
package/README.md
ADDED
|
@@ -0,0 +1,1682 @@
|
|
|
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. [Backend SaaS — Endpoints requis](#backend-saas--endpoints-requis)
|
|
15
|
+
6. [APIs IAM Account (Server-to-Server)](#apis-iam-account-server-to-server)
|
|
16
|
+
7. [Avatar par application](#avatar-par-application)
|
|
17
|
+
8. [Réponses d'erreur](#réponses-derreur)
|
|
18
|
+
9. [Configuration .env Laravel](#configuration-env-laravel)
|
|
19
|
+
10. [Migration Laravel](#migration-laravel)
|
|
20
|
+
11. [Flux d'authentification](#flux-dauthentification)
|
|
21
|
+
12. [Session & localStorage](#session--localstorage)
|
|
22
|
+
13. [OnboardingModal](#onboardingmodal)
|
|
23
|
+
14. [useTokenHealthCheck](#usetokenhealthcheck)
|
|
24
|
+
15. [Sécurité](#sécurité)
|
|
25
|
+
16. [Exports](#exports)
|
|
26
|
+
17. [Publication & Installation npm](#publication--installation-npm)
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Installation
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npm install @ollaid/native-sso
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Intégration rapide (3 étapes)
|
|
39
|
+
|
|
40
|
+
### 1. Installer le package
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
npm install @ollaid/native-sso
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### 2. Ajouter la route dans `App.tsx`
|
|
47
|
+
|
|
48
|
+
```tsx
|
|
49
|
+
import { NativeSSOPage } from '@ollaid/native-sso';
|
|
50
|
+
import { Route, Routes, useNavigate } from 'react-router-dom';
|
|
51
|
+
|
|
52
|
+
function App() {
|
|
53
|
+
const navigate = useNavigate();
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<Routes>
|
|
57
|
+
<Route
|
|
58
|
+
path="/auth/sso"
|
|
59
|
+
element={
|
|
60
|
+
<NativeSSOPage
|
|
61
|
+
saasApiUrl="https://mon-saas.com/api"
|
|
62
|
+
iamApiUrl="https://identityam.ollaid.com/api"
|
|
63
|
+
onLoginSuccess={(token, user) => {
|
|
64
|
+
console.log('Connecté !', user.name);
|
|
65
|
+
navigate('/dashboard');
|
|
66
|
+
}}
|
|
67
|
+
onLogout={() => navigate('/auth/sso')}
|
|
68
|
+
/>
|
|
69
|
+
}
|
|
70
|
+
/>
|
|
71
|
+
{/* ... autres routes */}
|
|
72
|
+
</Routes>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### 3. C'est tout ✅
|
|
78
|
+
|
|
79
|
+
La page `/auth/sso` gère automatiquement :
|
|
80
|
+
- ✅ Connexion par email (mot de passe + OTP)
|
|
81
|
+
- ✅ Connexion par téléphone (SMS OTP)
|
|
82
|
+
- ✅ Connexion par code d'accès
|
|
83
|
+
- ✅ Inscription complète (email ou téléphone uniquement 🇸🇳)
|
|
84
|
+
- ✅ Récupération de mot de passe
|
|
85
|
+
- ✅ Grant access (inscription auto à une nouvelle app)
|
|
86
|
+
- ✅ 2FA (TOTP)
|
|
87
|
+
- ✅ Session persistée en localStorage
|
|
88
|
+
- ✅ Branding Ollaid SSO
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## Props de `NativeSSOPage`
|
|
93
|
+
|
|
94
|
+
| Prop | Type | Requis | Description |
|
|
95
|
+
|------|------|--------|-------------|
|
|
96
|
+
| `saasApiUrl` | `string` | ✅ | URL du backend SaaS (ex: `https://mon-saas.com/api`) |
|
|
97
|
+
| `iamApiUrl` | `string` | ✅ | URL du backend IAM (ex: `https://identityam.ollaid.com/api`) |
|
|
98
|
+
| `onLoginSuccess` | `(token: string, user: UserInfos) => void` | ❌ | Callback après connexion réussie |
|
|
99
|
+
| `onLogout` | `() => void` | ❌ | Callback après déconnexion |
|
|
100
|
+
| `debug` | `boolean` | ❌ | Active les logs console |
|
|
101
|
+
| `title` | `string` | ❌ | Titre personnalisé (défaut: "Un compte, plusieurs accès") |
|
|
102
|
+
| `description` | `string` | ❌ | Description personnalisée |
|
|
103
|
+
| `logoUrl` | `string` | ❌ | URL du logo (remplace le slider) |
|
|
104
|
+
| `hideFooter` | `boolean` | ❌ | Masquer "Propulsé par iam.ollaid.com" |
|
|
105
|
+
| `onOnboardingComplete` | `(data: { image_url?: string; ccphone?: string; phone?: string }) => void` | ❌ | Callback après complétion de l'onboarding |
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## Usage avancé (composants individuels)
|
|
110
|
+
|
|
111
|
+
Pour ceux qui veulent plus de contrôle :
|
|
112
|
+
|
|
113
|
+
```tsx
|
|
114
|
+
import {
|
|
115
|
+
NativeSSOProvider,
|
|
116
|
+
LoginModal,
|
|
117
|
+
SignupModal,
|
|
118
|
+
useNativeAuth,
|
|
119
|
+
} from '@ollaid/native-sso';
|
|
120
|
+
|
|
121
|
+
function MyCustomAuth() {
|
|
122
|
+
const [showLogin, setShowLogin] = useState(false);
|
|
123
|
+
|
|
124
|
+
return (
|
|
125
|
+
<>
|
|
126
|
+
<button onClick={() => setShowLogin(true)}>Se connecter</button>
|
|
127
|
+
|
|
128
|
+
<LoginModal
|
|
129
|
+
open={showLogin}
|
|
130
|
+
onOpenChange={setShowLogin}
|
|
131
|
+
onSwitchToSignup={() => {}}
|
|
132
|
+
onLoginSuccess={(token, user) => console.log('OK', user)}
|
|
133
|
+
saasApiUrl="https://mon-saas.com/api"
|
|
134
|
+
iamApiUrl="https://identityam.ollaid.com/api"
|
|
135
|
+
/>
|
|
136
|
+
</>
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## Backend SaaS — Endpoints requis
|
|
144
|
+
|
|
145
|
+
Le backend SaaS (Laravel) doit exposer **4 endpoints**. Voici les spécifications exactes :
|
|
146
|
+
|
|
147
|
+
### `GET /api/native/config`
|
|
148
|
+
|
|
149
|
+
Retourne un **token opaque chiffré** (`encrypted_credentials`) contenant les credentials IAM.
|
|
150
|
+
⚠️ **Les clés `app_key` et `secret_key` ne sont JAMAIS exposées au frontend.**
|
|
151
|
+
|
|
152
|
+
**Headers requis :** aucun
|
|
153
|
+
|
|
154
|
+
**Réponse succès (200) :**
|
|
155
|
+
```json
|
|
156
|
+
{
|
|
157
|
+
"success": true,
|
|
158
|
+
"app_key": "oiam_ak_xxxxxxxxxxxxxxxxxxxxxxxx",
|
|
159
|
+
"encrypted_credentials": "base64_encoded_iv_plus_ciphertext...",
|
|
160
|
+
"debug": true // optionnel — active le DebugPanel et les logs console côté frontend
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
**Principe :**
|
|
165
|
+
Le SaaS chiffre `app_key + secret_key + timestamp` en AES-256-CBC en utilisant la `secret_key` elle-même comme clé de chiffrement (SHA-256 → 32 bytes).
|
|
166
|
+
Le frontend transporte le blob opaque + l'`app_key` en clair (non sensible) vers l'IAM.
|
|
167
|
+
L'IAM retrouve la `secret_key` via l'`app_key` dans sa table `applications`, puis déchiffre le blob.
|
|
168
|
+
|
|
169
|
+
**Implémentation Laravel :**
|
|
170
|
+
```php
|
|
171
|
+
// routes/api.php
|
|
172
|
+
Route::get('/native/config', function () {
|
|
173
|
+
$appKey = config('services.iam.app_key');
|
|
174
|
+
$secretKey = config('services.iam.secret_key');
|
|
175
|
+
|
|
176
|
+
// Clé AES = SHA-256 de la secret_key (32 bytes binaires)
|
|
177
|
+
$aesKey = hash('sha256', $secretKey, true);
|
|
178
|
+
$iv = random_bytes(16);
|
|
179
|
+
|
|
180
|
+
$payload = json_encode([
|
|
181
|
+
'app_key' => $appKey,
|
|
182
|
+
'secret_key' => $secretKey,
|
|
183
|
+
'ts' => time(),
|
|
184
|
+
]);
|
|
185
|
+
|
|
186
|
+
$encrypted = openssl_encrypt($payload, 'aes-256-cbc', $aesKey, 0, $iv);
|
|
187
|
+
|
|
188
|
+
return response()->json([
|
|
189
|
+
'success' => true,
|
|
190
|
+
'app_key' => $appKey, // En clair (non sensible, sert d'identifiant)
|
|
191
|
+
'encrypted_credentials' => base64_encode($iv . '::' . $encrypted),
|
|
192
|
+
'debug' => (bool) config('services.iam.debug'),
|
|
193
|
+
]);
|
|
194
|
+
});
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
**Décryptage côté IAM :**
|
|
198
|
+
```php
|
|
199
|
+
// Dans le endpoint /iam/native/encrypt
|
|
200
|
+
// 1. Retrouver la secret_key via l'app_key
|
|
201
|
+
$app = Application::where('app_key', $request->app_key)->first();
|
|
202
|
+
if (!$app) {
|
|
203
|
+
return response()->json(['success' => false, 'message' => 'Application inconnue'], 401);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// 2. Déchiffrer avec la secret_key de l'application
|
|
207
|
+
$aesKey = hash('sha256', $app->secret_key, true); // secret_key = colonne DB
|
|
208
|
+
$decoded = base64_decode($request->encrypted_credentials);
|
|
209
|
+
[$iv, $ciphertext] = explode('::', $decoded, 2);
|
|
210
|
+
|
|
211
|
+
$payload = json_decode(
|
|
212
|
+
openssl_decrypt($ciphertext, 'aes-256-cbc', $aesKey, 0, $iv),
|
|
213
|
+
true
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
// 3. Vérifier le timestamp (anti-replay, max 5 min)
|
|
217
|
+
if (!$payload || time() - $payload['ts'] > 300) {
|
|
218
|
+
return response()->json(['success' => false, 'message' => 'Credentials expirés ou invalides'], 401);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
$appKey = $payload['app_key'];
|
|
222
|
+
$secretKey = $payload['secret_key'];
|
|
223
|
+
// ... valider et continuer le flux
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
---
|
|
227
|
+
|
|
228
|
+
### `POST /api/native/exchange`
|
|
229
|
+
|
|
230
|
+
Échange le `callback_token` reçu de l'IAM contre un token Sanctum local.
|
|
231
|
+
|
|
232
|
+
**Headers requis :** `Content-Type: application/json`
|
|
233
|
+
|
|
234
|
+
**Body de la requête :**
|
|
235
|
+
```json
|
|
236
|
+
{
|
|
237
|
+
"callback_token": "eyJhbGciOiJIUzI1NiIs..."
|
|
238
|
+
}
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
**Logique backend :**
|
|
242
|
+
1. Recevoir le `callback_token`
|
|
243
|
+
2. Appeler l'IAM `POST /api/iam/auth/decrypt` avec `app_key`, `secret_key` et le `callback_token` (remplacer les espaces par `+`)
|
|
244
|
+
3. L'IAM retourne les `user_infos` (9 champs standardisés)
|
|
245
|
+
4. Créer ou mettre à jour l'utilisateur local
|
|
246
|
+
5. Générer un token Sanctum
|
|
247
|
+
6. Retourner le token + user
|
|
248
|
+
|
|
249
|
+
**Réponse succès (200) :**
|
|
250
|
+
```json
|
|
251
|
+
{
|
|
252
|
+
"success": true,
|
|
253
|
+
"token": "1|abc123def456ghi789...",
|
|
254
|
+
"expires_at": "2026-04-23T03:12:32.000000Z",
|
|
255
|
+
"user": {
|
|
256
|
+
"id": 1,
|
|
257
|
+
"reference": "USR-XXXXXXXX",
|
|
258
|
+
"alias_reference": "ALI-XXXXXXXX",
|
|
259
|
+
"name": "John Doe",
|
|
260
|
+
"email": "john@example.com",
|
|
261
|
+
"phone": "+221771234567",
|
|
262
|
+
"ccphone": "+221",
|
|
263
|
+
"image_url": "https://identityam.ollaid.com/storage/avatars/xxx.jpg",
|
|
264
|
+
"email_verified": true,
|
|
265
|
+
"phone_verified": true
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
**Implémentation Laravel :**
|
|
271
|
+
```php
|
|
272
|
+
// routes/api.php
|
|
273
|
+
Route::post('/native/exchange', function (Request $request) {
|
|
274
|
+
$callbackToken = $request->input('callback_token');
|
|
275
|
+
|
|
276
|
+
// ⚠️ IMPORTANT : Remplacer les espaces par '+' avant décryptage
|
|
277
|
+
$callbackToken = str_replace(' ', '+', $callbackToken);
|
|
278
|
+
|
|
279
|
+
// Appel IAM pour décrypter
|
|
280
|
+
$response = Http::post(config('services.iam.base_url') . '/iam/auth/decrypt', [
|
|
281
|
+
'app_key' => config('services.iam.app_key'),
|
|
282
|
+
'secret_key' => config('services.iam.secret_key'),
|
|
283
|
+
'callback_token' => $callbackToken,
|
|
284
|
+
]);
|
|
285
|
+
|
|
286
|
+
if (!$response->successful() || !$response->json('success')) {
|
|
287
|
+
return response()->json([
|
|
288
|
+
'success' => false,
|
|
289
|
+
'error' => 'Token invalide ou expiré',
|
|
290
|
+
'error_type' => 'invalid_token',
|
|
291
|
+
], 401);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
$userInfos = $response->json('user_infos');
|
|
295
|
+
|
|
296
|
+
// Créer ou mettre à jour l'utilisateur local
|
|
297
|
+
$user = User::updateOrCreate(
|
|
298
|
+
['reference' => $userInfos['reference']],
|
|
299
|
+
[
|
|
300
|
+
'name' => $userInfos['name'],
|
|
301
|
+
'email' => $userInfos['email'],
|
|
302
|
+
'phone' => $userInfos['phone'] ?? null,
|
|
303
|
+
'ccphone' => $userInfos['ccphone'] ?? null,
|
|
304
|
+
'image' => $userInfos['image_url'] ?? null,
|
|
305
|
+
'alias_reference' => $userInfos['alias_reference'],
|
|
306
|
+
'user_infos' => json_encode($userInfos),
|
|
307
|
+
'password' => bcrypt(Str::random(32)), // Mot de passe aléatoire
|
|
308
|
+
]
|
|
309
|
+
);
|
|
310
|
+
|
|
311
|
+
// Générer un token Sanctum
|
|
312
|
+
$token = $user->createToken('native-sso')->plainTextToken;
|
|
313
|
+
|
|
314
|
+
return response()->json([
|
|
315
|
+
'success' => true,
|
|
316
|
+
'token' => $token,
|
|
317
|
+
'expires_at' => now()->addDays(30)->toISOString(),
|
|
318
|
+
'user' => [
|
|
319
|
+
'id' => $user->id,
|
|
320
|
+
'reference' => $user->reference,
|
|
321
|
+
'alias_reference' => $user->alias_reference,
|
|
322
|
+
'name' => $user->name,
|
|
323
|
+
'email' => $user->email,
|
|
324
|
+
'phone' => $user->phone,
|
|
325
|
+
'ccphone' => $user->ccphone,
|
|
326
|
+
'image_url' => $user->image,
|
|
327
|
+
'email_verified' => $userInfos['email_verification'] === 'verified',
|
|
328
|
+
'phone_verified' => $userInfos['phone_verification'] === 'verified',
|
|
329
|
+
],
|
|
330
|
+
]);
|
|
331
|
+
});
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
---
|
|
335
|
+
|
|
336
|
+
### `POST /api/native/check-token`
|
|
337
|
+
|
|
338
|
+
Vérifie la validité du token Sanctum et retourne les `user_infos` fraîches. Appelé périodiquement par le package (2 min après login, puis toutes les 5 min).
|
|
339
|
+
|
|
340
|
+
**Headers requis :** `Authorization: Bearer {token}`
|
|
341
|
+
|
|
342
|
+
**Réponse succès (200) :**
|
|
343
|
+
```json
|
|
344
|
+
{
|
|
345
|
+
"success": true,
|
|
346
|
+
"valid": true,
|
|
347
|
+
"user_infos": {
|
|
348
|
+
"name": "John Doe",
|
|
349
|
+
"email": "john@example.com",
|
|
350
|
+
"ccphone": "+221",
|
|
351
|
+
"phone": "771234567",
|
|
352
|
+
"image_url": "https://...",
|
|
353
|
+
"town": "Dakar",
|
|
354
|
+
"country": "SN",
|
|
355
|
+
"auth_2fa": false
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
**Si token invalide/expiré :** Sanctum retourne automatiquement 401.
|
|
361
|
+
|
|
362
|
+
**Implémentation Laravel :**
|
|
363
|
+
```php
|
|
364
|
+
// routes/api.php
|
|
365
|
+
Route::post('/native/check-token', function (Request $request) {
|
|
366
|
+
$user = $request->user();
|
|
367
|
+
|
|
368
|
+
return response()->json([
|
|
369
|
+
'success' => true,
|
|
370
|
+
'valid' => true,
|
|
371
|
+
'user_infos' => [
|
|
372
|
+
'name' => $user->name,
|
|
373
|
+
'email' => $user->email,
|
|
374
|
+
'ccphone' => $user->ccphone,
|
|
375
|
+
'phone' => $user->phone,
|
|
376
|
+
'image_url' => $user->image,
|
|
377
|
+
'town' => $user->town ?? null,
|
|
378
|
+
'country' => $user->country ?? null,
|
|
379
|
+
'auth_2fa' => (bool) ($user->auth_2fa ?? false),
|
|
380
|
+
],
|
|
381
|
+
]);
|
|
382
|
+
})->middleware('auth:sanctum');
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
> **Comportement réseau :** Le package ne déconnecte l'utilisateur que si le backend retourne explicitement **401**. En cas d'erreur réseau, timeout ou serveur inaccessible, la session est conservée.
|
|
386
|
+
|
|
387
|
+
---
|
|
388
|
+
|
|
389
|
+
### `POST /api/native/logout` (single-session)
|
|
390
|
+
|
|
391
|
+
Invalide **uniquement** le token Sanctum courant (`currentAccessToken()->delete()`). Les autres sessions actives de l'utilisateur ne sont pas affectées.
|
|
392
|
+
|
|
393
|
+
**Headers requis :** `Authorization: Bearer {token}`
|
|
394
|
+
|
|
395
|
+
**Réponse succès (200) :**
|
|
396
|
+
```json
|
|
397
|
+
{
|
|
398
|
+
"success": true,
|
|
399
|
+
"message": "Déconnexion réussie"
|
|
400
|
+
}
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
**Implémentation Laravel :**
|
|
404
|
+
```php
|
|
405
|
+
// routes/api.php
|
|
406
|
+
Route::post('/native/logout', function (Request $request) {
|
|
407
|
+
// Supprime UNIQUEMENT le token courant (pas les autres sessions)
|
|
408
|
+
$request->user()->currentAccessToken()->delete();
|
|
409
|
+
|
|
410
|
+
return response()->json([
|
|
411
|
+
'success' => true,
|
|
412
|
+
'message' => 'Déconnexion réussie',
|
|
413
|
+
]);
|
|
414
|
+
})->middleware('auth:sanctum');
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
---
|
|
418
|
+
|
|
419
|
+
## Controller Laravel Complet (copier-coller)
|
|
420
|
+
|
|
421
|
+
Voici un `NativeAuthController.php` complet regroupant les 4 endpoints. Copiez-le dans `app/Http/Controllers/Api/NativeAuthController.php` :
|
|
422
|
+
|
|
423
|
+
```php
|
|
424
|
+
<?php
|
|
425
|
+
|
|
426
|
+
namespace App\Http\Controllers\Api;
|
|
427
|
+
|
|
428
|
+
use App\Http\Controllers\Controller;
|
|
429
|
+
use App\Models\User;
|
|
430
|
+
use Illuminate\Http\JsonResponse;
|
|
431
|
+
use Illuminate\Http\Request;
|
|
432
|
+
use Illuminate\Support\Facades\Http;
|
|
433
|
+
use Illuminate\Support\Facades\Log;
|
|
434
|
+
use Illuminate\Support\Str;
|
|
435
|
+
|
|
436
|
+
class NativeAuthController extends Controller
|
|
437
|
+
{
|
|
438
|
+
// ════════════════════════════════════════
|
|
439
|
+
// GET /api/native/config
|
|
440
|
+
// ════════════════════════════════════════
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Retourne les credentials IAM chiffrés (opaque token).
|
|
444
|
+
* Le frontend ne voit JAMAIS app_key ni secret_key en clair.
|
|
445
|
+
*/
|
|
446
|
+
public function config(): JsonResponse
|
|
447
|
+
{
|
|
448
|
+
$appKey = config('services.iam.app_key');
|
|
449
|
+
$secretKey = config('services.iam.secret_key');
|
|
450
|
+
|
|
451
|
+
if (empty($appKey) || empty($secretKey)) {
|
|
452
|
+
Log::error('[NativeSSO] Config manquante : IAM_APP_KEY ou IAM_SECRET_KEY');
|
|
453
|
+
return response()->json([
|
|
454
|
+
'success' => false,
|
|
455
|
+
'error' => 'Configuration SSO incomplète',
|
|
456
|
+
'error_type' => 'config_missing',
|
|
457
|
+
], 500);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// Clé AES = SHA-256 de la secret_key (32 bytes binaires)
|
|
461
|
+
$aesKey = hash('sha256', $secretKey, true);
|
|
462
|
+
$iv = random_bytes(16);
|
|
463
|
+
$payload = json_encode([
|
|
464
|
+
'app_key' => $appKey,
|
|
465
|
+
'secret_key' => $secretKey,
|
|
466
|
+
'ts' => time(),
|
|
467
|
+
]);
|
|
468
|
+
|
|
469
|
+
$encrypted = openssl_encrypt($payload, 'aes-256-cbc', $aesKey, 0, $iv);
|
|
470
|
+
|
|
471
|
+
return response()->json([
|
|
472
|
+
'success' => true,
|
|
473
|
+
'app_key' => $appKey, // En clair (non sensible, sert d'identifiant pour l'IAM)
|
|
474
|
+
'encrypted_credentials' => base64_encode($iv . '::' . $encrypted),
|
|
475
|
+
'debug' => (bool) config('services.iam.debug'), // Contrôlé par IAM_DEBUG dans .env
|
|
476
|
+
]);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// ════════════════════════════════════════
|
|
480
|
+
// POST /api/native/exchange
|
|
481
|
+
// ════════════════════════════════════════
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* Échange le callback_token IAM contre un token Sanctum local.
|
|
485
|
+
*
|
|
486
|
+
* 1. Reçoit callback_token du frontend
|
|
487
|
+
* 2. Appelle IAM /iam/auth/decrypt pour décrypter
|
|
488
|
+
* 3. Crée ou met à jour l'utilisateur local
|
|
489
|
+
* 4. Génère un token Sanctum
|
|
490
|
+
*/
|
|
491
|
+
public function exchange(Request $request): JsonResponse
|
|
492
|
+
{
|
|
493
|
+
$callbackToken = $request->input('callback_token');
|
|
494
|
+
|
|
495
|
+
if (empty($callbackToken)) {
|
|
496
|
+
return response()->json([
|
|
497
|
+
'success' => false,
|
|
498
|
+
'error' => 'callback_token est requis',
|
|
499
|
+
'error_type' => 'missing_token',
|
|
500
|
+
], 400);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// ⚠️ IMPORTANT : Remplacer les espaces par '+' (encodage URL)
|
|
504
|
+
$callbackToken = str_replace(' ', '+', $callbackToken);
|
|
505
|
+
|
|
506
|
+
try {
|
|
507
|
+
$response = Http::timeout(30)->post(
|
|
508
|
+
config('services.iam.base_url') . '/iam/auth/decrypt',
|
|
509
|
+
[
|
|
510
|
+
'app_key' => config('services.iam.app_key'),
|
|
511
|
+
'secret_key' => config('services.iam.secret_key'),
|
|
512
|
+
'callback_token' => $callbackToken,
|
|
513
|
+
]
|
|
514
|
+
);
|
|
515
|
+
|
|
516
|
+
if (!$response->successful() || !$response->json('success')) {
|
|
517
|
+
$error = $response->json();
|
|
518
|
+
Log::warning('[NativeSSO] Échec décryptage callback_token', [
|
|
519
|
+
'status' => $response->status(),
|
|
520
|
+
'error' => $error['message'] ?? 'Inconnu',
|
|
521
|
+
]);
|
|
522
|
+
|
|
523
|
+
return response()->json([
|
|
524
|
+
'success' => false,
|
|
525
|
+
'error' => $error['message'] ?? 'Token invalide ou expiré',
|
|
526
|
+
'error_type' => 'invalid_token',
|
|
527
|
+
], 401);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
$data = $response->json('data', $response->json());
|
|
531
|
+
$iamReference = $data['iam_reference'] ?? null;
|
|
532
|
+
$aliasReference = $data['alias_reference'] ?? null;
|
|
533
|
+
$userInfos = $data['user_infos'] ?? [];
|
|
534
|
+
|
|
535
|
+
if (!$iamReference || empty($userInfos)) {
|
|
536
|
+
return response()->json([
|
|
537
|
+
'success' => false,
|
|
538
|
+
'error' => 'Données utilisateur incomplètes depuis l\'IAM',
|
|
539
|
+
'error_type' => 'decrypt_failed',
|
|
540
|
+
], 422);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// Créer ou mettre à jour l'utilisateur local
|
|
544
|
+
$user = User::where('reference', $iamReference)->first();
|
|
545
|
+
if (!$user && $aliasReference) {
|
|
546
|
+
$user = User::where('alias_reference', $aliasReference)->first();
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
$userData = [
|
|
550
|
+
'name' => $userInfos['name'] ?? null,
|
|
551
|
+
'email' => $userInfos['email'] ?? null,
|
|
552
|
+
'phone' => $userInfos['phone'] ?? null,
|
|
553
|
+
'ccphone' => $userInfos['ccphone'] ?? null,
|
|
554
|
+
'image' => $userInfos['image_url'] ?? null,
|
|
555
|
+
'alias_reference' => $aliasReference,
|
|
556
|
+
'user_infos' => json_encode($userInfos),
|
|
557
|
+
];
|
|
558
|
+
|
|
559
|
+
if ($user) {
|
|
560
|
+
$user->update($userData);
|
|
561
|
+
} else {
|
|
562
|
+
$user = User::create(array_merge($userData, [
|
|
563
|
+
'reference' => $iamReference,
|
|
564
|
+
'password' => bcrypt(Str::random(32)),
|
|
565
|
+
]));
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// Générer un token Sanctum (expiration 30 jours)
|
|
569
|
+
$expiresAt = now()->addDays(30);
|
|
570
|
+
$token = $user->createToken('native-sso', ['*'], $expiresAt);
|
|
571
|
+
|
|
572
|
+
return response()->json([
|
|
573
|
+
'success' => true,
|
|
574
|
+
'token' => $token->plainTextToken,
|
|
575
|
+
'expires_at' => $expiresAt->toIso8601String(),
|
|
576
|
+
'user' => [
|
|
577
|
+
'id' => $user->id,
|
|
578
|
+
'reference' => $iamReference,
|
|
579
|
+
'alias_reference' => $aliasReference,
|
|
580
|
+
'name' => $userInfos['name'] ?? null,
|
|
581
|
+
'email' => $userInfos['email'] ?? null,
|
|
582
|
+
'phone' => isset($userInfos['phone'])
|
|
583
|
+
? ($userInfos['ccphone'] ?? '') . $userInfos['phone']
|
|
584
|
+
: null,
|
|
585
|
+
'ccphone' => $userInfos['ccphone'] ?? null,
|
|
586
|
+
'image_url' => $userInfos['image_url'] ?? null,
|
|
587
|
+
'email_verified' => ($userInfos['email_verification'] ?? '') === 'verified',
|
|
588
|
+
'phone_verified' => ($userInfos['phone_verification'] ?? '') === 'verified',
|
|
589
|
+
],
|
|
590
|
+
]);
|
|
591
|
+
|
|
592
|
+
} catch (\Illuminate\Http\Client\ConnectionException $e) {
|
|
593
|
+
Log::error('[NativeSSO] Timeout connexion IAM', ['error' => $e->getMessage()]);
|
|
594
|
+
return response()->json([
|
|
595
|
+
'success' => false,
|
|
596
|
+
'error' => 'Impossible de contacter le serveur d\'authentification',
|
|
597
|
+
'error_type' => 'exchange_error',
|
|
598
|
+
], 503);
|
|
599
|
+
|
|
600
|
+
} catch (\Exception $e) {
|
|
601
|
+
Log::error('[NativeSSO] Erreur exchange', ['error' => $e->getMessage()]);
|
|
602
|
+
$details = [];
|
|
603
|
+
if (config('services.iam.debug')) {
|
|
604
|
+
$details = [
|
|
605
|
+
'debug_error' => $e->getMessage(),
|
|
606
|
+
'debug_file' => basename($e->getFile()) . ':' . $e->getLine(),
|
|
607
|
+
];
|
|
608
|
+
}
|
|
609
|
+
return response()->json([
|
|
610
|
+
'success' => false,
|
|
611
|
+
'error' => 'Erreur lors de l\'authentification',
|
|
612
|
+
'error_type' => 'exchange_error',
|
|
613
|
+
...$details,
|
|
614
|
+
], 500);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// ════════════════════════════════════════
|
|
619
|
+
// POST /api/native/check-token
|
|
620
|
+
// ════════════════════════════════════════
|
|
621
|
+
|
|
622
|
+
/**
|
|
623
|
+
* Vérifie la validité du token Sanctum et retourne les user_infos fraîches.
|
|
624
|
+
* Route protégée par auth:sanctum.
|
|
625
|
+
*/
|
|
626
|
+
public function checkToken(Request $request): JsonResponse
|
|
627
|
+
{
|
|
628
|
+
$user = $request->user();
|
|
629
|
+
|
|
630
|
+
return response()->json([
|
|
631
|
+
'success' => true,
|
|
632
|
+
'valid' => true,
|
|
633
|
+
'user_infos' => [
|
|
634
|
+
'name' => $user->name,
|
|
635
|
+
'email' => $user->email,
|
|
636
|
+
'ccphone' => $user->ccphone,
|
|
637
|
+
'phone' => $user->phone,
|
|
638
|
+
'image_url' => $user->image,
|
|
639
|
+
'town' => $user->town ?? null,
|
|
640
|
+
'country' => $user->country ?? null,
|
|
641
|
+
'auth_2fa' => (bool) ($user->auth_2fa ?? false),
|
|
642
|
+
],
|
|
643
|
+
]);
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
// ════════════════════════════════════════
|
|
647
|
+
// POST /api/native/logout
|
|
648
|
+
// ════════════════════════════════════════
|
|
649
|
+
|
|
650
|
+
/**
|
|
651
|
+
* Révoque UNIQUEMENT le token Sanctum courant (single-session).
|
|
652
|
+
* Route protégée par auth:sanctum.
|
|
653
|
+
*/
|
|
654
|
+
public function logout(Request $request): JsonResponse
|
|
655
|
+
{
|
|
656
|
+
$request->user()->currentAccessToken()->delete();
|
|
657
|
+
|
|
658
|
+
return response()->json([
|
|
659
|
+
'success' => true,
|
|
660
|
+
'message' => 'Déconnexion réussie',
|
|
661
|
+
]);
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
```
|
|
665
|
+
|
|
666
|
+
### Routes `routes/api.php`
|
|
667
|
+
|
|
668
|
+
```php
|
|
669
|
+
use App\Http\Controllers\Api\NativeAuthController;
|
|
670
|
+
|
|
671
|
+
// Routes SSO Native (pas d'auth requise)
|
|
672
|
+
Route::get('/native/config', [NativeAuthController::class, 'config']);
|
|
673
|
+
Route::post('/native/exchange', [NativeAuthController::class, 'exchange']);
|
|
674
|
+
|
|
675
|
+
// Routes SSO Native (auth Sanctum requise)
|
|
676
|
+
Route::middleware('auth:sanctum')->group(function () {
|
|
677
|
+
Route::post('/native/check-token', [NativeAuthController::class, 'checkToken']);
|
|
678
|
+
Route::post('/native/logout', [NativeAuthController::class, 'logout']);
|
|
679
|
+
});
|
|
680
|
+
```
|
|
681
|
+
|
|
682
|
+
### Middleware `ForceJsonResponse` (recommandé)
|
|
683
|
+
|
|
684
|
+
Pour éviter que Laravel retourne du HTML au lieu de JSON en cas d'erreur :
|
|
685
|
+
|
|
686
|
+
```php
|
|
687
|
+
// app/Http/Middleware/ForceJsonResponse.php
|
|
688
|
+
namespace App\Http\Middleware;
|
|
689
|
+
|
|
690
|
+
use Closure;
|
|
691
|
+
use Illuminate\Http\Request;
|
|
692
|
+
|
|
693
|
+
class ForceJsonResponse
|
|
694
|
+
{
|
|
695
|
+
public function handle(Request $request, Closure $next)
|
|
696
|
+
{
|
|
697
|
+
$request->headers->set('Accept', 'application/json');
|
|
698
|
+
return $next($request);
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
```
|
|
702
|
+
|
|
703
|
+
Appliquez-le sur les routes API dans `bootstrap/app.php` (Laravel 11+) :
|
|
704
|
+
```php
|
|
705
|
+
->withMiddleware(function (Middleware $middleware) {
|
|
706
|
+
$middleware->api(prepend: [
|
|
707
|
+
\App\Http\Middleware\ForceJsonResponse::class,
|
|
708
|
+
]);
|
|
709
|
+
})
|
|
710
|
+
```
|
|
711
|
+
|
|
712
|
+
Ou dans `app/Http/Kernel.php` (Laravel 10 et avant) :
|
|
713
|
+
```php
|
|
714
|
+
'api' => [
|
|
715
|
+
\App\Http\Middleware\ForceJsonResponse::class,
|
|
716
|
+
// ... autres middlewares
|
|
717
|
+
],
|
|
718
|
+
```
|
|
719
|
+
|
|
720
|
+
---
|
|
721
|
+
|
|
722
|
+
## Réponses d'erreur
|
|
723
|
+
|
|
724
|
+
Tous les endpoints retournent le même format d'erreur :
|
|
725
|
+
|
|
726
|
+
```json
|
|
727
|
+
{
|
|
728
|
+
"success": false,
|
|
729
|
+
"error": "Description lisible de l'erreur",
|
|
730
|
+
"error_type": "code_erreur"
|
|
731
|
+
}
|
|
732
|
+
```
|
|
733
|
+
|
|
734
|
+
### Codes d'erreur par endpoint
|
|
735
|
+
|
|
736
|
+
#### `/api/native/config`
|
|
737
|
+
|
|
738
|
+
| Code HTTP | `error_type` | Description |
|
|
739
|
+
|-----------|-------------|-------------|
|
|
740
|
+
| 500 | `config_missing` | `IAM_APP_KEY` ou `IAM_SECRET_KEY` non configuré dans le `.env` |
|
|
741
|
+
|
|
742
|
+
#### `/api/native/exchange`
|
|
743
|
+
|
|
744
|
+
| Code HTTP | `error_type` | Description |
|
|
745
|
+
|-----------|-------------|-------------|
|
|
746
|
+
| 400 | `missing_token` | `callback_token` absent du body |
|
|
747
|
+
| 401 | `invalid_token` | Token expiré, invalide ou déjà utilisé |
|
|
748
|
+
| 422 | `decrypt_failed` | Erreur lors de l'appel à l'IAM `/iam/auth/decrypt` |
|
|
749
|
+
| 500 | `exchange_error` | Erreur interne lors de la création du user/token |
|
|
750
|
+
|
|
751
|
+
#### `/api/native/logout`
|
|
752
|
+
|
|
753
|
+
| Code HTTP | `error_type` | Description |
|
|
754
|
+
|-----------|-------------|-------------|
|
|
755
|
+
| 401 | `unauthenticated` | Token Bearer manquant ou invalide |
|
|
756
|
+
|
|
757
|
+
---
|
|
758
|
+
|
|
759
|
+
## Configuration .env Laravel
|
|
760
|
+
|
|
761
|
+
Ajoutez ces variables dans le fichier `.env` du backend SaaS :
|
|
762
|
+
|
|
763
|
+
```env
|
|
764
|
+
# Credentials IAM (récupérés depuis le dashboard iam.ollaid.com)
|
|
765
|
+
IAM_APP_KEY=oiam_ak_xxxxxxxxxxxxxxxxxxxxxxxx
|
|
766
|
+
IAM_SECRET_KEY=oiam_sk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
|
767
|
+
IAM_BASE_URL=https://identityam.ollaid.com/api
|
|
768
|
+
|
|
769
|
+
# Mode debug — contrôle le DebugPanel et les logs côté frontend
|
|
770
|
+
# true : active le DebugPanel + logs console + détails d'erreur dans les réponses JSON
|
|
771
|
+
# false : mode production (aucun détail d'erreur exposé, pas de DebugPanel)
|
|
772
|
+
# ⚠️ Si absent ou null → considéré comme false (mode production)
|
|
773
|
+
IAM_DEBUG=false
|
|
774
|
+
```
|
|
775
|
+
|
|
776
|
+
> **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`.
|
|
777
|
+
|
|
778
|
+
Et dans `config/services.php` :
|
|
779
|
+
|
|
780
|
+
```php
|
|
781
|
+
'iam' => [
|
|
782
|
+
'app_key' => env('IAM_APP_KEY'),
|
|
783
|
+
'secret_key' => env('IAM_SECRET_KEY'),
|
|
784
|
+
'base_url' => env('IAM_BASE_URL', 'https://identityam.ollaid.com/api'),
|
|
785
|
+
'debug' => env('IAM_DEBUG', false), // Active le debug côté frontend (false si absent)
|
|
786
|
+
],
|
|
787
|
+
```
|
|
788
|
+
|
|
789
|
+
---
|
|
790
|
+
|
|
791
|
+
## Migration Laravel
|
|
792
|
+
|
|
793
|
+
### Colonnes requises sur la table `users`
|
|
794
|
+
|
|
795
|
+
Si votre table `users` n'a pas encore ces colonnes, ajoutez-les :
|
|
796
|
+
|
|
797
|
+
```bash
|
|
798
|
+
php artisan make:migration add_iam_columns_to_users_table
|
|
799
|
+
```
|
|
800
|
+
|
|
801
|
+
```php
|
|
802
|
+
public function up(): void
|
|
803
|
+
{
|
|
804
|
+
Schema::table('users', function (Blueprint $table) {
|
|
805
|
+
$table->string('reference')->unique()->nullable()->after('id');
|
|
806
|
+
$table->string('alias_reference')->nullable()->after('reference');
|
|
807
|
+
$table->string('ccphone')->nullable();
|
|
808
|
+
$table->string('phone')->nullable();
|
|
809
|
+
$table->string('image')->nullable();
|
|
810
|
+
$table->json('user_infos')->nullable();
|
|
811
|
+
$table->string('phone_verification')->default('pending')->nullable();
|
|
812
|
+
$table->string('email_verification')->default('pending')->nullable();
|
|
813
|
+
});
|
|
814
|
+
}
|
|
815
|
+
```
|
|
816
|
+
|
|
817
|
+
> **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.
|
|
818
|
+
|
|
819
|
+
---
|
|
820
|
+
|
|
821
|
+
## Flux d'authentification
|
|
822
|
+
|
|
823
|
+
```
|
|
824
|
+
┌──────────────────┐ ┌──────────────┐ ┌──────────────┐
|
|
825
|
+
│ @ollaid/native │ │ IAM API │ │ SaaS API │
|
|
826
|
+
│ -sso (Frontend) │ │ (Ollaid) │ │ (Laravel) │
|
|
827
|
+
└────────┬─────────┘ └──────┬───────┘ └──────┬───────┘
|
|
828
|
+
│ │ │
|
|
829
|
+
│ 1. GET /api/native/config │
|
|
830
|
+
│───────────────────────────────────────────►│
|
|
831
|
+
│◄──────────────── encrypted_credentials ────│
|
|
832
|
+
│ │ │
|
|
833
|
+
│ 2. POST /iam/native/encrypt │
|
|
834
|
+
│─────────────────────►│ │
|
|
835
|
+
│◄── encrypted_credentials │
|
|
836
|
+
│ │ │
|
|
837
|
+
│ 3. POST /iam/native/init │
|
|
838
|
+
│─────────────────────►│ │
|
|
839
|
+
│◄── status + session │ │
|
|
840
|
+
│ │ │
|
|
841
|
+
│ 4. POST /iam/native/validate │
|
|
842
|
+
│─────────────────────►│ │
|
|
843
|
+
│◄── callback_token │ │
|
|
844
|
+
│ │ │
|
|
845
|
+
│ 5. POST /api/native/exchange │
|
|
846
|
+
│───────────────────────────────────────────►│
|
|
847
|
+
│ │ decrypt via IAM │
|
|
848
|
+
│ │◄────────────────────│
|
|
849
|
+
│ │────────────────────►│
|
|
850
|
+
│◄──────────────── sanctum token + user ─────│
|
|
851
|
+
│ │ │
|
|
852
|
+
│ ✅ Connecté ! │ │
|
|
853
|
+
```
|
|
854
|
+
|
|
855
|
+
### Détail des étapes :
|
|
856
|
+
|
|
857
|
+
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.
|
|
858
|
+
2. **Encrypt** — Le frontend envoie le blob `encrypted_credentials` à l'IAM, qui le déchiffre côté serveur pour valider les credentials
|
|
859
|
+
3. **Init** — L'IAM vérifie le compte et retourne le statut (`pending_password`, `pending_otp`, `needs_access`, etc.)
|
|
860
|
+
4. **Validate** — Le frontend envoie le mot de passe/OTP, l'IAM retourne un `callback_token`
|
|
861
|
+
5. **Exchange** — Le frontend envoie le `callback_token` au backend SaaS, qui le décrypte via l'IAM et crée une session Sanctum
|
|
862
|
+
|
|
863
|
+
> **Important :** Le package gère les étapes 1-5 automatiquement. Le backend SaaS doit implémenter **4 endpoints** (`config`, `exchange`, `check-token`, `logout`).
|
|
864
|
+
|
|
865
|
+
---
|
|
866
|
+
|
|
867
|
+
## APIs IAM Account (Server-to-Server)
|
|
868
|
+
|
|
869
|
+
> ⚠️ **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.
|
|
870
|
+
|
|
871
|
+
Le package expose un service `iamAccountService` pour interagir avec les APIs IAM de gestion de compte :
|
|
872
|
+
|
|
873
|
+
```ts
|
|
874
|
+
import { iamAccountService } from '@ollaid/native-sso';
|
|
875
|
+
```
|
|
876
|
+
|
|
877
|
+
---
|
|
878
|
+
|
|
879
|
+
### `POST /api/iam/link-phone`
|
|
880
|
+
|
|
881
|
+
Associe un numéro de téléphone à un compte utilisateur existant dans l'IAM.
|
|
882
|
+
|
|
883
|
+
**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.
|
|
884
|
+
|
|
885
|
+
**Paramètres requis :**
|
|
886
|
+
|
|
887
|
+
```json
|
|
888
|
+
{
|
|
889
|
+
"app_key": "oiam_ak_xxxxxxxxxxxxxxxxxxxxxxxx",
|
|
890
|
+
"secret_key": "oiam_sk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
|
|
891
|
+
"iam_reference": "USR-XXXXXXXX",
|
|
892
|
+
"ccphone": "+221",
|
|
893
|
+
"phone": "771234567"
|
|
894
|
+
}
|
|
895
|
+
```
|
|
896
|
+
|
|
897
|
+
| Champ | Type | Description |
|
|
898
|
+
|-------|------|-------------|
|
|
899
|
+
| `app_key` | `string` | Clé publique de l'application IAM |
|
|
900
|
+
| `secret_key` | `string` | Clé secrète de l'application IAM |
|
|
901
|
+
| `iam_reference` | `string` | Référence unique de l'utilisateur dans l'IAM (`USR-XXXXXXXX`) |
|
|
902
|
+
| `ccphone` | `string` | Indicatif téléphonique (ex: `+221`) |
|
|
903
|
+
| `phone` | `string` | Numéro de téléphone sans indicatif (ex: `771234567`) |
|
|
904
|
+
|
|
905
|
+
**Réponse succès (200) :**
|
|
906
|
+
|
|
907
|
+
```json
|
|
908
|
+
{
|
|
909
|
+
"success": true,
|
|
910
|
+
"message": "Numéro de téléphone lié avec succès",
|
|
911
|
+
"user_reference": "USR-XXXXXXXX",
|
|
912
|
+
"user_infos": {
|
|
913
|
+
"name": "John Doe",
|
|
914
|
+
"email": "john@example.com",
|
|
915
|
+
"ccphone": "+221",
|
|
916
|
+
"phone": "771234567",
|
|
917
|
+
"address": null,
|
|
918
|
+
"town": "Dakar",
|
|
919
|
+
"country": "SN",
|
|
920
|
+
"image_url": "https://identityam.ollaid.com/storage/avatars/xxx.jpg",
|
|
921
|
+
"auth_2fa": false
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
```
|
|
925
|
+
|
|
926
|
+
**Codes d'erreur :**
|
|
927
|
+
|
|
928
|
+
| Code HTTP | `error_type` | Description |
|
|
929
|
+
|-----------|-------------|-------------|
|
|
930
|
+
| 401 | `invalid_credentials` | `app_key` ou `secret_key` invalide |
|
|
931
|
+
| 404 | `user_not_found` | `iam_reference` introuvable |
|
|
932
|
+
| 409 | `phone_already_linked` | Ce numéro est déjà associé à un autre compte |
|
|
933
|
+
| 422 | `validation_error` | Champs manquants ou format invalide |
|
|
934
|
+
|
|
935
|
+
**Exemple Node.js (backend) :**
|
|
936
|
+
|
|
937
|
+
```js
|
|
938
|
+
import { iamAccountService } from '@ollaid/native-sso';
|
|
939
|
+
|
|
940
|
+
const result = await iamAccountService.linkPhone({
|
|
941
|
+
app_key: process.env.IAM_APP_KEY,
|
|
942
|
+
secret_key: process.env.IAM_SECRET_KEY,
|
|
943
|
+
iam_reference: 'USR-XXXXXXXX',
|
|
944
|
+
ccphone: '+221',
|
|
945
|
+
phone: '771234567',
|
|
946
|
+
});
|
|
947
|
+
|
|
948
|
+
if (result.success) {
|
|
949
|
+
// Mettre à jour l'utilisateur local avec result.user_infos
|
|
950
|
+
console.log('Téléphone lié :', result.user_infos.phone);
|
|
951
|
+
}
|
|
952
|
+
```
|
|
953
|
+
|
|
954
|
+
---
|
|
955
|
+
|
|
956
|
+
### `POST /api/iam/link-email`
|
|
957
|
+
|
|
958
|
+
Associe une adresse email à un compte utilisateur existant dans l'IAM.
|
|
959
|
+
|
|
960
|
+
**Cas d'usage :** Un utilisateur inscrit par téléphone (phone-only) souhaite ajouter son email.
|
|
961
|
+
|
|
962
|
+
**Paramètres requis :**
|
|
963
|
+
|
|
964
|
+
```json
|
|
965
|
+
{
|
|
966
|
+
"app_key": "oiam_ak_xxxxxxxxxxxxxxxxxxxxxxxx",
|
|
967
|
+
"secret_key": "oiam_sk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
|
|
968
|
+
"iam_reference": "USR-XXXXXXXX",
|
|
969
|
+
"email": "john@example.com"
|
|
970
|
+
}
|
|
971
|
+
```
|
|
972
|
+
|
|
973
|
+
| Champ | Type | Description |
|
|
974
|
+
|-------|------|-------------|
|
|
975
|
+
| `app_key` | `string` | Clé publique de l'application IAM |
|
|
976
|
+
| `secret_key` | `string` | Clé secrète de l'application IAM |
|
|
977
|
+
| `iam_reference` | `string` | Référence unique de l'utilisateur dans l'IAM |
|
|
978
|
+
| `email` | `string` | Adresse email à associer |
|
|
979
|
+
|
|
980
|
+
**Réponse succès (200) :**
|
|
981
|
+
|
|
982
|
+
```json
|
|
983
|
+
{
|
|
984
|
+
"success": true,
|
|
985
|
+
"message": "Adresse email liée avec succès",
|
|
986
|
+
"user_reference": "USR-XXXXXXXX",
|
|
987
|
+
"user_infos": {
|
|
988
|
+
"name": "John Doe",
|
|
989
|
+
"email": "john@example.com",
|
|
990
|
+
"ccphone": "+221",
|
|
991
|
+
"phone": "771234567",
|
|
992
|
+
"address": null,
|
|
993
|
+
"town": "Dakar",
|
|
994
|
+
"country": "SN",
|
|
995
|
+
"image_url": "https://identityam.ollaid.com/storage/avatars/xxx.jpg",
|
|
996
|
+
"auth_2fa": false
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
```
|
|
1000
|
+
|
|
1001
|
+
**Codes d'erreur :**
|
|
1002
|
+
|
|
1003
|
+
| Code HTTP | `error_type` | Description |
|
|
1004
|
+
|-----------|-------------|-------------|
|
|
1005
|
+
| 401 | `invalid_credentials` | `app_key` ou `secret_key` invalide |
|
|
1006
|
+
| 404 | `user_not_found` | `iam_reference` introuvable |
|
|
1007
|
+
| 409 | `email_already_linked` | Cet email est déjà associé à un autre compte |
|
|
1008
|
+
| 422 | `validation_error` | Champs manquants ou format invalide |
|
|
1009
|
+
|
|
1010
|
+
**Exemple Node.js (backend) :**
|
|
1011
|
+
|
|
1012
|
+
```js
|
|
1013
|
+
import { iamAccountService } from '@ollaid/native-sso';
|
|
1014
|
+
|
|
1015
|
+
const result = await iamAccountService.linkEmail({
|
|
1016
|
+
app_key: process.env.IAM_APP_KEY,
|
|
1017
|
+
secret_key: process.env.IAM_SECRET_KEY,
|
|
1018
|
+
iam_reference: 'USR-XXXXXXXX',
|
|
1019
|
+
email: 'john@example.com',
|
|
1020
|
+
});
|
|
1021
|
+
|
|
1022
|
+
if (result.success) {
|
|
1023
|
+
console.log('Email lié :', result.user_infos.email);
|
|
1024
|
+
}
|
|
1025
|
+
```
|
|
1026
|
+
|
|
1027
|
+
---
|
|
1028
|
+
|
|
1029
|
+
### `POST /api/iam/refresh-user-info` (Single)
|
|
1030
|
+
|
|
1031
|
+
Récupère les informations à jour d'un utilisateur depuis l'IAM.
|
|
1032
|
+
|
|
1033
|
+
**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.
|
|
1034
|
+
|
|
1035
|
+
**Paramètres requis :**
|
|
1036
|
+
|
|
1037
|
+
```json
|
|
1038
|
+
{
|
|
1039
|
+
"secret_key": "oiam_sk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
|
|
1040
|
+
"alias_reference": "ALI-XXXXXXXX"
|
|
1041
|
+
}
|
|
1042
|
+
```
|
|
1043
|
+
|
|
1044
|
+
| Champ | Type | Description |
|
|
1045
|
+
|-------|------|-------------|
|
|
1046
|
+
| `secret_key` | `string` | Clé secrète de l'application IAM |
|
|
1047
|
+
| `alias_reference` | `string` | Référence de l'alias utilisateur dans le contexte de votre app (`ALI-XXXXXXXX`) |
|
|
1048
|
+
|
|
1049
|
+
**Réponse succès (200) :**
|
|
1050
|
+
|
|
1051
|
+
```json
|
|
1052
|
+
{
|
|
1053
|
+
"success": true,
|
|
1054
|
+
"message": "Informations utilisateur récupérées",
|
|
1055
|
+
"user_reference": "USR-XXXXXXXX",
|
|
1056
|
+
"alias_reference": "ALI-XXXXXXXX",
|
|
1057
|
+
"login_type": "email",
|
|
1058
|
+
"user_infos": {
|
|
1059
|
+
"name": "John Doe",
|
|
1060
|
+
"email": "john@example.com",
|
|
1061
|
+
"ccphone": "+221",
|
|
1062
|
+
"phone": "771234567",
|
|
1063
|
+
"address": null,
|
|
1064
|
+
"town": "Dakar",
|
|
1065
|
+
"country": "SN",
|
|
1066
|
+
"image_url": "https://identityam.ollaid.com/storage/avatars/xxx.jpg",
|
|
1067
|
+
"auth_2fa": false
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
```
|
|
1071
|
+
|
|
1072
|
+
**Codes d'erreur :**
|
|
1073
|
+
|
|
1074
|
+
| Code HTTP | `error_type` | Description |
|
|
1075
|
+
|-----------|-------------|-------------|
|
|
1076
|
+
| 401 | `invalid_credentials` | `secret_key` invalide |
|
|
1077
|
+
| 404 | `alias_not_found` | `alias_reference` introuvable |
|
|
1078
|
+
|
|
1079
|
+
**Exemple Node.js :**
|
|
1080
|
+
|
|
1081
|
+
```js
|
|
1082
|
+
import { iamAccountService } from '@ollaid/native-sso';
|
|
1083
|
+
|
|
1084
|
+
const result = await iamAccountService.refreshUserInfo({
|
|
1085
|
+
secret_key: process.env.IAM_SECRET_KEY,
|
|
1086
|
+
alias_reference: 'ALI-XXXXXXXX',
|
|
1087
|
+
});
|
|
1088
|
+
|
|
1089
|
+
if (result.success) {
|
|
1090
|
+
// Mettre à jour le profil local
|
|
1091
|
+
await updateLocalUser(result.alias_reference, result.user_infos);
|
|
1092
|
+
}
|
|
1093
|
+
```
|
|
1094
|
+
|
|
1095
|
+
---
|
|
1096
|
+
|
|
1097
|
+
### `POST /api/iam/refresh-user-info` (Bulk)
|
|
1098
|
+
|
|
1099
|
+
Synchronise les informations de **plusieurs utilisateurs** en un seul appel (maximum **100 références** par requête).
|
|
1100
|
+
|
|
1101
|
+
**Cas d'usage :** Synchronisation batch quotidienne, ou mise à jour groupée après un import.
|
|
1102
|
+
|
|
1103
|
+
**Paramètres requis :**
|
|
1104
|
+
|
|
1105
|
+
```json
|
|
1106
|
+
{
|
|
1107
|
+
"secret_key": "oiam_sk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
|
|
1108
|
+
"alias_references": [
|
|
1109
|
+
"ALI-AAAAAAAA",
|
|
1110
|
+
"ALI-BBBBBBBB",
|
|
1111
|
+
"ALI-CCCCCCCC"
|
|
1112
|
+
]
|
|
1113
|
+
}
|
|
1114
|
+
```
|
|
1115
|
+
|
|
1116
|
+
| Champ | Type | Description |
|
|
1117
|
+
|-------|------|-------------|
|
|
1118
|
+
| `secret_key` | `string` | Clé secrète de l'application IAM |
|
|
1119
|
+
| `alias_references` | `string[]` | Liste des alias à synchroniser (max 100) |
|
|
1120
|
+
|
|
1121
|
+
**Réponse succès (200) :**
|
|
1122
|
+
|
|
1123
|
+
```json
|
|
1124
|
+
{
|
|
1125
|
+
"success": true,
|
|
1126
|
+
"message": "Synchronisation terminée",
|
|
1127
|
+
"total_requested": 3,
|
|
1128
|
+
"total_found": 2,
|
|
1129
|
+
"total_errors": 1,
|
|
1130
|
+
"data": [
|
|
1131
|
+
{
|
|
1132
|
+
"user_reference": "USR-AAAAAAAA",
|
|
1133
|
+
"alias_reference": "ALI-AAAAAAAA",
|
|
1134
|
+
"login_type": "email",
|
|
1135
|
+
"user_infos": { "name": "Alice", "email": "alice@example.com", "..." : "..." }
|
|
1136
|
+
},
|
|
1137
|
+
{
|
|
1138
|
+
"user_reference": "USR-BBBBBBBB",
|
|
1139
|
+
"alias_reference": "ALI-BBBBBBBB",
|
|
1140
|
+
"login_type": "phone",
|
|
1141
|
+
"user_infos": { "name": "Bob", "phone": "771234567", "..." : "..." }
|
|
1142
|
+
}
|
|
1143
|
+
],
|
|
1144
|
+
"errors": [
|
|
1145
|
+
{
|
|
1146
|
+
"alias_reference": "ALI-CCCCCCCC",
|
|
1147
|
+
"error": "Alias introuvable"
|
|
1148
|
+
}
|
|
1149
|
+
]
|
|
1150
|
+
}
|
|
1151
|
+
```
|
|
1152
|
+
|
|
1153
|
+
**Codes d'erreur :**
|
|
1154
|
+
|
|
1155
|
+
| Code HTTP | `error_type` | Description |
|
|
1156
|
+
|-----------|-------------|-------------|
|
|
1157
|
+
| 401 | `invalid_credentials` | `secret_key` invalide |
|
|
1158
|
+
| 422 | `too_many_references` | Plus de 100 références envoyées |
|
|
1159
|
+
| 422 | `validation_error` | `alias_references` manquant ou invalide |
|
|
1160
|
+
|
|
1161
|
+
**Exemple Node.js :**
|
|
1162
|
+
|
|
1163
|
+
```js
|
|
1164
|
+
import { iamAccountService } from '@ollaid/native-sso';
|
|
1165
|
+
|
|
1166
|
+
const result = await iamAccountService.refreshUserInfoBulk({
|
|
1167
|
+
secret_key: process.env.IAM_SECRET_KEY,
|
|
1168
|
+
alias_references: ['ALI-AAAAAAAA', 'ALI-BBBBBBBB', 'ALI-CCCCCCCC'],
|
|
1169
|
+
});
|
|
1170
|
+
|
|
1171
|
+
console.log(`Synchronisés: ${result.total_found}/${result.total_requested}`);
|
|
1172
|
+
|
|
1173
|
+
// Traiter les succès
|
|
1174
|
+
for (const item of result.data ?? []) {
|
|
1175
|
+
await updateLocalUser(item.alias_reference, item.user_infos);
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
// Logger les erreurs
|
|
1179
|
+
for (const err of result.errors ?? []) {
|
|
1180
|
+
console.warn(`Erreur pour ${err.alias_reference}: ${err.error}`);
|
|
1181
|
+
}
|
|
1182
|
+
```
|
|
1183
|
+
|
|
1184
|
+
---
|
|
1185
|
+
|
|
1186
|
+
## Avatar par application
|
|
1187
|
+
|
|
1188
|
+
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`.
|
|
1189
|
+
|
|
1190
|
+
### Cascade de résolution `image_url`
|
|
1191
|
+
|
|
1192
|
+
Dans toutes les réponses `user_infos`, le champ `image_url` est résolu selon cette priorité :
|
|
1193
|
+
|
|
1194
|
+
1. **`app_access.avatar`** — Avatar spécifique à l'application (si défini et valide)
|
|
1195
|
+
2. **`user.image`** — Image globale du profil IAM (si définie et valide)
|
|
1196
|
+
3. **`no-image.png`** — Fallback par défaut
|
|
1197
|
+
|
|
1198
|
+
### Migration requise
|
|
1199
|
+
|
|
1200
|
+
```sql
|
|
1201
|
+
ALTER TABLE app_accesses ADD COLUMN avatar VARCHAR(255) NULL AFTER status;
|
|
1202
|
+
```
|
|
1203
|
+
|
|
1204
|
+
Ou via Laravel :
|
|
1205
|
+
|
|
1206
|
+
```php
|
|
1207
|
+
Schema::table('app_accesses', function (Blueprint $table) {
|
|
1208
|
+
$table->string('avatar')->nullable()->after('status');
|
|
1209
|
+
});
|
|
1210
|
+
```
|
|
1211
|
+
|
|
1212
|
+
### Pré-remplissage
|
|
1213
|
+
|
|
1214
|
+
Lorsqu'un `AppAccess` est créé (inscription, grant-access), le champ `avatar` est automatiquement pré-rempli avec l'image globale du user (`user.image`).
|
|
1215
|
+
|
|
1216
|
+
### `POST /api/iam/update-avatar`
|
|
1217
|
+
|
|
1218
|
+
Met à jour l'avatar d'un utilisateur **pour une application spécifique**.
|
|
1219
|
+
|
|
1220
|
+
> ⚠️ **Server-to-Server uniquement** — Requiert `app_key` + `secret_key`.
|
|
1221
|
+
|
|
1222
|
+
**Paramètres requis :**
|
|
1223
|
+
|
|
1224
|
+
```json
|
|
1225
|
+
{
|
|
1226
|
+
"app_key": "oiam_ak_xxxxxxxxxxxxxxxxxxxxxxxx",
|
|
1227
|
+
"secret_key": "oiam_sk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
|
|
1228
|
+
"alias_reference": "ALI-XXXXXXXX",
|
|
1229
|
+
"avatar_url": "https://example.com/avatars/user123.jpg"
|
|
1230
|
+
}
|
|
1231
|
+
```
|
|
1232
|
+
|
|
1233
|
+
| Champ | Type | Description |
|
|
1234
|
+
|-------|------|-------------|
|
|
1235
|
+
| `app_key` | `string` | Clé publique de l'application IAM |
|
|
1236
|
+
| `secret_key` | `string` | Clé secrète de l'application IAM |
|
|
1237
|
+
| `alias_reference` | `string` | Référence de l'alias utilisateur |
|
|
1238
|
+
| `avatar_url` | `string` | URL de la nouvelle image avatar |
|
|
1239
|
+
|
|
1240
|
+
**Réponse succès (200) :**
|
|
1241
|
+
|
|
1242
|
+
```json
|
|
1243
|
+
{
|
|
1244
|
+
"success": true,
|
|
1245
|
+
"message": "Avatar mis à jour",
|
|
1246
|
+
"user_reference": "USR-XXXXXXXX",
|
|
1247
|
+
"alias_reference": "ALI-XXXXXXXX",
|
|
1248
|
+
"user_infos": {
|
|
1249
|
+
"name": "John Doe",
|
|
1250
|
+
"email": "john@example.com",
|
|
1251
|
+
"ccphone": "+221",
|
|
1252
|
+
"phone": "771234567",
|
|
1253
|
+
"address": null,
|
|
1254
|
+
"town": "Dakar",
|
|
1255
|
+
"country": "SN",
|
|
1256
|
+
"image_url": "https://example.com/avatars/user123.jpg",
|
|
1257
|
+
"auth_2fa": false
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
```
|
|
1261
|
+
|
|
1262
|
+
**Codes d'erreur :**
|
|
1263
|
+
|
|
1264
|
+
| Code HTTP | Description |
|
|
1265
|
+
|-----------|-------------|
|
|
1266
|
+
| 403 | `app_key` ou `secret_key` invalide |
|
|
1267
|
+
| 404 | `alias_reference` introuvable ou pas d'accès à cette app |
|
|
1268
|
+
| 422 | Champs manquants ou format invalide |
|
|
1269
|
+
|
|
1270
|
+
**Exemple Node.js (backend) :**
|
|
1271
|
+
|
|
1272
|
+
```js
|
|
1273
|
+
import { iamAccountService } from '@ollaid/native-sso';
|
|
1274
|
+
|
|
1275
|
+
const result = await iamAccountService.updateAvatar({
|
|
1276
|
+
app_key: process.env.IAM_APP_KEY,
|
|
1277
|
+
secret_key: process.env.IAM_SECRET_KEY,
|
|
1278
|
+
alias_reference: 'ALI-XXXXXXXX',
|
|
1279
|
+
avatar_url: 'https://example.com/avatars/user123.jpg',
|
|
1280
|
+
});
|
|
1281
|
+
|
|
1282
|
+
if (result.success) {
|
|
1283
|
+
console.log('Avatar mis à jour :', result.user_infos.image_url);
|
|
1284
|
+
}
|
|
1285
|
+
```
|
|
1286
|
+
|
|
1287
|
+
### `POST /api/iam/reset-avatar`
|
|
1288
|
+
|
|
1289
|
+
Réinitialise l'avatar d'un utilisateur pour une application, le remettant à `null`. La cascade retombera sur `user.image` ou `no-image.png`.
|
|
1290
|
+
|
|
1291
|
+
> ⚠️ **Server-to-Server uniquement** — Requiert `app_key` + `secret_key`.
|
|
1292
|
+
|
|
1293
|
+
**Paramètres requis :**
|
|
1294
|
+
|
|
1295
|
+
```json
|
|
1296
|
+
{
|
|
1297
|
+
"app_key": "oiam_ak_xxxxxxxxxxxxxxxxxxxxxxxx",
|
|
1298
|
+
"secret_key": "oiam_sk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
|
|
1299
|
+
"alias_reference": "ALI-XXXXXXXX"
|
|
1300
|
+
}
|
|
1301
|
+
```
|
|
1302
|
+
|
|
1303
|
+
| Champ | Type | Description |
|
|
1304
|
+
|-------|------|-------------|
|
|
1305
|
+
| `app_key` | `string` | Clé publique de l'application IAM |
|
|
1306
|
+
| `secret_key` | `string` | Clé secrète de l'application IAM |
|
|
1307
|
+
| `alias_reference` | `string` | Référence de l'alias utilisateur |
|
|
1308
|
+
|
|
1309
|
+
**Réponse succès (200) :**
|
|
1310
|
+
|
|
1311
|
+
```json
|
|
1312
|
+
{
|
|
1313
|
+
"success": true,
|
|
1314
|
+
"message": "Avatar réinitialisé",
|
|
1315
|
+
"user_reference": "USR-XXXXXXXX",
|
|
1316
|
+
"alias_reference": "ALI-XXXXXXXX",
|
|
1317
|
+
"user_infos": {
|
|
1318
|
+
"name": "John Doe",
|
|
1319
|
+
"email": "john@example.com",
|
|
1320
|
+
"image_url": "https://iam.example.com/storage/users/image.jpg"
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
```
|
|
1324
|
+
|
|
1325
|
+
**Codes d'erreur :**
|
|
1326
|
+
|
|
1327
|
+
| Code HTTP | Description |
|
|
1328
|
+
|-----------|-------------|
|
|
1329
|
+
| 403 | `app_key` ou `secret_key` invalide |
|
|
1330
|
+
| 404 | `alias_reference` introuvable ou pas d'accès à cette app |
|
|
1331
|
+
| 422 | Champs manquants |
|
|
1332
|
+
|
|
1333
|
+
**Exemple Node.js (backend) :**
|
|
1334
|
+
|
|
1335
|
+
```js
|
|
1336
|
+
import { iamAccountService } from '@ollaid/native-sso';
|
|
1337
|
+
|
|
1338
|
+
const result = await iamAccountService.resetAvatar({
|
|
1339
|
+
app_key: process.env.IAM_APP_KEY,
|
|
1340
|
+
secret_key: process.env.IAM_SECRET_KEY,
|
|
1341
|
+
alias_reference: 'ALI-XXXXXXXX',
|
|
1342
|
+
});
|
|
1343
|
+
|
|
1344
|
+
if (result.success) {
|
|
1345
|
+
console.log('Avatar réinitialisé, image actuelle :', result.user_infos.image_url);
|
|
1346
|
+
}
|
|
1347
|
+
```
|
|
1348
|
+
|
|
1349
|
+
---
|
|
1350
|
+
|
|
1351
|
+
### Format `user_infos` (9 champs standardisés)
|
|
1352
|
+
|
|
1353
|
+
Toutes les APIs IAM retournent le même objet `user_infos` avec exactement **9 champs** :
|
|
1354
|
+
|
|
1355
|
+
| Champ | Type | Description |
|
|
1356
|
+
|-------|------|-------------|
|
|
1357
|
+
| `name` | `string` | Nom complet de l'utilisateur |
|
|
1358
|
+
| `email` | `string \| null` | Adresse email (null si compte phone-only) |
|
|
1359
|
+
| `ccphone` | `string \| null` | Indicatif téléphonique (ex: `+221`) |
|
|
1360
|
+
| `phone` | `string \| null` | Numéro de téléphone sans indicatif |
|
|
1361
|
+
| `address` | `string \| null` | Adresse postale |
|
|
1362
|
+
| `town` | `string \| null` | Ville |
|
|
1363
|
+
| `country` | `string \| null` | Code pays ISO (ex: `SN`, `FR`) |
|
|
1364
|
+
| `image_url` | `string \| null` | URL de l'avatar (résolu via la cascade app_access → user → fallback) |
|
|
1365
|
+
| `auth_2fa` | `boolean` | Indique si la 2FA est activée |
|
|
1366
|
+
|
|
1367
|
+
> **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`.
|
|
1368
|
+
|
|
1369
|
+
---
|
|
1370
|
+
|
|
1371
|
+
## Session & localStorage
|
|
1372
|
+
|
|
1373
|
+
Le package utilise **4 clés** dans `localStorage` pour persister la session :
|
|
1374
|
+
|
|
1375
|
+
| Clé | Contenu | Source | Valeurs possibles |
|
|
1376
|
+
|-----|---------|--------|-------------------|
|
|
1377
|
+
| `token` | Token Sanctum (bearer) | Réponse de `/api/native/exchange` | Chaîne `"1\|abc123..."` |
|
|
1378
|
+
| `auth_token` | Copie du token (compatibilité) | Même source que `token` | Idem |
|
|
1379
|
+
| `user` | Objet `user_infos` sérialisé en JSON | Réponse de `/api/native/exchange` ou mise à jour via health check | `{"name":"...","email":"...",...}` |
|
|
1380
|
+
| `account_type` | Type de compte | Déterminé lors du `exchange` selon le mode d'inscription | `"user"` (défaut) ou `"client"` (inscription phone-only) |
|
|
1381
|
+
|
|
1382
|
+
> **Note :** `account_type` est stocké **séparément** de l'objet `user` — il n'est pas inclus dans le JSON de la clé `user`.
|
|
1383
|
+
|
|
1384
|
+
### Nettoyage
|
|
1385
|
+
|
|
1386
|
+
Lors du `logout()`, les 4 clés sont supprimées via `clearAuthToken()`.
|
|
1387
|
+
|
|
1388
|
+
### Accès programmatique
|
|
1389
|
+
|
|
1390
|
+
```ts
|
|
1391
|
+
import { getAuthToken, getAuthUser, getAccountType } from '@ollaid/native-sso';
|
|
1392
|
+
|
|
1393
|
+
const token = getAuthToken(); // string | null
|
|
1394
|
+
const user = getAuthUser<UserInfos>(); // UserInfos | null
|
|
1395
|
+
const type = getAccountType(); // string | null
|
|
1396
|
+
```
|
|
1397
|
+
|
|
1398
|
+
---
|
|
1399
|
+
|
|
1400
|
+
## OnboardingModal
|
|
1401
|
+
|
|
1402
|
+
Modal post-connexion qui invite l'utilisateur à compléter les informations manquantes de son profil.
|
|
1403
|
+
|
|
1404
|
+
### Props
|
|
1405
|
+
|
|
1406
|
+
| Prop | Type | Requis | Description |
|
|
1407
|
+
|------|------|--------|-------------|
|
|
1408
|
+
| `open` | `boolean` | ✅ | Contrôle l'ouverture de la modal |
|
|
1409
|
+
| `onOpenChange` | `(open: boolean) => void` | ✅ | Callback changement d'état |
|
|
1410
|
+
| `user` | `NativeUser` | ✅ | Objet utilisateur courant (pour détecter les champs manquants) |
|
|
1411
|
+
| `onComplete` | `(data) => void` | ✅ | Callback avec les données saisies |
|
|
1412
|
+
| `onSkip` | `() => void` | ✅ | Callback si l'utilisateur passe l'étape |
|
|
1413
|
+
|
|
1414
|
+
### Champs affichés
|
|
1415
|
+
|
|
1416
|
+
La modal affiche **uniquement** les champs manquants :
|
|
1417
|
+
- **Photo de profil** — si `user.image_url` est vide (max 2 Mo, JPG/PNG)
|
|
1418
|
+
- **Numéro de téléphone** — si `user.phone` est vide
|
|
1419
|
+
- **Adresse email** — si `user.email` est vide (**optionnel**, ne bloque pas la validation)
|
|
1420
|
+
|
|
1421
|
+
### Callback `onComplete`
|
|
1422
|
+
|
|
1423
|
+
```ts
|
|
1424
|
+
onComplete: (data: {
|
|
1425
|
+
image_url?: string; // Base64 de la photo (si ajoutée)
|
|
1426
|
+
ccphone?: string; // Indicatif (si téléphone ajouté)
|
|
1427
|
+
phone?: string; // Numéro (si téléphone ajouté)
|
|
1428
|
+
email?: string; // Email (si renseigné — optionnel)
|
|
1429
|
+
}) => void;
|
|
1430
|
+
```
|
|
1431
|
+
|
|
1432
|
+
### Condition de soumission
|
|
1433
|
+
|
|
1434
|
+
L'utilisateur **doit** :
|
|
1435
|
+
1. Cocher la case de confirmation
|
|
1436
|
+
2. Fournir une photo (si manquante)
|
|
1437
|
+
3. Fournir un téléphone valide (si manquant)
|
|
1438
|
+
|
|
1439
|
+
L'email est **toujours optionnel** — il n'est pas requis pour valider.
|
|
1440
|
+
|
|
1441
|
+
### Exemple
|
|
1442
|
+
|
|
1443
|
+
```tsx
|
|
1444
|
+
import { OnboardingModal } from '@ollaid/native-sso';
|
|
1445
|
+
|
|
1446
|
+
<OnboardingModal
|
|
1447
|
+
open={showOnboarding}
|
|
1448
|
+
onOpenChange={setShowOnboarding}
|
|
1449
|
+
user={currentUser}
|
|
1450
|
+
onComplete={async (data) => {
|
|
1451
|
+
// Envoyer les données au backend pour mise à jour
|
|
1452
|
+
await api.updateProfile(data);
|
|
1453
|
+
setShowOnboarding(false);
|
|
1454
|
+
}}
|
|
1455
|
+
onSkip={() => setShowOnboarding(false)}
|
|
1456
|
+
/>
|
|
1457
|
+
```
|
|
1458
|
+
|
|
1459
|
+
---
|
|
1460
|
+
|
|
1461
|
+
## useTokenHealthCheck
|
|
1462
|
+
|
|
1463
|
+
Hook qui vérifie périodiquement la validité du token Sanctum via `POST /api/native/check-token`.
|
|
1464
|
+
|
|
1465
|
+
### Timing
|
|
1466
|
+
|
|
1467
|
+
| Événement | Délai |
|
|
1468
|
+
|-----------|-------|
|
|
1469
|
+
| Premier check après login | **2 minutes** |
|
|
1470
|
+
| Checks suivants | **toutes les 5 minutes** |
|
|
1471
|
+
| Requêtes par heure | ~12 |
|
|
1472
|
+
|
|
1473
|
+
### Comportement réseau
|
|
1474
|
+
|
|
1475
|
+
| Situation | Action |
|
|
1476
|
+
|-----------|--------|
|
|
1477
|
+
| ✅ 200 OK | Met à jour `user_infos` en localStorage |
|
|
1478
|
+
| ❌ 401 Unauthorized | Déconnecte l'utilisateur (token expiré/révoqué) |
|
|
1479
|
+
| ⚠️ Erreur réseau / timeout / 500 | **Aucune action** — la session est conservée |
|
|
1480
|
+
| 📴 Appareil hors ligne | Le check échoue silencieusement, session conservée |
|
|
1481
|
+
|
|
1482
|
+
> **Philosophie** : Ne déconnecter que sur un rejet explicite du serveur (401). Jamais sur un problème réseau.
|
|
1483
|
+
|
|
1484
|
+
### Callbacks
|
|
1485
|
+
|
|
1486
|
+
```ts
|
|
1487
|
+
useTokenHealthCheck({
|
|
1488
|
+
enabled: true, // Activer/désactiver
|
|
1489
|
+
onUserUpdate: (user) => {
|
|
1490
|
+
// Appelé quand user_infos sont rafraîchies
|
|
1491
|
+
},
|
|
1492
|
+
onSessionExpired: () => {
|
|
1493
|
+
// Appelé sur 401 — rediriger vers login
|
|
1494
|
+
navigate('/auth/sso');
|
|
1495
|
+
},
|
|
1496
|
+
});
|
|
1497
|
+
```
|
|
1498
|
+
|
|
1499
|
+
---
|
|
1500
|
+
|
|
1501
|
+
## Sécurité
|
|
1502
|
+
|
|
1503
|
+
### Credentials protégés par chiffrement — Opaque Token
|
|
1504
|
+
|
|
1505
|
+
L'architecture utilise un **token opaque** (`encrypted_credentials`) pour protéger les clés API :
|
|
1506
|
+
|
|
1507
|
+
1. Le backend SaaS chiffre `app_key + secret_key + timestamp` en AES-256-CBC avec `SHA-256(secret_key)` comme clé
|
|
1508
|
+
2. Le frontend reçoit un **blob opaque** + l'`app_key` en clair (non sensible)
|
|
1509
|
+
3. Le frontend transporte le blob + `app_key` vers l'IAM
|
|
1510
|
+
4. L'IAM retrouve la `secret_key` via l'`app_key` dans sa table `applications`, déchiffre le blob et valide
|
|
1511
|
+
|
|
1512
|
+
**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.
|
|
1513
|
+
|
|
1514
|
+
### APIs S2S — Backend uniquement
|
|
1515
|
+
|
|
1516
|
+
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.
|
|
1517
|
+
|
|
1518
|
+
> ⚠️ **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.
|
|
1519
|
+
|
|
1520
|
+
```ts
|
|
1521
|
+
// ✅ CORRECT — dans un contrôleur Node.js / route API backend
|
|
1522
|
+
import { iamAccountService } from '@ollaid/native-sso';
|
|
1523
|
+
await iamAccountService.linkPhone({ ... });
|
|
1524
|
+
|
|
1525
|
+
// ❌ INTERDIT — dans un composant React ou du code frontend
|
|
1526
|
+
// La secret_key serait visible dans le navigateur
|
|
1527
|
+
```
|
|
1528
|
+
|
|
1529
|
+
### Champs de debug en production
|
|
1530
|
+
|
|
1531
|
+
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.
|
|
1532
|
+
|
|
1533
|
+
```php
|
|
1534
|
+
// ✅ Laravel — ne retourner les champs debug que si IAM_DEBUG=true
|
|
1535
|
+
if (config('services.iam.debug')) {
|
|
1536
|
+
$response['debug_error'] = $exception->getMessage();
|
|
1537
|
+
$response['debug_file'] = $exception->getFile();
|
|
1538
|
+
}
|
|
1539
|
+
// ⚠️ Si IAM_DEBUG est absent/null → considéré comme false (production)
|
|
1540
|
+
```
|
|
1541
|
+
|
|
1542
|
+
### Bonnes pratiques
|
|
1543
|
+
|
|
1544
|
+
- ✅ Le token Sanctum est stocké en `localStorage` (standard pour SPA)
|
|
1545
|
+
- ✅ Les credentials IAM sont gardés **en mémoire uniquement** (jamais persistés)
|
|
1546
|
+
- ✅ Le device ID est un identifiant aléatoire (pas de fingerprinting invasif)
|
|
1547
|
+
- ✅ Le logout est single-session (ne déconnecte pas les autres appareils)
|
|
1548
|
+
- ✅ Le health check ne déconnecte que sur 401 explicite
|
|
1549
|
+
- ✅ Les photos sont validées à 2 Mo max côté client
|
|
1550
|
+
- ✅ Les lectures `localStorage` sont protégées par try/catch
|
|
1551
|
+
|
|
1552
|
+
---
|
|
1553
|
+
|
|
1554
|
+
## Exports
|
|
1555
|
+
|
|
1556
|
+
### Composants
|
|
1557
|
+
- `NativeSSOPage` — Page complète autonome (recommandé)
|
|
1558
|
+
- `LoginModal` — Modal de connexion
|
|
1559
|
+
- `SignupModal` — Modal d'inscription
|
|
1560
|
+
- `PasswordRecoveryModal` — Modal de récupération
|
|
1561
|
+
- `OTPInput` — Input OTP 6 chiffres
|
|
1562
|
+
- `PhoneInput` — Input téléphone avec indicatif
|
|
1563
|
+
- `AppsLogoSlider` — Slider logos applications
|
|
1564
|
+
|
|
1565
|
+
### Hooks
|
|
1566
|
+
- `useNativeAuth` — Hook principal d'authentification
|
|
1567
|
+
- `useMobilePassword` — Hook récupération mot de passe
|
|
1568
|
+
- `useMobileRegistration` — Hook inscription
|
|
1569
|
+
- `useTokenHealthCheck` — Hook vérification périodique du token
|
|
1570
|
+
|
|
1571
|
+
### Provider
|
|
1572
|
+
- `NativeSSOProvider` — Provider React pour configuration centralisée
|
|
1573
|
+
- `useNativeSSOConfig` — Hook pour accéder à la config
|
|
1574
|
+
|
|
1575
|
+
### Services
|
|
1576
|
+
- `nativeAuthService` — Service d'authentification
|
|
1577
|
+
- `mobilePasswordService` — Service mot de passe
|
|
1578
|
+
- `setNativeAuthConfig` — Configuration manuelle des URLs
|
|
1579
|
+
- `iamAccountService` — Service APIs IAM Account (link-phone, link-email, refresh-user-info, update-avatar, reset-avatar)
|
|
1580
|
+
- `getAuthToken` — Récupérer le token depuis localStorage
|
|
1581
|
+
- `getAuthUser` — Récupérer l'utilisateur depuis localStorage
|
|
1582
|
+
- `getAccountType` — Récupérer le type de compte depuis localStorage
|
|
1583
|
+
|
|
1584
|
+
### Types
|
|
1585
|
+
- `UserInfos`, `NativeAuthState`, `NativeAuthStatus`, `NativeCredentials`, etc.
|
|
1586
|
+
- `LinkPhoneRequest`, `LinkPhoneResponse` — Types pour l'API link-phone
|
|
1587
|
+
- `LinkEmailRequest`, `LinkEmailResponse` — Types pour l'API link-email
|
|
1588
|
+
- `RefreshUserInfoSingleRequest`, `RefreshUserInfoSingleResponse` — Types pour refresh single
|
|
1589
|
+
- `RefreshUserInfoBulkRequest`, `RefreshUserInfoBulkResponse` — Types pour refresh bulk
|
|
1590
|
+
- `UpdateAvatarRequest`, `UpdateAvatarResponse` — Types pour l'API update-avatar
|
|
1591
|
+
- `ResetAvatarRequest`, `ResetAvatarResponse` — Types pour l'API reset-avatar
|
|
1592
|
+
|
|
1593
|
+
---
|
|
1594
|
+
|
|
1595
|
+
## Publication & Installation npm
|
|
1596
|
+
|
|
1597
|
+
### Prérequis
|
|
1598
|
+
|
|
1599
|
+
- **Node.js** ≥ 18 et **npm** ≥ 9
|
|
1600
|
+
- Un compte [npmjs.com](https://www.npmjs.com/) connecté : `npm login`
|
|
1601
|
+
- Être membre de l'organisation **@ollaid** sur npm
|
|
1602
|
+
|
|
1603
|
+
### Build
|
|
1604
|
+
|
|
1605
|
+
```bash
|
|
1606
|
+
cd packages/ollaid-native-sso
|
|
1607
|
+
npm run build
|
|
1608
|
+
```
|
|
1609
|
+
|
|
1610
|
+
### Versioning
|
|
1611
|
+
|
|
1612
|
+
Avant chaque publication, incrémenter la version selon [semver](https://semver.org/) :
|
|
1613
|
+
|
|
1614
|
+
```bash
|
|
1615
|
+
# Correction de bug (1.0.0 → 1.0.1)
|
|
1616
|
+
npm version patch
|
|
1617
|
+
|
|
1618
|
+
# Nouvelle fonctionnalité rétro-compatible (1.0.1 → 1.1.0)
|
|
1619
|
+
npm version minor
|
|
1620
|
+
|
|
1621
|
+
# Breaking change (1.1.0 → 2.0.0)
|
|
1622
|
+
npm version major
|
|
1623
|
+
```
|
|
1624
|
+
|
|
1625
|
+
### Publication
|
|
1626
|
+
|
|
1627
|
+
Les scoped packages (`@ollaid/*`) sont **privés par défaut** sur npm.
|
|
1628
|
+
Le flag `--access public` est **obligatoire** pour publier en accès libre :
|
|
1629
|
+
|
|
1630
|
+
```bash
|
|
1631
|
+
npm publish --access public
|
|
1632
|
+
```
|
|
1633
|
+
|
|
1634
|
+
> **Astuce** : Pour ne plus avoir à passer le flag à chaque fois, ajoutez dans le `package.json` du package :
|
|
1635
|
+
> ```json
|
|
1636
|
+
> "publishConfig": {
|
|
1637
|
+
> "access": "public"
|
|
1638
|
+
> }
|
|
1639
|
+
> ```
|
|
1640
|
+
|
|
1641
|
+
### Installation côté client
|
|
1642
|
+
|
|
1643
|
+
Dans le projet qui consomme le package :
|
|
1644
|
+
|
|
1645
|
+
```bash
|
|
1646
|
+
npm install @ollaid/native-sso
|
|
1647
|
+
```
|
|
1648
|
+
|
|
1649
|
+
### Vérification
|
|
1650
|
+
|
|
1651
|
+
```bash
|
|
1652
|
+
# Voir les infos du package publié
|
|
1653
|
+
npm info @ollaid/native-sso
|
|
1654
|
+
|
|
1655
|
+
# Voir toutes les versions publiées
|
|
1656
|
+
npm info @ollaid/native-sso versions
|
|
1657
|
+
```
|
|
1658
|
+
|
|
1659
|
+
### Mise à jour
|
|
1660
|
+
|
|
1661
|
+
```bash
|
|
1662
|
+
# Mettre à jour vers la dernière version
|
|
1663
|
+
npm update @ollaid/native-sso
|
|
1664
|
+
|
|
1665
|
+
# Ou forcer une version spécifique
|
|
1666
|
+
npm install @ollaid/native-sso@1.0.0
|
|
1667
|
+
```
|
|
1668
|
+
|
|
1669
|
+
### Workflow complet de publication
|
|
1670
|
+
|
|
1671
|
+
```bash
|
|
1672
|
+
cd packages/ollaid-native-sso
|
|
1673
|
+
npm run build # 1. Build
|
|
1674
|
+
npm version patch # 2. Incrémenter la version
|
|
1675
|
+
npm publish --access public # 3. Publier sur npm
|
|
1676
|
+
```
|
|
1677
|
+
|
|
1678
|
+
---
|
|
1679
|
+
|
|
1680
|
+
## Licence
|
|
1681
|
+
|
|
1682
|
+
Propriétaire — Ollaid © 2026
|