Guide d'intégration

Pour les développeurs qui intègrent FASO LOGIN comme fournisseur d'identité.

Ce que FASO LOGIN fournit

FASO LOGIN est un IDP souverain burkinabè. Il permet à vos utilisateurs de s'authentifier via leur numéro de téléphone +226, sans créer un compte spécifique à votre application. Protocole : OpenID Connect — Authorization Code Flow avec PKCE obligatoire.

Discovery : https://api.fasologin.tino-ti.com/oidc/.well-known/openid-configuration

Étape 1 — Enregistrement

Soumettez une demande d'accès (ou depuis votre espace partenaire si vous en avez déjà un — section "Nouvelle app"). L'admin valide sous 48h ouvrées.

Ce qu'il faut préparer — récapitulatif complet

Tous les champs possibles pour un client OIDC FASO LOGIN, et à quel moment chacun se renseigne. Rien n'est caché ailleurs : si un champ n'est pas dans ce tableau, il n'existe pas.

ChampObligatoire ?Où le renseigner
Nom de l’applicationOuiÀ la demande
Email de contactOuiÀ la demande (auto si connecté à votre espace partenaire)
Description du projetOui (≥ 10 caractères)À la demande
Redirect URI(s) — loginOui (au moins 1)À la demande
ScopesOui (openid inclus automatiquement)À la demande
Post-logout redirect URI(s)NonÀ la demande, ou ajoutée ensuite (admin)
Backchannel logout URINon/partner/settings, après approbation
Webhook URL + événementsNon/partner/webhooks, après approbation
Logo (écran de consentement)Non/partner/settings, après approbation
Durée de vie access tokenNon (défaut 1h)/partner/settings, après approbation
Durée de vie refresh tokenNon (défaut 14j)/partner/settings, après approbation

Les 5 derniers champs (backchannel logout, webhook, logo, durées de vie) n'existent pas encore au moment de la demande initiale — le client OIDC n'est créé qu'à l'approbation. Ne les demandez pas à l'inscription : les valeurs par défaut fonctionnent très bien pour un premier lancement, configurez-les ensuite depuis votre espace partenaire si besoin.

Redirect URIs

Web : HTTPS uniquement (https://app.monservice.bf/auth/callback).
Mobile natif : custom scheme URI — le reverse domain est recommandé pour éviter les collisions OS (com.monentreprise.monapp://auth/callback), mais un schéma simple est accepté (monapp://auth/callback).

Scopes disponibles

ScopeClaims retournés
openidsub (identifiant unique — obligatoire)
profilegiven_name, family_name, preferred_username, birthdate, gender, locale
phonephone_number, phone_number_verified
emailemail, email_verified
addresslocality, region, country, formatted

Étape 2 — Credentials

Après approbation, vous recevrez par email votre client_id et client_secret. Le secret est affiché une seule fois — conservez-le immédiatement dans votre gestionnaire de secrets. Ne le committez jamais dans votre code source.

Clients publics (mobile natif sans serveur) : vous recevrez uniquement un client_id — pas de secret. L'authentification se fait exclusivement via PKCE (code_verifier).

App web + mobile (“Les deux”) : 2 clients distincts sont créés — vous recevrez dans un seul email un client_id + client_secret pour le backend web, et un client_id seul pour l'app mobile (PKCE).

Étape 3 — Implémenter le flow (PKCE)

Générer le PKCE (côté backend)

const crypto = require('crypto');

function generatePKCE() {
  const verifier = crypto.randomBytes(32).toString('base64url');
  const challenge = crypto.createHash('sha256').update(verifier).digest('base64url');
  return { verifier, challenge };
}

Construire l'URL d'autorisation

GET /oidc/auth
  ?client_id=fasologin_xxxxxxxxxxxxxxxx
  &redirect_uri=https://app.monservice.bf/auth/callback
  &response_type=code
  &scope=openid profile phone
  &state=<random_state>
  &code_challenge=<base64url_sha256_verifier>
  &code_challenge_method=S256

Échanger le code contre les tokens

POST /oidc/token
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code
&code=<code>
&redirect_uri=https://app.monservice.bf/auth/callback
&client_id=fasologin_xxxxxxxxxxxxxxxx
&client_secret=<secret>
&code_verifier=<verifier>

Client public (mobile natif) : omettez client_secret du body — sa présence déclenchera une erreur invalid_client. Le code_verifier PKCE suffit.

Récupérer les claims utilisateur

GET /oidc/me
Authorization: Bearer <access_token>

Étape 4 — Refresh tokens

L'access_token expire en 1h par défaut. Le stockage du refresh_token dépend du type de client — ne mélangez pas les deux :

Client confidentiel (avec backend) — stockage côté serveur uniquement

POST /oidc/token
Content-Type: application/x-www-form-urlencoded

grant_type=refresh_token
&refresh_token=<token>
&client_id=fasologin_xxxxxxxxxxxxxxxx
&client_secret=<secret>

Client public (app mobile sans backend) — stockage sécurisé natif uniquement (Keychain/Keystore)

POST /oidc/token
Content-Type: application/x-www-form-urlencoded

grant_type=refresh_token
&refresh_token=<token>
&client_id=fasologin_xxxxxxxxxxxxxxxx

Jamais dans localStorage/AsyncStorage ni un fichier en clair, quel que soit le type de client. Le SDK Flutter officiel gère ça automatiquement (Keychain/Keystore + rotation).

Introspection token

Pour vérifier un access_token côté backend sans le décoder :

POST /oidc/token/introspection
Content-Type: application/x-www-form-urlencoded

token=<access_token>
&client_id=fasologin_xxxxxxxxxxxxxxxx
&client_secret=<secret>

Apps mobiles (Expo / React Native / Flutter)

Utilisez expo-auth-session ou AppAuth. PKCE est géré automatiquement. Déclarez votre scheme dans app.json : "scheme": "com.monentreprise.monapp". Votre redirect URI : com.monentreprise.monapp://auth/callback.

Flutter : un SDK officiel fasologin_flutter est disponible — il encapsule AppAuth, la gestion des tokens et le refresh automatique. Contactez l'admin pour y accéder.

import * as AuthSession from 'expo-auth-session';

// Discovery URL fournit tous les endpoints automatiquement
const discovery = await AuthSession.fetchDiscoveryAsync(
  'https://api.fasologin.tino-ti.com/oidc'
);

Logout

Révoquez le refresh token, puis redirigez vers l'end_session_endpoint :

POST /oidc/token/revocation
token=<refresh_token>&client_id=...&client_secret=...

GET /oidc/session/end
  ?client_id=...
  &post_logout_redirect_uri=https://app.monservice.bf/
  &id_token_hint=<id_token>

Back-channel logout

Mécanisme serveur-à-serveur : quand un utilisateur se déconnecte de FASO LOGIN, le serveur envoie un logout token (JWT signé) en HTTP POST vers votre endpoint. Votre serveur peut alors invalider la session locale sans attendre le navigateur.

Enregistrement

Renseignez votre backchannel_logout_uri depuis votre /partner/settings (espace partenaire) une fois votre app approuvée — ou auprès de l'admin FASO LOGIN si vous n'avez pas encore de compte partenaire. Doit être une URL HTTPS accessible depuis Internet (HTTP accepté en développement). Non applicable aux clients publics (apps mobiles natives sans serveur).

Endpoint à implémenter côté RP

// Express / NestJS — POST /auth/backchannel-logout
app.post('/auth/backchannel-logout', express.urlencoded({ extended: false }), async (req, res) => {
  const logoutToken = req.body.logout_token;
  if (!logoutToken) return res.status(400).end();

  // Vérifier le JWT avec la clé publique FasoLogin
  // jwks_uri : https://api.fasologin.tino-ti.com/oidc/jwks
  const { sub, jti } = await verifyLogoutToken(logoutToken);

  // Protection replay : vérifier que jti n'a pas déjà été traité (cache Redis 60s)
  // if (await redis.get('logout_jti:' + jti)) return res.status(200).end();
  // await redis.setex('logout_jti:' + jti, 60, '1');

  // Invalider toutes les sessions de l'utilisateur (sub = UUID FasoLogin)
  await sessionStore.destroyByUserId(sub);

  // Répondre 200 dans les 60 secondes (délai max oidc-provider)
  res.status(200).end();
});

Valider le logout token

Le logout token est un JWT RS256 signé par FASO LOGIN. Vérifications obligatoires :

  • iss = issuer FASO LOGIN
  • aud = votre client_id
  • events contient http://schemas.openid.net/event/backchannel-logout
  • jti unique — rejeter les replays (stocker les jti vus en cache 60s)
  • Signature valide via https://api.fasologin.tino-ti.com/oidc/jwks
// Vérification avec jose (npm install jose)
import { jwtVerify, createRemoteJWKSet } from 'jose';

const JWKS = createRemoteJWKSet(
  new URL('https://api.fasologin.tino-ti.com/oidc/jwks')
);

async function verifyLogoutToken(token: string) {
  const { payload } = await jwtVerify(token, JWKS, {
    issuer: 'https://api.fasologin.tino-ti.com/oidc',
    audience: process.env.OIDC_CLIENT_ID,
  });
  if (!payload.events?.['http://schemas.openid.net/event/backchannel-logout']) {
    throw new Error('Not a logout token');
  }
  return payload; // .sub = UUID utilisateur FasoLogin
}

Recevoir des webhooks

FASO LOGIN notifie votre serveur en temps réel lorsque certains événements surviennent pour un utilisateur ayant accordé son consentement à votre application.

Activation : renseignez l'URL et les événements souhaités depuis votre espace partenaire — /partner/webhooks, une fois votre app approuvée. Aucun webhook n'est envoyé tant que ce n'est pas configuré.

Événements disponibles

ÉvénementDéclencheurChamps data
user.consent_revokedL'utilisateur révoque son consentementsub, client_id
user.account_suspendedUn admin suspend le comptesub
user.profile_updatedL'utilisateur modifie son profilsub, fields_updated[]

Format du payload

POST https://votre-serveur.bf/webhooks/fasologin
Content-Type: application/json
X-FasoLogin-Signature: sha256=<hmac-sha256-hex>
X-FasoLogin-Event: user.consent_revoked
X-FasoLogin-Delivery: <uuid>

{
  "event": "user.consent_revoked",
  "timestamp": "2026-05-20T10:00:00.000Z",
  "data": {
    "sub": "uuid-utilisateur",
    "client_id": "fasologin_xxx"
  }
}

Valider la signature

Calculez HMAC-SHA256(webhookSecret, rawBody) et comparez avec le header X-FasoLogin-Signature. Un header absent ou de longueur différente ne doit jamais faire planter votre handler (une exception non catchée y renvoie une 500 au lieu d'une 401) — validez la présence et la longueur avant la comparaison. Vérifiez aussi payload.timestamp (couvert par la signature) pour rejeter un webhook rejoué au-delà de 5 minutes.

Node.js / TypeScript

import * as crypto from 'crypto';
import express from 'express';

const WEBHOOK_SECRET = process.env.FASOLOGIN_WEBHOOK_SECRET!;
const MAX_AGE_MS = 5 * 60 * 1000;

function isValidSignature(signature: string | undefined, rawBody: Buffer): boolean {
  if (!signature) return false;
  const expected = 'sha256=' + crypto
    .createHmac('sha256', WEBHOOK_SECRET)
    .update(rawBody)
    .digest('hex');
  const a = Buffer.from(signature);
  const b = Buffer.from(expected);
  // Longueurs différentes → timingSafeEqual lève une exception : rejeter avant, pas de try/catch
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

app.post('/webhooks/fasologin', express.raw({ type: 'application/json' }), (req, res) => {
  const signature = req.headers['x-fasologin-signature'] as string | undefined;
  if (!isValidSignature(signature, req.body)) {
    return res.status(401).json({ error: 'invalid_signature' });
  }

  const payload = JSON.parse(req.body.toString());
  if (Date.now() - new Date(payload.timestamp).getTime() > MAX_AGE_MS) {
    return res.status(401).json({ error: 'webhook_too_old' }); // rejeu probable
  }

  res.status(200).json({ ok: true });
  // Traitement asynchrone recommandé
});

PHP

<?php
$secret     = $_ENV['FASOLOGIN_WEBHOOK_SECRET'];
$body       = file_get_contents('php://input');
$sig        = $_SERVER['HTTP_X_FASOLOGIN_SIGNATURE'] ?? '';
$expected   = 'sha256=' . hash_hmac('sha256', $body, $secret);
$max_age_s  = 5 * 60;

// hash_equals() gère nativement les longueurs différentes (retourne false, ne lève rien)
if ($sig === '' || !hash_equals($expected, $sig)) {
    http_response_code(401);
    exit;
}

$payload = json_decode($body, true);
if (time() - strtotime($payload['timestamp']) > $max_age_s) {
    http_response_code(401); // rejeu probable
    exit;
}

http_response_code(200);
echo '{"ok":true}';

Bonnes pratiques

  • Répondre 2xx en moins de 10 secondes — traitez le payload de façon asynchrone.
  • Utilisez X-FasoLogin-Delivery (UUID) pour dédupliquer les retries.
  • En cas d'échec, FASO LOGIN réessaie automatiquement : 1 min, 5 min, 30 min, 2h (5 tentatives max).
  • Le secret webhook est différent du client_secret OIDC — conservez-les séparément.

Bouton de connexion

Utilisez le bouton officiel FASO LOGIN pour la cohérence visuelle entre les applications. Cela rassure l'utilisateur et renforce la confiance dans l'écosystème.

Aperçu

Se connecter avec FASO LOGIN

HTML / Web

<!-- Option 1 — Bouton SVG officiel (recommandé) -->
<a href="<URL_AUTORISATION_FASOLOGIN>">
  <img
    src="https://app.fasologin.tino-ti.com/btn-fasologin.svg"
    alt="Se connecter avec FASO LOGIN"
    height="48"
  />
</a>

<!-- Option 2 — Bouton HTML pur (personnalisable) -->
<a href="<URL_AUTORISATION_FASOLOGIN>" class="fl-btn">
  Se connecter avec FASO LOGIN
</a>

<style>
.fl-btn {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  padding: 12px 20px;
  background: #15803d;
  color: white;
  border-radius: 10px;
  font-family: system-ui, sans-serif;
  font-size: 14px;
  font-weight: 600;
  text-decoration: none;
  transition: background 0.15s;
}
.fl-btn:hover { background: #166534; }
</style>

Flutter

ElevatedButton(
  style: ElevatedButton.styleFrom(
    backgroundColor: const Color(0xFF15803D),
    foregroundColor: Colors.white,
    padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 14),
    shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
  ),
  onPressed: () => FasoLoginClient.instance.login(),
  child: const Text(
    'Se connecter avec FASO LOGIN',
    style: TextStyle(fontWeight: FontWeight.w600, fontSize: 14),
  ),
)

Règles d'utilisation

  • Ne modifiez pas les couleurs officielles (#15803d)
  • Conservez le texte exact "Se connecter avec FASO LOGIN"
  • Hauteur minimale recommandée : 44px (accessibilité mobile)
  • Ne placez pas le bouton sur un fond de même couleur

Guides par stack

FASO LOGIN respecte le standard OpenID Connect — tout client OIDC existant fonctionne sans modification. Les exemples ci-dessous couvrent les stacks les plus utilisées.

PHP (vanilla + Laravel)

La bibliothèque jumbojett/openid-connect-php gère le PKCE, la découverte automatique des endpoints et le callback en une seule méthode.

composer require jumbojett/openid-connect-php
<?php
// auth.php — déclarer cette URL comme redirect_uri dans votre demande d'accès
session_start();
require 'vendor/autoload.php';
use Jumbojett\OpenIDConnectClient;

$oidc = new OpenIDConnectClient(
    getenv('FASOLOGIN_ISSUER'),       // https://api.fasologin.tino-ti.com/oidc
    getenv('FASOLOGIN_CLIENT_ID'),
    getenv('FASOLOGIN_CLIENT_SECRET')
);
$oidc->setRedirectURL('https://app.monservice.bf/auth/callback');
$oidc->addScope(['openid', 'profile', 'phone']);
$oidc->setCodeChallengeMethod('S256'); // PKCE — obligatoire

// Gère à la fois la redirection initiale ET le callback automatiquement
$oidc->authenticate();

$sub   = $oidc->requestUserInfo('sub');   // UUID immuable — votre FK en base
$phone = $oidc->requestUserInfo('phone_number');
$name  = $oidc->requestUserInfo('given_name');

$_SESSION['fasologin_sub'] = $sub;
header('Location: /dashboard');

Laravel : même bibliothèque, une seule route GET qui gère redirection et callback :

// routes/web.php
use Jumbojett\OpenIDConnectClient;

Route::get('/auth/fasologin', function () {
    $oidc = new OpenIDConnectClient(
        env('FASOLOGIN_ISSUER'), env('FASOLOGIN_CLIENT_ID'), env('FASOLOGIN_CLIENT_SECRET')
    );
    $oidc->setRedirectURL(route('auth.fasologin')); // même URL = redirect_uri enregistrée
    $oidc->addScope(['openid', 'profile', 'phone']);
    $oidc->setCodeChallengeMethod('S256');
    $oidc->authenticate();

    $sub  = $oidc->requestUserInfo('sub');
    $user = \App\Models\User::updateOrCreate(
        ['fasologin_sub' => $sub],
        ['name' => $oidc->requestUserInfo('given_name')]
    );
    Auth::login($user);
    return redirect('/dashboard');
})->name('auth.fasologin');

Colonne à ajouter dans votre table users : fasologin_sub VARCHAR(36) UNIQUE NOT NULL. Ne jamais stocker phone_number comme clé étrangère — le numéro peut changer.

Node.js / Express

Exemple pour openid-client v6.x (API fonctionnelle — Issuer/generators de la v5 ont été retirés). Pinnez la version majeure : un npm install openid-client sans version installe toujours la dernière v6+.

npm install openid-client@^6 express-session
import * as client from 'openid-client';
import express from 'express';
import session from 'express-session';

const app = express();
app.use(session({ secret: process.env.SESSION_SECRET, resave: false, saveUninitialized: false }));

// Initialiser une fois au démarrage du serveur
const config = await client.discovery(
  new URL(process.env.FASOLOGIN_ISSUER),
  process.env.FASOLOGIN_CLIENT_ID,
  process.env.FASOLOGIN_CLIENT_SECRET,
);

// Rediriger l'utilisateur vers FASO LOGIN
app.get('/auth/login', async (req, res) => {
  const state          = client.randomState();
  const code_verifier  = client.randomPKCECodeVerifier();
  const code_challenge = await client.calculatePKCECodeChallenge(code_verifier);
  req.session.state         = state;
  req.session.code_verifier = code_verifier;
  const redirectTo = client.buildAuthorizationUrl(config, {
    redirect_uri: process.env.FASOLOGIN_REDIRECT,
    scope: 'openid profile phone',
    state, code_challenge, code_challenge_method: 'S256',
  });
  res.redirect(redirectTo.href);
});

// Callback — même URL que redirect_uri enregistrée
app.get('/auth/callback', async (req, res) => {
  const currentUrl = new URL(req.originalUrl, process.env.FASOLOGIN_REDIRECT);
  const tokens = await client.authorizationCodeGrant(config, currentUrl, {
    pkceCodeVerifier: req.session.code_verifier,
    expectedState: req.session.state,
  });
  const userinfo   = await client.fetchUserInfo(config, tokens.access_token, tokens.claims().sub);
  req.session.user = { id: userinfo.sub }; // sub = UUID immuable
  res.redirect('/dashboard');
});
# .env
FASOLOGIN_ISSUER=https://api.fasologin.tino-ti.com/oidc
FASOLOGIN_CLIENT_ID=fasologin_xxxxxxxxxxxxxxxx
FASOLOGIN_CLIENT_SECRET=<votre_secret>
FASOLOGIN_REDIRECT=https://app.monservice.bf/auth/callback
SESSION_SECRET=<chaîne_aléatoire_longue>

WordPress (zéro code — configuration plugin)

Installez le plugin "OpenID Connect Generic" (auteur : daggerhart, 400 000+ installations actives) depuis le répertoire officiel WordPress. Version 3.9.0 minimum requise (PKCE).

Champ (Settings → OpenID Connect Generic)Valeur
Login TypeOpenID Connect button
Client IDfasologin_xxxxxxxxxxxxxxxx
Client Secret Key<votre_secret>
OpenID Scopeopenid profile phone
Login Endpoint URLhttps://api.fasologin.tino-ti.com/oidc/auth
Userinfo Endpoint URLhttps://api.fasologin.tino-ti.com/oidc/me
Token Validation Endpoint URLhttps://api.fasologin.tino-ti.com/oidc/token
End Session Endpoint URLhttps://api.fasologin.tino-ti.com/oidc/session/end
Identity Keysub
Enable PKCE✓ (cocher)

Le plugin affiche sa Redirect URI en bas de la page de configuration (ex : https://monsite.bf/?oidc-callback). Transmettez cette URI à l'admin FASO LOGIN lors de votre demande d'accès. Le mapping des champs profil (prénom, nom, email) se configure dans l'onglet "Attribute Mapping" du plugin.

Checklist avant production

  • ✓sub (UUID) utilisé comme clé étrangère — jamais phone_number ni email
  • ✓phone_number_verified: true vérifié avant tout accès sensible
  • ✓Refresh token stocké côté serveur (client confidentiel) ou en stockage sécurisé natif Keychain/Keystore (client public) — jamais en localStorage/AsyncStorage
  • ✓Refresh silencieux implémenté (access_token expire en 1h)
  • ✓POST /oidc/token/revocation appelé à la déconnexion
  • ✓Redirect URI(s) en HTTPS (web) ou custom scheme (mobile — reverse domain recommandé)
  • ✓client_secret dans les variables d'environnement, jamais committé
  • ✓Back-channel logout URI enregistrée et endpoint validé si sessions serveur utilisées

Erreurs courantes

ErreurCause probable
invalid_clientclient_id ou client_secret incorrect — ou client public qui envoie client_secret (à omettre pour les clients mobiles natifs)
invalid_grantCode expiré, déjà utilisé, ou code_verifier incorrect
redirect_uri_mismatchURI non enregistrée dans FASO LOGIN
invalid_scopeScope non accordé lors de l'enregistrement
unauthorized_clientClient suspendu ou non approuvé
access_deniedL'utilisateur a refusé le consentement

Rotation du client_secret

Depuis votre espace partenaire (ou via l'administrateur FASO LOGIN), deux options pour faire tourner votre client_secret :

  • "Régénérer le secret" (rotation normale) — l'ancien secret reste accepté pendant 1h, le temps de déployer le nouveau sans coupure de service.
  • "🚨 Secret compromis" (rotation d'urgence) — l'ancien secret est invalidé immédiatement, aucune grâce. À utiliser dès qu'une fuite est suspectée, quitte à couper temporairement votre intégration le temps de redéployer.