npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@ollaid/native-sso

v2.1.5

Published

Package NPM fullstack pour l'authentification Native SSO Ollaid - Frontend-First

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

  1. Installation
  2. Intégration rapide (3 étapes)
  3. Props de NativeSSOPage
  4. Usage avancé (composants individuels)
  5. Props des composants individuels
  6. Gestion des conflits d'inscription
  7. Hook useMobileRegistration
  8. Backend SaaS — Endpoints requis
  9. APIs IAM Account (Server-to-Server)
  10. Avatar par application
  11. Réponses d'erreur
  12. Configuration .env Laravel
  13. Migration Laravel
  14. Flux d'authentification
  15. Session & localStorage
  16. Déconnexion synchronisée
  17. OnboardingModal
  18. useTokenHealthCheck
  19. Sécurité
  20. Exports
  21. Publication & Installation npm

Installation

npm install @ollaid/native-sso

Intégration rapide (3 étapes)

1. Installer le package

npm install @ollaid/native-sso

2. 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 debug est contrôlé uniquement par le backend via la variable d'environnement IAM_DEBUG dans le .env du SaaS. Il n'y a plus de prop debug à passer au composant. Le DebugPanel est réactif : il apparaît automatiquement après le chargement des credentials si debug: true est retourné par le backend.

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 accountType et 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 redirectAfterLogin est défini → redirection via window.location.href après connexion
  • Si onLoginSuccess est 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 le localStorage manuellement ni utiliser clearAuthToken() 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() ?

  1. Révoque le token SaaSPOST /api/native/logout (supprime le Sanctum token)
  2. Révoque la session IAMPOST /api/iam/disconnect (avec sanctum_token + app_access_token_ref)
  3. 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 dynamique

Cô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=true

Cô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-Prefix dans exchange, check-token et logout.


Debug Mode — Troubleshooting

Le debug est piloté par le backend : le package n'a aucun prop debug.

Comment ça marche

  1. Le backend lit IAM_DEBUG (ou IAM_VENDOR_DEBUG, etc.) depuis .env
  2. GET /api/native/config retourne "debug": true
  3. Le package active les logs console + le DebugPanel
  4. Le DebugPanel est 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=true

Après correction, n'oubliez pas :

php artisan config:clear
php artisan config:cache

Usage 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

  1. L'utilisateur remplit le formulaire d'inscription et soumet
  2. Le backend détecte un conflit (email_exists ou phone_exists) et retourne un objet conflict
  3. Le SignupModal affiche la ConflictView avec 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 : initverify-otpcomplete.

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 flux

POST /api/native/exchange

⚠️ IMPORTANT — Route PUBLIQUE obligatoire
Cette route NE DOIT PAS être derrière le middleware auth:sanctum.
Si vous la placez dans un groupe Route::middleware('auth:sanctum'), le frontend recevra une erreur 401 Unauthenticated systématique car l'utilisateur n'a pas encore de token Sanctum à ce stade du flux.
Assurez-vous que /api/native/config et /api/native/exchange sont 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 :

  1. Recevoir le callback_token
  2. Appeler l'IAM POST /iam/auth/decrypt avec les credentials dans les headers (X-IAM-App-Key, X-IAM-Secret-Key) et le token dans le body
  3. L'IAM retourne les user_infos (9 champs standardisés) + app_access_token_ref
  4. Créer ou mettre à jour l'utilisateur local
  5. Générer un token Sanctum
  6. 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 !
config et exchange doivent être publiques (pas de middleware auth).
check-token et logout doivent être protégées par auth:sanctum.
Si exchange est derrière auth: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=false

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.

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_table
public 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 reference est l'identifiant unique IAM de l'utilisateur. Le champ alias_reference identifie 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 :

  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.
  2. Encrypt — Le frontend envoie le blob encrypted_credentials à l'IAM, qui le déchiffre côté serveur pour valider les credentials
  3. Init — L'IAM vérifie le compte et retourne le statut (pending_password, pending_otp, needs_access, etc.)
  4. Validate — Le frontend envoie le mot de passe/OTP, l'IAM retourne un callback_token
  5. Exchange — Le frontend envoie le callback_token au 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_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.

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é :

  1. app_access.avatar — Avatar spécifique à l'application (si défini et valide)
  2. user.image — Image globale du profil IAM (si définie et valide)
  3. 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_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.


Session & localStorage

Le