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://auth-sandbox.fasologin.tino-ti.com/.well-known/openid-configurationÉtape 1 — Enregistrement
Soumettez une demande d'accès avec votre email, vos redirect URIs et les scopes nécessaires. L'admin valide sous 48h ouvrées.
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).
É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/userinfo
Authorization: Bearer <access_token>Étape 4 — Refresh tokens
Stockez le refresh_token côté serveur uniquement, jamais dans le client mobile. Implémentez le refresh silencieux — l'access_token expire en 1h par défaut.
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 : omettez client_secret.
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://auth-sandbox.fasologin.tino-ti.com'
);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 auprès de l'admin FASO LOGIN (champ disponible dans la page de modification du client). 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://auth-sandbox.fasologin.tino-ti.com/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://auth-sandbox.fasologin.tino-ti.com/jwks
// Vérification avec jose (npm install jose)
import { jwtVerify, createRemoteJWKSet } from 'jose';
const JWKS = createRemoteJWKSet(
new URL('https://auth-sandbox.fasologin.tino-ti.com/jwks')
);
async function verifyLogoutToken(token: string) {
const { payload } = await jwtVerify(token, JWKS, {
issuer: 'https://auth-sandbox.fasologin.tino-ti.com',
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.
É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.
Node.js / TypeScript
import * as crypto from 'crypto';
import express from 'express';
const WEBHOOK_SECRET = process.env.FASOLOGIN_WEBHOOK_SECRET!;
app.post('/webhooks/fasologin', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['x-fasologin-signature'] as string;
const expected = 'sha256=' + crypto
.createHmac('sha256', WEBHOOK_SECRET)
.update(req.body)
.digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
return res.status(401).json({ error: 'invalid_signature' });
}
const payload = JSON.parse(req.body.toString());
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);
if (!hash_equals($expected, $sig)) {
http_response_code(401);
exit;
}
$payload = json_decode($body, true);
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://sandbox.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://auth-sandbox.fasologin.tino-ti.com
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
npm install openid-client express-sessionimport { Issuer, generators } 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 issuer = await Issuer.discover(process.env.FASOLOGIN_ISSUER);
const client = new issuer.Client({
client_id: process.env.FASOLOGIN_CLIENT_ID,
client_secret: process.env.FASOLOGIN_CLIENT_SECRET,
redirect_uris: [process.env.FASOLOGIN_REDIRECT],
response_types: ['code'],
});
// Rediriger l'utilisateur vers FASO LOGIN
app.get('/auth/login', (req, res) => {
const state = generators.state();
const code_verifier = generators.codeVerifier();
const code_challenge = generators.codeChallenge(code_verifier);
req.session.state = state;
req.session.code_verifier = code_verifier;
res.redirect(client.authorizationUrl({
scope: 'openid profile phone',
state, code_challenge, code_challenge_method: 'S256',
}));
});
// Callback — même URL que redirect_uri enregistrée
app.get('/auth/callback', async (req, res) => {
const params = client.callbackParams(req);
const tokens = await client.callback(process.env.FASOLOGIN_REDIRECT, params, {
state: req.session.state, code_verifier: req.session.code_verifier,
});
const userinfo = await client.userinfo(tokens.access_token);
req.session.user = { id: userinfo.sub }; // sub = UUID immuable
res.redirect('/dashboard');
});# .env
FASOLOGIN_ISSUER=https://auth-sandbox.fasologin.tino-ti.com
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://auth-sandbox.fasologin.tino-ti.com/auth |
| Userinfo Endpoint URL | https://auth-sandbox.fasologin.tino-ti.com/userinfo |
| Token Validation Endpoint URL | https://auth-sandbox.fasologin.tino-ti.com/token |
| End Session Endpoint URL | https://auth-sandbox.fasologin.tino-ti.com/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, jamais dans le client mobile
- ✓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
Contactez l'administrateur FASO LOGIN pour faire tourner votre client_secret. L'ancien secret est invalidé immédiatement — mettez à jour vos variables d'environnement avant de demander la rotation.