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.
| Champ | Obligatoire ? | Où le renseigner |
|---|---|---|
| Nom de l’application | Oui | À la demande |
| Email de contact | Oui | À la demande (auto si connecté à votre espace partenaire) |
| Description du projet | Oui (≥ 10 caractères) | À la demande |
| Redirect URI(s) — login | Oui (au moins 1) | À la demande |
| Scopes | Oui (openid inclus automatiquement) | À la demande |
| Post-logout redirect URI(s) | Non | À la demande, ou ajoutée ensuite (admin) |
| Backchannel logout URI | Non | /partner/settings, après approbation |
| Webhook URL + événements | Non | /partner/webhooks, après approbation |
| Logo (écran de consentement) | Non | /partner/settings, après approbation |
| Durée de vie access token | Non (défaut 1h) | /partner/settings, après approbation |
| Durée de vie refresh token | Non (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
| Scope | Claims retournés |
|---|---|
| openid | sub (identifiant unique — obligatoire) |
| profile | given_name, family_name, preferred_username, birthdate, gender, locale |
| phone | phone_number, phone_number_verified |
| email, email_verified | |
| address | locality, 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_xxxxxxxxxxxxxxxxJamais 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 LOGINaud= votreclient_ideventscontienthttp://schemas.openid.net/event/backchannel-logoutjtiunique — 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énement | Déclencheur | Champs data |
|---|---|---|
| user.consent_revoked | L'utilisateur révoque son consentement | sub, client_id |
| user.account_suspended | Un admin suspend le compte | sub |
| user.profile_updated | L'utilisateur modifie son profil | sub, 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
2xxen 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_secretOIDC — 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
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-sessionimport * 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 Type | OpenID Connect button |
| Client ID | fasologin_xxxxxxxxxxxxxxxx |
| Client Secret Key | <votre_secret> |
| OpenID Scope | openid profile phone |
| Login Endpoint URL | https://api.fasologin.tino-ti.com/oidc/auth |
| Userinfo Endpoint URL | https://api.fasologin.tino-ti.com/oidc/me |
| Token Validation Endpoint URL | https://api.fasologin.tino-ti.com/oidc/token |
| End Session Endpoint URL | https://api.fasologin.tino-ti.com/oidc/session/end |
| Identity Key | sub |
| 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
| Erreur | Cause probable |
|---|---|
| invalid_client | client_id ou client_secret incorrect — ou client public qui envoie client_secret (à omettre pour les clients mobiles natifs) |
| invalid_grant | Code expiré, déjà utilisé, ou code_verifier incorrect |
| redirect_uri_mismatch | URI non enregistrée dans FASO LOGIN |
| invalid_scope | Scope non accordé lors de l'enregistrement |
| unauthorized_client | Client suspendu ou non approuvé |
| access_denied | L'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.