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

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).

É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://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 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://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.

É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.

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

npm install openid-client express-session
import { 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://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/userinfo
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, 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

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

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.