@ollaid/native-sso
v2.1.5
Published
Package NPM fullstack pour l'authentification Native SSO Ollaid - Frontend-First
Maintainers
Readme
@ollaid/native-sso
Package NPM Frontend-First pour l'authentification Native SSO Ollaid.
Un npm install et une route — c'est tout.
Table des matières
- Installation
- Intégration rapide (3 étapes)
- Props de NativeSSOPage
- Usage avancé (composants individuels)
- Props des composants individuels
- Gestion des conflits d'inscription
- Hook useMobileRegistration
- Backend SaaS — Endpoints requis
- APIs IAM Account (Server-to-Server)
- Avatar par application
- Réponses d'erreur
- Configuration .env Laravel
- Migration Laravel
- Flux d'authentification
- Session & localStorage
- Déconnexion synchronisée
- OnboardingModal
- useTokenHealthCheck
- Sécurité
- Exports
- Publication & Installation npm
Installation
npm install @ollaid/native-ssoIntégration rapide (3 étapes)
1. Installer le package
npm install @ollaid/native-sso2. Ajouter la route dans App.tsx
import { NativeSSOPage } from '@ollaid/native-sso';
import { Route, Routes, useNavigate } from 'react-router-dom';
function App() {
const navigate = useNavigate();
return (
<Routes>
<Route
path="/auth/sso"
element={
<NativeSSOPage
saasApiUrl="https://mon-saas.com/api"
iamApiUrl="https://identityam.ollaid.com/api"
accountType="user"
onLoginSuccess={(token, user) => {
console.log('Connecté !', user.name);
navigate('/dashboard');
}}
onLogout={() => navigate('/auth/sso')}
/>
}
/>
{/* ... autres routes */}
</Routes>
);
}3. C'est tout ✅
La page /auth/sso gère automatiquement :
- ✅ Connexion par email (mot de passe + OTP)
- ✅ Connexion par téléphone (SMS OTP)
- ✅ Connexion par code d'accès
- ✅ Inscription complète (email ou téléphone uniquement 🇸🇳)
- ✅ Récupération de mot de passe
- ✅ Grant access (inscription auto à une nouvelle app)
- ✅ 2FA (TOTP)
- ✅ Session persistée en localStorage
- ✅ Branding Ollaid SSO
Props de NativeSSOPage
| Prop | Type | Requis | Description |
|------|------|--------|-------------|
| saasApiUrl | string | ✅ | URL du backend SaaS (ex: https://mon-saas.com/api) |
| iamApiUrl | string | ✅ | URL du backend IAM (ex: https://identityam.ollaid.com/api) |
| accountType | 'user' \| 'client' | ❌ | Type de compte à persister dans localStorage (défaut: 'user'). Utile si vous avez plusieurs pages SSO avec des rôles différents. |
| 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. |
| onLoginSuccess | (token: string, user: UserInfos) => void | ❌ | Callback après connexion réussie |
| onLogout | () => void | ❌ | Callback après déconnexion |
| title | string | ❌ | Titre personnalisé (défaut: "Un compte, plusieurs accès") |
| description | string | ❌ | Description personnalisée |
| logoUrl | string | ❌ | URL du logo (remplace le slider) |
| hideFooter | boolean | ❌ | Masquer "Propulsé par iam.ollaid.com" |
| onOnboardingComplete | (data: { image_url?: string; ccphone?: string; phone?: string }) => void | ❌ | Callback après complétion de l'onboarding |
| redirectAfterLogin | string | ❌ | Route vers laquelle rediriger après connexion réussie (ex: /client/dashboard). Utilise window.location.href. Compatible avec ou sans react-router. |
| redirectAfterLogout | string | ❌ | Route vers laquelle rediriger après déconnexion (ex: /auth/client). Utilise window.location.href. |
Note : Le mode
debugest contrôlé uniquement par le backend via la variable d'environnementIAM_DEBUGdans le.envdu SaaS. Il n'y a plus de propdebugà passer au composant. LeDebugPanelest réactif : il apparaît automatiquement après le chargement des credentials sidebug: trueest retourné par le backend.
Redirections automatiques (optionnel)
Ces props sont entièrement optionnels. Si vous ne les définissez pas, c'est votre backend SaaS qui gère la redirection après l'exchange — il connaît déjà le
accountTypeet peut rediriger l'utilisateur vers la bonne page après avoir sauvegardé le token dans le localStorage.
Utilisez ces props uniquement si vous souhaitez que le composant lui-même déclenche la redirection côté frontend :
<NativeSSOPage
saasApiUrl="https://votre-saas.com/api"
iamApiUrl="https://identityam.ollaid.com/api"
configPrefix="iam_client"
accountType="client"
redirectAfterLogin="/client/dashboard"
redirectAfterLogout="/auth/client"
/>Sans ces props (usage minimal, le SaaS gère la redirection) :
<NativeSSOPage
saasApiUrl="https://votre-saas.com/api"
iamApiUrl="https://identityam.ollaid.com/api"
configPrefix="iam_client"
accountType="client"
onLoginSuccess={(token, user) => { /* votre logique */ }}
/>Comportement :
- Si
redirectAfterLoginest défini → redirection viawindow.location.hrefaprès connexion - Si
onLoginSuccessest aussi défini → le callback est appelé avant la redirection - Si aucun prop de redirection n'est défini → aucune redirection automatique, comportement inchangé
Déconnexion externe (sans le composant)
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.
⚠️ RÈGLE OBLIGATOIRE : Toute déconnexion frontend DOIT passer par
logout(). Ne jamais effacer lelocalStoragemanuellement ni utiliserclearAuthToken()seul — cela laisserait des sessions orphelines actives sur l'IAM.
Utilisation
import { logout } from '@ollaid/native-sso';
const handleLogout = async () => {
await logout(); // ✅ Double revocation (SaaS + IAM) + nettoyage localStorage
navigate('/auth/login');
};Que fait logout() ?
- Révoque le token SaaS —
POST /api/native/logout(supprime le Sanctum token) - Révoque la session IAM —
POST /api/iam/disconnect(avecsanctum_token+app_access_token_ref) - Nettoie le localStorage — supprime les 6 clés du package
Les appels réseau sont en Promise.allSettled (best-effort) : même si le serveur est injoignable, le localStorage est toujours nettoyé.
Clés localStorage nettoyées
| Clé | Description |
|-----|-------------|
| auth_token | Token Sanctum actif |
| token | Token legacy |
| user | Objet utilisateur (avec iam_reference, alias_reference) |
| account_type | Type de compte (user ou client) |
| alias_reference | Référence de l'alias de connexion |
| app_access_token_ref | Référence de l'AppAccessToken IAM (pour revocation optimisée) |
⛔ clearAuthToken() est déprécié
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.
// ❌ NE PAS FAIRE — sessions orphelines sur l'IAM
import { clearAuthToken } from '@ollaid/native-sso';
clearAuthToken();
// ✅ FAIRE — double revocation garantie
import { logout } from '@ollaid/native-sso';
await logout();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.
Multi-Tenant (plusieurs applications sur le même backend)
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.
Principe
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.
Frontend (configPrefix="iam_vendor")
→ GET /api/native/config [Header: X-IAM-Config-Prefix: iam_vendor]
Backend SaaS:
$prefix = $request->header('X-IAM-Config-Prefix', 'iam');
$appKey = config("services.{$prefix}.app_key"); // ← résolution dynamiqueCôté Frontend
{/* Page login principale */}
<NativeSSOPage
saasApiUrl="https://votre-saas.com/api"
iamApiUrl="https://identityam.ollaid.com/api"
configPrefix="iam"
accountType="user"
redirectAfterLogin="/dashboard"
redirectAfterLogout="/auth/sso"
/>
{/* Page login espace vendeur */}
<NativeSSOPage
saasApiUrl="https://votre-saas.com/api"
iamApiUrl="https://identityam.ollaid.com/api"
configPrefix="iam_vendor"
accountType="client"
redirectAfterLogin="/vendor/dashboard"
redirectAfterLogout="/auth/vendor"
/>
{/* Page login admin — même backend, app IAM différente */}
<NativeSSOPage
saasApiUrl="https://votre-saas.com/api"
iamApiUrl="https://identityam.ollaid.com/api"
configPrefix="iam_admin"
accountType="user"
redirectAfterLogin="/admin/dashboard"
redirectAfterLogout="/auth/admin"
/>Côté Backend SaaS — .env
Le préfixe .env correspond au configPrefix en UPPER_CASE. Ajoutez autant de blocs que nécessaire :
# ===== Serveur IAM (partagé) =====
IAM_API_URL=https://identityam.ollaid.com/api
IAM_AUTH_URL=https://iam.ollaid.com
# ===== Préfixe "iam" (application principale) =====
IAM_APP_KEY=oiam_ak_xxx
IAM_PUBLIC_KEY=oiam_pk_xxx
IAM_SECRET_KEY=oiam_sk_xxx
IAM_WEBHOOK_SECRET=oiam_whsec_xxx
IAM_DEBUG=true
# ===== Préfixe "iam_vendor" (espace vendeur/shop) =====
IAM_VENDOR_APP_KEY=oiam_ak_yyy
IAM_VENDOR_PUBLIC_KEY=oiam_pk_yyy
IAM_VENDOR_SECRET_KEY=oiam_sk_yyy
IAM_VENDOR_WEBHOOK_SECRET=oiam_whsec_yyy
IAM_VENDOR_DEBUG=false
# ===== Préfixe "iam_client" (espace client) =====
IAM_CLIENT_APP_KEY=oiam_ak_zzz
IAM_CLIENT_SECRET_KEY=oiam_sk_zzz
IAM_CLIENT_DEBUG=true
# ===== Préfixe "iam_admin" (back-office) =====
IAM_ADMIN_APP_KEY=oiam_ak_aaa
IAM_ADMIN_SECRET_KEY=oiam_sk_aaa
IAM_ADMIN_DEBUG=trueCôté Backend SaaS — config/services.php
return [
'iam' => [
'api_url' => env('IAM_API_URL', 'https://identityam.ollaid.com/api'),
'app_key' => env('IAM_APP_KEY'),
'public_key' => env('IAM_PUBLIC_KEY'),
'secret_key' => env('IAM_SECRET_KEY'),
'debug' => env('IAM_DEBUG', false),
],
'iam_vendor' => [
'api_url' => env('IAM_API_URL'),
'app_key' => env('IAM_VENDOR_APP_KEY'),
'public_key' => env('IAM_VENDOR_PUBLIC_KEY'),
'secret_key' => env('IAM_VENDOR_SECRET_KEY'),
'debug' => env('IAM_VENDOR_DEBUG', false),
],
'iam_client' => [
'api_url' => env('IAM_API_URL'),
'app_key' => env('IAM_CLIENT_APP_KEY'),
'secret_key' => env('IAM_CLIENT_SECRET_KEY'),
'debug' => env('IAM_CLIENT_DEBUG', false),
],
'iam_admin' => [
'api_url' => env('IAM_API_URL'),
'app_key' => env('IAM_ADMIN_APP_KEY'),
'secret_key' => env('IAM_ADMIN_SECRET_KEY'),
'debug' => env('IAM_ADMIN_DEBUG', false),
],
// Ajoutez d'autres blocs selon vos besoins...
];Côté Backend SaaS — Controller multi-tenant
Tous les controllers Native (config, exchange, check-token, logout) doivent lire le header :
class NativeConfigController extends Controller
{
public function getConfig(Request $request): JsonResponse
{
// Multi-tenant : résolution dynamique du préfixe
$prefix = $request->header('X-IAM-Config-Prefix', 'iam');
// Sécurité : valider que le préfixe commence par "iam"
if (!str_starts_with($prefix, 'iam')) {
return response()->json(['success' => false, 'message' => 'Invalid config prefix'], 400);
}
$appKey = config("services.{$prefix}.app_key");
$secretKey = config("services.{$prefix}.secret_key");
$debug = (bool) config("services.{$prefix}.debug", false);
if (!$appKey || !$secretKey) {
return response()->json([
'success' => false,
'message' => "Configuration '{$prefix}' non trouvée",
], 404);
}
// Chiffrement Opaque Token (AES-256-CBC)
$payload = json_encode(['secret_key' => $secretKey, 'ts' => time()]);
$key = hash('sha256', $secretKey, true);
$iv = random_bytes(16);
$encrypted = openssl_encrypt($payload, 'AES-256-CBC', $key, OPENSSL_RAW_DATA, $iv);
$encryptedCredentials = base64_encode($iv . '::' . base64_encode($encrypted));
return response()->json([
'success' => true,
'app_key' => $appKey,
'encrypted_credentials' => $encryptedCredentials,
'iam_api_url' => config("services.{$prefix}.api_url", 'https://identityam.ollaid.com/api'),
'credentials_ttl' => 300,
'debug' => $debug,
]);
}
}⚠️ Important : Appliquez la même logique
X-IAM-Config-Prefixdansexchange,check-tokenetlogout.
Debug Mode — Troubleshooting
Le debug est piloté par le backend : le package n'a aucun prop debug.
Comment ça marche
- Le backend lit
IAM_DEBUG(ouIAM_VENDOR_DEBUG, etc.) depuis.env GET /api/native/configretourne"debug": true- Le package active les logs console + le
DebugPanel - Le
DebugPanelest réactif : il apparaît automatiquement après le chargement des credentials
Checklist de troubleshooting
| Problème | Solution |
|----------|----------|
| DebugPanel n'apparaît pas | Vérifier que GET /api/native/config retourne "debug": true dans la réponse JSON |
| La valeur est toujours false | Vérifier le .env : pas de typo ! (IAM_DEBUG et non IAM_DEBUGL) |
| Changement non pris en compte | Lancer php artisan config:clear && php artisan config:cache |
| Debug actif en production | Mettre IAM_DEBUG=false dans le .env de production |
| Debug marche pour main mais pas vendor | Vérifier IAM_VENDOR_DEBUG=true dans .env + config/services.php |
⚠️ Erreur fréquente : typo dans .env
# ❌ MAUVAIS (typo "DEBUGL" avec un L en trop)
IAM_DEBUGL=true
# ✅ CORRECT
IAM_DEBUG=trueAprès correction, n'oubliez pas :
php artisan config:clear
php artisan config:cacheUsage avancé (composants individuels)
Pour ceux qui veulent plus de contrôle :
import {
NativeSSOProvider,
LoginModal,
SignupModal,
useNativeAuth,
} from '@ollaid/native-sso';
function MyCustomAuth() {
const [showLogin, setShowLogin] = useState(false);
return (
<>
<button onClick={() => setShowLogin(true)}>Se connecter</button>
<LoginModal
open={showLogin}
onOpenChange={setShowLogin}
onSwitchToSignup={() => {}}
onLoginSuccess={(token, user) => console.log('OK', user)}
saasApiUrl="https://mon-saas.com/api"
iamApiUrl="https://identityam.ollaid.com/api"
defaultAccountType="user"
/>
</>
);
}Props des composants individuels
SignupModal
| Prop | Type | Requis | Description |
|------|------|--------|-------------|
| open | boolean | ✅ | Contrôle l'ouverture du modal |
| onOpenChange | (open: boolean) => void | ✅ | Callback de changement d'état |
| onSwitchToLogin | () => void | ✅ | Callback pour basculer vers le login |
| onSignupSuccess | (token: string, user: UserInfos) => void | ✅ | Callback après inscription réussie |
| saasApiUrl | string | ✅ | URL du backend SaaS |
| iamApiUrl | string | ✅ | URL du backend IAM |
| 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'. |
| onSwitchToLoginWithPhone | (phone: string) => void | ❌ | Callback lors d'un conflit phone +221 — permet de basculer vers LoginModal avec le numéro pré-rempli |
LoginModal
| Prop | Type | Requis | Description |
|------|------|--------|-------------|
| open | boolean | ✅ | Contrôle l'ouverture du modal |
| onOpenChange | (open: boolean) => void | ✅ | Callback de changement d'état |
| onSwitchToSignup | () => void | ✅ | Callback pour basculer vers l'inscription |
| onLoginSuccess | (token: string, user: UserInfos) => void | ❌ | Callback après connexion réussie |
| saasApiUrl | string | ✅ | URL du backend SaaS |
| iamApiUrl | string | ✅ | URL du backend IAM |
| loading | boolean | ❌ | État de chargement externe |
| showSwitchToSignup | boolean | ❌ | Afficher le lien "Pas de compte ? S'inscrire" (défaut: true) |
| 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'. |
| 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. |
Gestion des conflits d'inscription
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).
Comportement automatique
- L'utilisateur remplit le formulaire d'inscription et soumet
- Le backend détecte un conflit (
email_existsouphone_exists) et retourne un objetconflict - Le
SignupModalaffiche laConflictViewavec les options disponibles
Affichage intelligent des identifiants
- L'identifiant saisi par l'utilisateur (email ou téléphone) est affiché en clair pour confirmer sa saisie
- 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)
Options proposées
Selon le type de conflit et les capacités du compte existant, la vue propose :
| Option | Condition | Action |
|--------|-----------|--------|
| Se connecter | conflict.options.can_login === true | Bascule vers LoginModal via onSwitchToLogin |
| Se connecter par téléphone | Conflit phone + numéro +221 | Bascule vers LoginModal avec le numéro pré-rempli via onSwitchToLoginWithPhone(phone) |
| Récupérer par email | conflict.options.can_recover_by_email === true | Ouvre le PasswordRecoveryModal |
| Récupérer par SMS | conflict.options.can_recover_by_sms === true | Ouvre le PasswordRecoveryModal |
| Modifier les informations | Toujours disponible | Retour au formulaire pour changer l'identifiant |
Exemple d'intégration avec hand-off téléphone
function AuthPage() {
const [showSignup, setShowSignup] = useState(false);
const [showLogin, setShowLogin] = useState(false);
const [loginPhone, setLoginPhone] = useState<string | undefined>();
return (
<>
<SignupModal
open={showSignup}
onOpenChange={setShowSignup}
onSwitchToLogin={() => { setShowSignup(false); setShowLogin(true); }}
onSwitchToLoginWithPhone={(phone) => {
setLoginPhone(phone);
setShowSignup(false);
setShowLogin(true);
}}
onSignupSuccess={(token, user) => navigate('/dashboard')}
saasApiUrl="https://mon-saas.com/api"
iamApiUrl="https://identityam.ollaid.com/api"
/>
<LoginModal
open={showLogin}
onOpenChange={setShowLogin}
onSwitchToSignup={() => { setShowLogin(false); setShowSignup(true); }}
initialPhone={loginPhone}
saasApiUrl="https://mon-saas.com/api"
iamApiUrl="https://identityam.ollaid.com/api"
/>
</>
);
}Type RegistrationConflict
L'objet retourné par le backend lors d'un conflit :
interface RegistrationConflict {
type: 'email' | 'phone';
masked_identifier: string;
options: {
can_login: boolean;
can_recover_by_email: boolean;
can_recover_by_sms: boolean;
masked_email?: string;
masked_phone?: string;
};
}Hook useMobileRegistration
Hook React pour gérer le flow d'inscription mobile en 3 étapes : init → verify-otp → complete.
Import
import { useMobileRegistration } from '@ollaid/native-sso';Propriétés retournées
État
| Propriété | Type | Description |
|-----------|------|-------------|
| processToken | string \| null | Token de processus d'inscription en cours |
| status | 'idle' \| 'pending_otp' \| 'pending_password' \| 'pending_registration' \| 'completed' | Étape actuelle du flow |
| formData | Partial<MobileRegistrationFormData> | Données du formulaire stockées |
| loading | boolean | Indique si une opération est en cours |
| error | string \| null | Message d'erreur (français, user-friendly) |
| conflict | RegistrationConflict \| null | Objet de conflit si l'identifiant existe déjà |
| isCompleted | boolean | true si l'inscription est terminée |
| hasConflict | boolean | true si un conflit est en cours |
Type de compte (phone-only)
| Propriété | Type | Description |
|-----------|------|-------------|
| accountType | 'email' \| 'phone-only' | Type de compte sélectionné |
| setAccountType | (type: AccountType) => void | Change le type de compte |
| isPhoneOnly | boolean | true si accountType === 'phone-only' |
Méthodes
| Méthode | Signature | Description |
|---------|-----------|-------------|
| updateFormData | (data: Partial<MobileRegistrationFormData>) => void | Met à jour les données du formulaire |
| initRegistration | (data) => Promise<{ success, otp_code_dev?, otp_method?, otp_sent_to? }> | Initialise l'inscription (envoi OTP). Peut retourner un conflit. |
| verifyOtp | (otpCode: string) => Promise<{ success, completed?, callback_token? }> | Vérifie le code OTP |
| completeRegistration | (password: string) => Promise<{ success, callback_token? }> | Finalise avec mot de passe (type email) |
| completePhoneOnlyRegistration | () => Promise<{ success, callback_token? }> | Finalise sans mot de passe (type phone-only, Sénégal 🇸🇳) |
| resendOtp | () => Promise<{ success, cooldown?, otp_code_dev? }> | Renvoie le code OTP |
| reset | () => void | Réinitialise tout le hook |
| clearError | () => void | Efface l'erreur et le conflit |
Backend SaaS — Endpoints requis
Le backend SaaS (Laravel) doit exposer 4 endpoints. Voici les spécifications exactes :
GET /api/native/config
Retourne un token opaque chiffré (encrypted_credentials) contenant les credentials IAM.
⚠️ Les clés app_key et secret_key ne sont JAMAIS exposées au frontend.
Headers requis : aucun
Réponse succès (200) :
{
"success": true,
"app_key": "oiam_ak_xxxxxxxxxxxxxxxxxxxxxxxx",
"encrypted_credentials": "base64_encoded_iv_plus_ciphertext...",
"debug": true // optionnel — active le DebugPanel et les logs console côté frontend
}Principe :
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).
Le frontend transporte le blob opaque + l'app_key en clair (non sensible) vers l'IAM.
L'IAM retrouve la secret_key via l'app_key dans sa table applications, puis déchiffre le blob.
Implémentation Laravel :
// routes/api.php
Route::get('/native/config', function () {
$appKey = config('services.iam.app_key');
$secretKey = config('services.iam.secret_key');
$iamApiUrl = config('services.iam.api_url');
// Clé AES = SHA-256 de la secret_key (32 bytes binaires)
$encryptionKey = hash('sha256', $secretKey, true);
$iv = random_bytes(16);
$payload = json_encode([
'secret_key' => $secretKey,
'ts' => time(),
]);
$encrypted = openssl_encrypt($payload, 'AES-256-CBC', $encryptionKey, OPENSSL_RAW_DATA, $iv);
return response()->json([
'success' => true,
'app_key' => $appKey, // En clair (non sensible, sert d'identifiant)
'encrypted_credentials' => base64_encode($iv . '::' . base64_encode($encrypted)),
'iam_api_url' => $iamApiUrl,
'credentials_ttl' => 300,
'debug' => (bool) config('services.iam.debug'),
]);
});Décryptage côté IAM :
// Dans le endpoint /iam/native/encrypt
// 1. Retrouver la secret_key via l'app_key
$app = Application::where('app_key', $request->app_key)->first();
if (!$app) {
return response()->json(['success' => false, 'message' => 'Application inconnue'], 401);
}
// 2. Déchiffrer avec la secret_key de l'application
$encryptionKey = hash('sha256', $app->secret_key, true);
$decoded = base64_decode($request->encrypted_credentials);
[$iv, $ciphertextB64] = explode('::', $decoded, 2);
$payload = json_decode(
openssl_decrypt(base64_decode($ciphertextB64), 'AES-256-CBC', $encryptionKey, OPENSSL_RAW_DATA, $iv),
true
);
// 3. Vérifier le timestamp (anti-replay, max 5 min)
if (!$payload || time() - $payload['ts'] > 300) {
return response()->json(['success' => false, 'message' => 'Credentials expirés ou invalides'], 401);
}
$secretKey = $payload['secret_key'];
// ... valider et continuer le fluxPOST /api/native/exchange
⚠️ IMPORTANT — Route PUBLIQUE obligatoire
Cette route NE DOIT PAS être derrière le middlewareauth:sanctum.
Si vous la placez dans un groupeRoute::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.
Assurez-vous que/api/native/configet/api/native/exchangesont en dehors de tout groupe authentifié.
Échange le callback_token reçu de l'IAM contre un token Sanctum local.
Headers requis : Content-Type: application/json
Body de la requête :
{
"callback_token": "eyJhbGciOiJIUzI1NiIs..."
}Logique backend :
- Recevoir le
callback_token - Appeler l'IAM
POST /iam/auth/decryptavec les credentials dans les headers (X-IAM-App-Key,X-IAM-Secret-Key) et le token dans le body - L'IAM retourne les
user_infos(9 champs standardisés) +app_access_token_ref - Créer ou mettre à jour l'utilisateur local
- Générer un token Sanctum
- Retourner le token + user
Réponse succès (200) :
{
"success": true,
"token": "1|abc123def456ghi789...",
"expires_at": "2026-04-23T03:12:32.000000Z",
"app_access_token_ref": "aat_ref_XXXXXXXX",
"user": {
"id": 1,
"reference": "USR-XXXXXXXX",
"alias_reference": "ALI-XXXXXXXX",
"name": "John Doe",
"email": "[email protected]",
"phone": "+221771234567",
"ccphone": "+221",
"image_url": "https://identityam.ollaid.com/storage/avatars/xxx.jpg",
"email_verified": true,
"phone_verified": true
}
}Implémentation Laravel :
// routes/api.php
Route::post('/native/exchange', function (Request $request) {
$iamApiUrl = config('services.iam.api_url');
// Appel IAM pour décrypter — credentials dans les HEADERS
$response = Http::withHeaders([
'X-IAM-App-Key' => config('services.iam.app_key'),
'X-IAM-Secret-Key' => config('services.iam.secret_key'),
])->post("{$iamApiUrl}/iam/auth/decrypt", [
'token' => $request->callback_token,
]);
if (!$response->successful() || !$response->json('success')) {
return response()->json([
'success' => false,
'error' => 'Token invalide ou expiré',
'error_type' => 'invalid_token',
], 401);
}
$data = $response->json();
$userInfos = $data['user_infos'];
$appAccessTokenRef = $data['app_access_token_ref'] ?? null;
// Créer ou mettre à jour l'utilisateur local
$user = User::updateOrCreate(
['reference' => $userInfos['reference']],
[
'name' => $userInfos['name'],
'email' => $userInfos['email'],
'phone' => $userInfos['phone'] ?? null,
'ccphone' => $userInfos['ccphone'] ?? null,
'image' => $userInfos['image_url'] ?? null,
'alias_reference' => $userInfos['alias_reference'],
'user_infos' => json_encode($userInfos),
'password' => bcrypt(Str::random(32)),
]
);
// Générer un token Sanctum
$token = $user->createToken('native-sso')->plainTextToken;
return response()->json([
'success' => true,
'token' => $token,
'expires_at' => now()->addDays(30)->toISOString(),
'app_access_token_ref' => $appAccessTokenRef,
'user' => [
'id' => $user->id,
'reference' => $user->reference,
'alias_reference' => $user->alias_reference,
'name' => $user->name,
'email' => $user->email,
'phone' => $user->phone,
'ccphone' => $user->ccphone,
'image_url' => $user->image,
'email_verified' => $userInfos['email_verification'] === 'verified',
'phone_verified' => $userInfos['phone_verification'] === 'verified',
],
]);
});POST /api/native/check-token
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).
Headers requis : Authorization: Bearer {token}
Réponse succès (200) :
{
"status": "connected",
"user": {
"name": "John Doe",
"email": "[email protected]",
"ccphone": "+221",
"phone": "771234567",
"image_url": "https://...",
"town": "Dakar",
"country": "SN",
"auth_2fa": false
}
}Si token invalide/expiré : Sanctum retourne automatiquement 401.
Implémentation Laravel :
// routes/api.php
Route::post('/native/check-token', function (Request $request) {
$user = $request->user();
return response()->json([
'status' => 'connected',
'user' => [
'name' => $user->name,
'email' => $user->email,
'ccphone' => $user->ccphone,
'phone' => $user->phone,
'image_url' => $user->image,
'town' => $user->town ?? null,
'country' => $user->country ?? null,
'auth_2fa' => (bool) ($user->auth_2fa ?? false),
],
]);
})->middleware('auth:sanctum');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.
POST /api/native/logout (single-session)
Invalide uniquement le token Sanctum courant (currentAccessToken()->delete()). Les autres sessions actives de l'utilisateur ne sont pas affectées.
Headers requis : Authorization: Bearer {token}
Réponse succès (200) :
{
"success": true,
"message": "Déconnexion réussie"
}Implémentation Laravel :
// routes/api.php
Route::post('/native/logout', function (Request $request) {
// Supprime UNIQUEMENT le token courant (pas les autres sessions)
$request->user()->currentAccessToken()->delete();
return response()->json([
'success' => true,
'message' => 'Déconnexion réussie',
]);
})->middleware('auth:sanctum');Controller Laravel Complet (copier-coller)
Voici un NativeAuthController.php complet regroupant les 4 endpoints. Copiez-le dans app/Http/Controllers/Api/NativeAuthController.php :
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
class NativeAuthController extends Controller
{
// ════════════════════════════════════════
// GET /api/native/config
// ════════════════════════════════════════
/**
* Retourne les credentials IAM chiffrés (opaque token).
* Le frontend ne voit JAMAIS app_key ni secret_key en clair.
*/
public function config(): JsonResponse
{
$appKey = config('services.iam.app_key');
$secretKey = config('services.iam.secret_key');
if (empty($appKey) || empty($secretKey)) {
Log::error('[NativeSSO] Config manquante : IAM_APP_KEY ou IAM_SECRET_KEY');
return response()->json([
'success' => false,
'error' => 'Configuration SSO incomplète',
'error_type' => 'config_missing',
], 500);
}
// Clé AES = SHA-256 de la secret_key (32 bytes binaires)
$aesKey = hash('sha256', $secretKey, true);
$iv = random_bytes(16);
$payload = json_encode([
'secret_key' => $secretKey,
'ts' => time(),
]);
$encrypted = openssl_encrypt($payload, 'aes-256-cbc', $aesKey, OPENSSL_RAW_DATA, $iv);
$encryptedCredentials = base64_encode($iv . '::' . base64_encode($encrypted));
return response()->json([
'success' => true,
'app_key' => $appKey, // En clair (non sensible, sert d'identifiant pour l'IAM)
'encrypted_credentials' => $encryptedCredentials,
'iam_api_url' => config('services.iam.api_url', 'https://identityam.ollaid.com/api'),
'credentials_ttl' => 300,
'debug' => (bool) config('services.iam.debug'), // Contrôlé par IAM_DEBUG dans .env
]);
}
// ════════════════════════════════════════
// POST /api/native/exchange
// ════════════════════════════════════════
/**
* Échange le callback_token IAM contre un token Sanctum local.
*
* 1. Reçoit callback_token du frontend
* 2. Appelle IAM /iam/auth/decrypt pour décrypter
* 3. Crée ou met à jour l'utilisateur local
* 4. Génère un token Sanctum
*/
public function exchange(Request $request): JsonResponse
{
$callbackToken = $request->input('callback_token');
if (empty($callbackToken)) {
return response()->json([
'success' => false,
'error' => 'callback_token est requis',
'error_type' => 'missing_token',
], 400);
}
try {
$response = Http::timeout(30)
->withHeaders([
'Content-Type' => 'application/json',
'Accept' => 'application/json',
'X-IAM-App-Key' => config('services.iam.app_key'),
'X-IAM-Secret-Key' => config('services.iam.secret_key'),
])
->post(config('services.iam.api_url') . '/iam/auth/decrypt', [
'token' => $callbackToken,
]);
if (!$response->successful() || !$response->json('success')) {
$error = $response->json();
Log::warning('[NativeSSO] Échec décryptage callback_token', [
'status' => $response->status(),
'error' => $error['message'] ?? 'Inconnu',
]);
return response()->json([
'success' => false,
'error' => $error['message'] ?? 'Token invalide ou expiré',
'error_type' => 'invalid_token',
], 401);
}
$data = $response->json('data', $response->json());
$iamReference = $data['iam_reference'] ?? null;
$aliasReference = $data['alias_reference'] ?? null;
$userInfos = $data['user_infos'] ?? [];
if (!$iamReference || empty($userInfos)) {
return response()->json([
'success' => false,
'error' => 'Données utilisateur incomplètes depuis l\'IAM',
'error_type' => 'decrypt_failed',
], 422);
}
// Créer ou mettre à jour l'utilisateur local
$user = User::where('reference', $iamReference)->first();
if (!$user && $aliasReference) {
$user = User::where('alias_reference', $aliasReference)->first();
}
$userData = [
'name' => $userInfos['name'] ?? null,
'email' => $userInfos['email'] ?? null,
'phone' => $userInfos['phone'] ?? null,
'ccphone' => $userInfos['ccphone'] ?? null,
'image' => $userInfos['image_url'] ?? null,
'alias_reference' => $aliasReference,
'user_infos' => json_encode($userInfos),
];
if ($user) {
$user->update($userData);
} else {
$user = User::create(array_merge($userData, [
'reference' => $iamReference,
'password' => bcrypt(Str::random(32)),
]));
}
// Générer un token Sanctum (expiration 30 jours)
$expiresAt = now()->addDays(30);
$token = $user->createToken('native-sso', ['*'], $expiresAt);
// Récupérer app_access_token_ref depuis la racine de la réponse IAM
$appAccessTokenRef = $data['app_access_token_ref'] ?? null;
// Stocker la ref dans le token Sanctum pour le webhook de révocation
if ($appAccessTokenRef) {
$token->accessToken->forceFill([
'app_access_token_ref' => $appAccessTokenRef,
])->save();
}
return response()->json([
'success' => true,
'token' => $token->plainTextToken,
'expires_at' => $expiresAt->toIso8601String(),
'app_access_token_ref' => $appAccessTokenRef,
'user' => [
'id' => $user->id,
'reference' => $iamReference,
'alias_reference' => $aliasReference,
'name' => $userInfos['name'] ?? null,
'email' => $userInfos['email'] ?? null,
'phone' => isset($userInfos['phone'])
? ($userInfos['ccphone'] ?? '') . $userInfos['phone']
: null,
'ccphone' => $userInfos['ccphone'] ?? null,
'image_url' => $userInfos['image_url'] ?? null,
'email_verified' => ($userInfos['email_verification'] ?? '') === 'verified',
'phone_verified' => ($userInfos['phone_verification'] ?? '') === 'verified',
],
]);
} catch (\Illuminate\Http\Client\ConnectionException $e) {
Log::error('[NativeSSO] Timeout connexion IAM', ['error' => $e->getMessage()]);
return response()->json([
'success' => false,
'error' => 'Impossible de contacter le serveur d\'authentification',
'error_type' => 'exchange_error',
], 503);
} catch (\Exception $e) {
Log::error('[NativeSSO] Erreur exchange', ['error' => $e->getMessage()]);
$details = [];
if (config('services.iam.debug')) {
$details = [
'debug_error' => $e->getMessage(),
'debug_file' => basename($e->getFile()) . ':' . $e->getLine(),
];
}
return response()->json([
'success' => false,
'error' => 'Erreur lors de l\'authentification',
'error_type' => 'exchange_error',
...$details,
], 500);
}
}
// ════════════════════════════════════════
// POST /api/native/check-token
// ════════════════════════════════════════
/**
* Vérifie la validité du token Sanctum et retourne les user_infos fraîches.
* Route protégée par auth:sanctum.
*/
public function checkToken(Request $request): JsonResponse
{
$user = $request->user();
return response()->json([
'status' => 'connected',
'message' => 'Utilisateur connecté',
'user' => [
'name' => $user->name,
'email' => $user->email,
'ccphone' => $user->ccphone,
'phone' => $user->phone,
'image_url' => $user->image,
'town' => $user->town ?? null,
'country' => $user->country ?? null,
'auth_2fa' => (bool) ($user->auth_2fa ?? false),
],
]);
}
// ════════════════════════════════════════
// POST /api/native/logout (déconnexion synchronisée)
// ════════════════════════════════════════
/**
* Révoque le token Sanctum courant (single-session)
* ET notifie l'IAM pour révoquer l'AppAccessToken lié.
* Route protégée par auth:sanctum.
*/
public function logout(Request $request): JsonResponse
{
try {
$user = $request->user();
if ($user) {
$sanctumTokenPlain = $request->bearerToken();
$currentToken = $user->currentAccessToken();
$appAccessTokenRef = $currentToken?->app_access_token_ref ?? null;
// Supprimer le token APRÈS avoir lu la ref
$currentToken?->delete();
// Notifier l'IAM (fire-and-forget, timeout 5s)
if ($sanctumTokenPlain || $appAccessTokenRef) {
$iamPrefix = $request->attributes->get('iam_prefix', 'iam');
$iamApiUrl = config("services.{$iamPrefix}.api_url", 'https://identityam.ollaid.com/api');
try {
Http::timeout(5)->post("{$iamApiUrl}/iam/disconnect", array_filter([
'sanctum_token' => $sanctumTokenPlain,
'app_access_token_ref' => $appAccessTokenRef,
]));
} catch (\Exception $e) {
Log::warning("[NativeSSO] IAM disconnect failed (non-blocking)", ['error' => $e->getMessage()]);
}
}
}
return response()->json(['success' => true, 'message' => 'Déconnexion réussie']);
} catch (\Exception $e) {
return response()->json(['success' => true, 'message' => 'Déconnexion effectuée']);
}
}
}Routes routes/api.php
⚠️ Attention au placement des routes !
configetexchangedoivent être publiques (pas de middleware auth).check-tokenetlogoutdoivent être protégées parauth:sanctum.
Siexchangeest derrièreauth:sanctum, le frontend recevra un 401 Unauthenticated.
use App\Http\Controllers\Api\NativeAuthController;
// ✅ Routes PUBLIQUES (pas d'auth requise — l'utilisateur n'a pas encore de token)
Route::get('/native/config', [NativeAuthController::class, 'config']);
Route::post('/native/exchange', [NativeAuthController::class, 'exchange']);
// 🔒 Routes PROTÉGÉES (auth Sanctum requise — l'utilisateur a un token)
Route::middleware('auth:sanctum')->group(function () {
Route::post('/native/check-token', [NativeAuthController::class, 'checkToken']);
Route::post('/native/logout', [NativeAuthController::class, 'logout']);
});Middleware ForceJsonResponse (recommandé)
Pour éviter que Laravel retourne du HTML au lieu de JSON en cas d'erreur :
// app/Http/Middleware/ForceJsonResponse.php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
class ForceJsonResponse
{
public function handle(Request $request, Closure $next)
{
$request->headers->set('Accept', 'application/json');
return $next($request);
}
}Appliquez-le sur les routes API dans bootstrap/app.php (Laravel 11+) :
->withMiddleware(function (Middleware $middleware) {
$middleware->api(prepend: [
\App\Http\Middleware\ForceJsonResponse::class,
]);
})Ou dans app/Http/Kernel.php (Laravel 10 et avant) :
'api' => [
\App\Http\Middleware\ForceJsonResponse::class,
// ... autres middlewares
],Réponses d'erreur
Tous les endpoints retournent le même format d'erreur :
{
"success": false,
"error": "Description lisible de l'erreur",
"error_type": "code_erreur"
}Codes d'erreur par endpoint
/api/native/config
| Code HTTP | error_type | Description |
|-----------|-------------|-------------|
| 500 | config_missing | IAM_APP_KEY ou IAM_SECRET_KEY non configuré dans le .env |
/api/native/exchange
| Code HTTP | error_type | Description |
|-----------|-------------|-------------|
| 400 | missing_token | callback_token absent du body |
| 401 | invalid_token | Token expiré, invalide ou déjà utilisé |
| 422 | decrypt_failed | Erreur lors de l'appel à l'IAM /iam/auth/decrypt |
| 500 | exchange_error | Erreur interne lors de la création du user/token |
/api/native/logout
| Code HTTP | error_type | Description |
|-----------|-------------|-------------|
| 401 | unauthenticated | Token Bearer manquant ou invalide |
Configuration .env Laravel
Ajoutez ces variables dans le fichier .env du backend SaaS :
# Credentials IAM (récupérés depuis le dashboard iam.ollaid.com)
IAM_APP_KEY=oiam_ak_xxxxxxxxxxxxxxxxxxxxxxxx
IAM_SECRET_KEY=oiam_sk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
IAM_API_URL=https://identityam.ollaid.com/api
# Mode debug — contrôle le DebugPanel et les logs côté frontend
# true : active le DebugPanel + logs console + détails d'erreur dans les réponses JSON
# false : mode production (aucun détail d'erreur exposé, pas de DebugPanel)
# ⚠️ Si absent ou null → considéré comme false (mode production)
IAM_DEBUG=falseNote : Pas de clé partagée supplémentaire. Le chiffrement AES-256-CBC utilise directement un hash SHA-256 de la
secret_keycomme clé de chiffrement. L'IAM retrouve lasecret_keyvia l'app_keydans sa tableapplications.
Et dans config/services.php :
'iam' => [
'app_key' => env('IAM_APP_KEY'),
'secret_key' => env('IAM_SECRET_KEY'),
'api_url' => env('IAM_API_URL', 'https://identityam.ollaid.com/api'),
'debug' => env('IAM_DEBUG', false), // Active le debug côté frontend (false si absent)
],Migration Laravel
Colonnes requises sur la table users
Si votre table users n'a pas encore ces colonnes, ajoutez-les :
php artisan make:migration add_iam_columns_to_users_tablepublic function up(): void
{
Schema::table('users', function (Blueprint $table) {
$table->string('reference')->unique()->nullable()->after('id');
$table->string('alias_reference')->nullable()->after('reference');
$table->string('ccphone')->nullable();
$table->string('phone')->nullable();
$table->string('image')->nullable();
$table->json('user_infos')->nullable();
$table->string('phone_verification')->default('pending')->nullable();
$table->string('email_verification')->default('pending')->nullable();
});
}Note : Le champ
referenceest l'identifiant unique IAM de l'utilisateur. Le champalias_referenceidentifie l'utilisateur dans le contexte d'une application spécifique.
Flux d'authentification
┌──────────────────┐ ┌──────────────┐ ┌──────────────┐
│ @ollaid/native │ │ IAM API │ │ SaaS API │
│ -sso (Frontend) │ │ (Ollaid) │ │ (Laravel) │
└────────┬─────────┘ └──────┬───────┘ └──────┬───────┘
│ │ │
│ 1. GET /api/native/config │
│───────────────────────────────────────────►│
│◄──────────────── encrypted_credentials ────│
│ │ │
│ 2. POST /iam/native/encrypt │
│─────────────────────►│ │
│◄── encrypted_credentials │
│ │ │
│ 3. POST /iam/native/init │
│─────────────────────►│ │
│◄── status + session │ │
│ │ │
│ 4. POST /iam/native/validate │
│─────────────────────►│ │
│◄── callback_token │ │
│ │ │
│ 5. POST /api/native/exchange │
│───────────────────────────────────────────►│
│ │ decrypt via IAM │
│ │◄────────────────────│
│ │────────────────────►│
│◄──────────────── sanctum token + user ─────│
│ │ │
│ ✅ Connecté ! │ │Détail des étapes :
- Config — Le frontend récupère
encrypted_credentials(blob opaque chiffré AES-256-CBC) depuis le backend SaaS. Lesapp_keyetsecret_keyne sont jamais exposées au frontend. - Encrypt — Le frontend envoie le blob
encrypted_credentialsà l'IAM, qui le déchiffre côté serveur pour valider les credentials - Init — L'IAM vérifie le compte et retourne le statut (
pending_password,pending_otp,needs_access, etc.) - Validate — Le frontend envoie le mot de passe/OTP, l'IAM retourne un
callback_token - Exchange — Le frontend envoie le
callback_tokenau backend SaaS, qui le décrypte via l'IAM et crée une session Sanctum
Important : Le package gère les étapes 1-5 automatiquement. Le backend SaaS doit implémenter 4 endpoints (
config,exchange,check-token,logout).
APIs IAM Account (Server-to-Server)
⚠️ ATTENTION : Ces APIs utilisent la
secret_keyde votre application IAM. Elles doivent être appelées exclusivement depuis votre backend (server-to-server). Ne jamais exposer lasecret_keycôté frontend.
Le package expose un service iamAccountService pour interagir avec les APIs IAM de gestion de compte :
import { iamAccountService } from '@ollaid/native-sso';POST /api/iam/link-phone
Associe un numéro de téléphone à un compte utilisateur existant dans l'IAM.
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.
Paramètres requis :
{
"app_key": "oiam_ak_xxxxxxxxxxxxxxxxxxxxxxxx",
"secret_key": "oiam_sk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"iam_reference": "USR-XXXXXXXX",
"ccphone": "+221",
"phone": "771234567"
}| Champ | Type | Description |
|-------|------|-------------|
| app_key | string | Clé publique de l'application IAM |
| secret_key | string | Clé secrète de l'application IAM |
| iam_reference | string | Référence unique de l'utilisateur dans l'IAM (USR-XXXXXXXX) |
| ccphone | string | Indicatif téléphonique (ex: +221) |
| phone | string | Numéro de téléphone sans indicatif (ex: 771234567) |
Réponse succès (200) :
{
"success": true,
"message": "Numéro de téléphone lié avec succès",
"user_reference": "USR-XXXXXXXX",
"user_infos": {
"name": "John Doe",
"email": "[email protected]",
"ccphone": "+221",
"phone": "771234567",
"address": null,
"town": "Dakar",
"country": "SN",
"image_url": "https://identityam.ollaid.com/storage/avatars/xxx.jpg",
"auth_2fa": false
}
}Codes d'erreur :
| Code HTTP | error_type | Description |
|-----------|-------------|-------------|
| 401 | invalid_credentials | app_key ou secret_key invalide |
| 404 | user_not_found | iam_reference introuvable |
| 409 | phone_already_linked | Ce numéro est déjà associé à un autre compte |
| 422 | validation_error | Champs manquants ou format invalide |
Exemple Node.js (backend) :
import { iamAccountService } from '@ollaid/native-sso';
const result = await iamAccountService.linkPhone({
app_key: process.env.IAM_APP_KEY,
secret_key: process.env.IAM_SECRET_KEY,
iam_reference: 'USR-XXXXXXXX',
ccphone: '+221',
phone: '771234567',
});
if (result.success) {
// Mettre à jour l'utilisateur local avec result.user_infos
console.log('Téléphone lié :', result.user_infos.phone);
}POST /api/iam/link-email
Associe une adresse email à un compte utilisateur existant dans l'IAM.
Cas d'usage : Un utilisateur inscrit par téléphone (phone-only) souhaite ajouter son email.
Paramètres requis :
{
"app_key": "oiam_ak_xxxxxxxxxxxxxxxxxxxxxxxx",
"secret_key": "oiam_sk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"iam_reference": "USR-XXXXXXXX",
"email": "[email protected]"
}| Champ | Type | Description |
|-------|------|-------------|
| app_key | string | Clé publique de l'application IAM |
| secret_key | string | Clé secrète de l'application IAM |
| iam_reference | string | Référence unique de l'utilisateur dans l'IAM |
| email | string | Adresse email à associer |
Réponse succès (200) :
{
"success": true,
"message": "Adresse email liée avec succès",
"user_reference": "USR-XXXXXXXX",
"user_infos": {
"name": "John Doe",
"email": "[email protected]",
"ccphone": "+221",
"phone": "771234567",
"address": null,
"town": "Dakar",
"country": "SN",
"image_url": "https://identityam.ollaid.com/storage/avatars/xxx.jpg",
"auth_2fa": false
}
}Codes d'erreur :
| Code HTTP | error_type | Description |
|-----------|-------------|-------------|
| 401 | invalid_credentials | app_key ou secret_key invalide |
| 404 | user_not_found | iam_reference introuvable |
| 409 | email_already_linked | Cet email est déjà associé à un autre compte |
| 422 | validation_error | Champs manquants ou format invalide |
Exemple Node.js (backend) :
import { iamAccountService } from '@ollaid/native-sso';
const result = await iamAccountService.linkEmail({
app_key: process.env.IAM_APP_KEY,
secret_key: process.env.IAM_SECRET_KEY,
iam_reference: 'USR-XXXXXXXX',
email: '[email protected]',
});
if (result.success) {
console.log('Email lié :', result.user_infos.email);
}POST /api/iam/refresh-user-info (Single)
Récupère les informations à jour d'un utilisateur depuis l'IAM.
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.
Paramètres requis :
{
"secret_key": "oiam_sk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"alias_reference": "ALI-XXXXXXXX"
}| Champ | Type | Description |
|-------|------|-------------|
| secret_key | string | Clé secrète de l'application IAM |
| alias_reference | string | Référence de l'alias utilisateur dans le contexte de votre app (ALI-XXXXXXXX) |
Réponse succès (200) :
{
"success": true,
"message": "Informations utilisateur récupérées",
"user_reference": "USR-XXXXXXXX",
"alias_reference": "ALI-XXXXXXXX",
"login_type": "email",
"user_infos": {
"name": "John Doe",
"email": "[email protected]",
"ccphone": "+221",
"phone": "771234567",
"address": null,
"town": "Dakar",
"country": "SN",
"image_url": "https://identityam.ollaid.com/storage/avatars/xxx.jpg",
"auth_2fa": false
}
}Codes d'erreur :
| Code HTTP | error_type | Description |
|-----------|-------------|-------------|
| 401 | invalid_credentials | secret_key invalide |
| 404 | alias_not_found | alias_reference introuvable |
Exemple Node.js :
import { iamAccountService } from '@ollaid/native-sso';
const result = await iamAccountService.refreshUserInfo({
secret_key: process.env.IAM_SECRET_KEY,
alias_reference: 'ALI-XXXXXXXX',
});
if (result.success) {
// Mettre à jour le profil local
await updateLocalUser(result.alias_reference, result.user_infos);
}POST /api/iam/refresh-user-info (Bulk)
Synchronise les informations de plusieurs utilisateurs en un seul appel (maximum 100 références par requête).
Cas d'usage : Synchronisation batch quotidienne, ou mise à jour groupée après un import.
Paramètres requis :
{
"secret_key": "oiam_sk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"alias_references": [
"ALI-AAAAAAAA",
"ALI-BBBBBBBB",
"ALI-CCCCCCCC"
]
}| Champ | Type | Description |
|-------|------|-------------|
| secret_key | string | Clé secrète de l'application IAM |
| alias_references | string[] | Liste des alias à synchroniser (max 100) |
Réponse succès (200) :
{
"success": true,
"message": "Synchronisation terminée",
"total_requested": 3,
"total_found": 2,
"total_errors": 1,
"data": [
{
"user_reference": "USR-AAAAAAAA",
"alias_reference": "ALI-AAAAAAAA",
"login_type": "email",
"user_infos": { "name": "Alice", "email": "[email protected]", "..." : "..." }
},
{
"user_reference": "USR-BBBBBBBB",
"alias_reference": "ALI-BBBBBBBB",
"login_type": "phone",
"user_infos": { "name": "Bob", "phone": "771234567", "..." : "..." }
}
],
"errors": [
{
"alias_reference": "ALI-CCCCCCCC",
"error": "Alias introuvable"
}
]
}Codes d'erreur :
| Code HTTP | error_type | Description |
|-----------|-------------|-------------|
| 401 | invalid_credentials | secret_key invalide |
| 422 | too_many_references | Plus de 100 références envoyées |
| 422 | validation_error | alias_references manquant ou invalide |
Exemple Node.js :
import { iamAccountService } from '@ollaid/native-sso';
const result = await iamAccountService.refreshUserInfoBulk({
secret_key: process.env.IAM_SECRET_KEY,
alias_references: ['ALI-AAAAAAAA', 'ALI-BBBBBBBB', 'ALI-CCCCCCCC'],
});
console.log(`Synchronisés: ${result.total_found}/${result.total_requested}`);
// Traiter les succès
for (const item of result.data ?? []) {
await updateLocalUser(item.alias_reference, item.user_infos);
}
// Logger les erreurs
for (const err of result.errors ?? []) {
console.warn(`Erreur pour ${err.alias_reference}: ${err.error}`);
}Avatar par application
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.
Cascade de résolution image_url
Dans toutes les réponses user_infos, le champ image_url est résolu selon cette priorité :
app_access.avatar— Avatar spécifique à l'application (si défini et valide)user.image— Image globale du profil IAM (si définie et valide)no-image.png— Fallback par défaut
Migration requise
ALTER TABLE app_accesses ADD COLUMN avatar VARCHAR(255) NULL AFTER status;Ou via Laravel :
Schema::table('app_accesses', function (Blueprint $table) {
$table->string('avatar')->nullable()->after('status');
});Pré-remplissage
Lorsqu'un AppAccess est créé (inscription, grant-access), le champ avatar est automatiquement pré-rempli avec l'image globale du user (user.image).
POST /api/iam/update-avatar
Met à jour l'avatar d'un utilisateur pour une application spécifique.
⚠️ Server-to-Server uniquement — Requiert
app_key+secret_key.
Paramètres requis :
{
"app_key": "oiam_ak_xxxxxxxxxxxxxxxxxxxxxxxx",
"secret_key": "oiam_sk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"alias_reference": "ALI-XXXXXXXX",
"avatar_url": "https://example.com/avatars/user123.jpg"
}| Champ | Type | Description |
|-------|------|-------------|
| app_key | string | Clé publique de l'application IAM |
| secret_key | string | Clé secrète de l'application IAM |
| alias_reference | string | Référence de l'alias utilisateur |
| avatar_url | string | URL de la nouvelle image avatar |
Réponse succès (200) :
{
"success": true,
"message": "Avatar mis à jour",
"user_reference": "USR-XXXXXXXX",
"alias_reference": "ALI-XXXXXXXX",
"user_infos": {
"name": "John Doe",
"email": "[email protected]",
"ccphone": "+221",
"phone": "771234567",
"address": null,
"town": "Dakar",
"country": "SN",
"image_url": "https://example.com/avatars/user123.jpg",
"auth_2fa": false
}
}Codes d'erreur :
| Code HTTP | Description |
|-----------|-------------|
| 403 | app_key ou secret_key invalide |
| 404 | alias_reference introuvable ou pas d'accès à cette app |
| 422 | Champs manquants ou format invalide |
Exemple Node.js (backend) :
import { iamAccountService } from '@ollaid/native-sso';
const result = await iamAccountService.updateAvatar({
app_key: process.env.IAM_APP_KEY,
secret_key: process.env.IAM_SECRET_KEY,
alias_reference: 'ALI-XXXXXXXX',
avatar_url: 'https://example.com/avatars/user123.jpg',
});
if (result.success) {
console.log('Avatar mis à jour :', result.user_infos.image_url);
}POST /api/iam/reset-avatar
Réinitialise l'avatar d'un utilisateur pour une application, le remettant à null. La cascade retombera sur user.image ou no-image.png.
⚠️ Server-to-Server uniquement — Requiert
app_key+secret_key.
Paramètres requis :
{
"app_key": "oiam_ak_xxxxxxxxxxxxxxxxxxxxxxxx",
"secret_key": "oiam_sk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"alias_reference": "ALI-XXXXXXXX"
}| Champ | Type | Description |
|-------|------|-------------|
| app_key | string | Clé publique de l'application IAM |
| secret_key | string | Clé secrète de l'application IAM |
| alias_reference | string | Référence de l'alias utilisateur |
Réponse succès (200) :
{
"success": true,
"message": "Avatar réinitialisé",
"user_reference": "USR-XXXXXXXX",
"alias_reference": "ALI-XXXXXXXX",
"user_infos": {
"name": "John Doe",
"email": "[email protected]",
"image_url": "https://iam.example.com/storage/users/image.jpg"
}
}Codes d'erreur :
| Code HTTP | Description |
|-----------|-------------|
| 403 | app_key ou secret_key invalide |
| 404 | alias_reference introuvable ou pas d'accès à cette app |
| 422 | Champs manquants |
Exemple Node.js (backend) :
import { iamAccountService } from '@ollaid/native-sso';
const result = await iamAccountService.resetAvatar({
app_key: process.env.IAM_APP_KEY,
secret_key: process.env.IAM_SECRET_KEY,
alias_reference: 'ALI-XXXXXXXX',
});
if (result.success) {
console.log('Avatar réinitialisé, image actuelle :', result.user_infos.image_url);
}Format user_infos (9 champs standardisés)
Toutes les APIs IAM retournent le même objet user_infos avec exactement 9 champs :
| Champ | Type | Description |
|-------|------|-------------|
| name | string | Nom complet de l'utilisateur |
| email | string \| null | Adresse email (null si compte phone-only) |
| ccphone | string \| null | Indicatif téléphonique (ex: +221) |
| phone | string \| null | Numéro de téléphone sans indicatif |
| address | string \| null | Adresse postale |
| town | string \| null | Ville |
| country | string \| null | Code pays ISO (ex: SN, FR) |
| image_url | string \| null | URL de l'avatar (résolu via la cascade app_access → user → fallback) |
| auth_2fa | boolean | Indique si la 2FA est activée |
Note : Le champ
image_urlutilise 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 fallbackno-image.png.
Session & localStorage
Le
