Guide d'intégration
Ce guide décrit ce que vous devez fournir et mettre en œuvre pour intégrer votre ressource à ProxyNinja : la configuration d’authentification (OIDC, SAML2 ou CAS3), le point d’entrée fonctionnel (vérification de session), et des exemples de code par stack (PHP, Laravel, Node), y compris la déconnexion et le rafraîchissement des jetons.
Ce que vous fournissez
Section intitulée « Ce que vous fournissez »| Élément | OIDC | SAML2 | CAS3 |
|---|---|---|---|
| Identifiant client | client_id + client_secret |
entityID du SP + certificat |
nom de service |
| URL de retour | redirect_uri (callback) |
ACS (Assertion Consumer Service) | service URL |
| URL de login (point d’entrée) | ✅ | ✅ | ✅ |
| URL de logout (back-channel / SLO) | ✅ | ✅ | ✅ |
Vous fournissez ces éléments pour la préproduction et la production. Voir le questionnaire, sections B et F.
Configuration de l’authentification
Section intitulée « Configuration de l’authentification »Rappel des bases realm (<EDITEUR> = votre realm = le slug de votre tenant) :
| Environnement | Modèle de déploiement | Base realm |
|---|---|---|
| Préproduction | instance partagée | https://preprod-proxy.gar.ninja/realms/<EDITEUR> |
| Production | instance dédiée par éditeur | https://<EDITEUR>-auth.proxyninja.fr/realms/<EDITEUR> |
OIDC (OpenID Connect)
Section intitulée « OIDC (OpenID Connect) »ProxyNinja est l’OP (OpenID Provider). Tout se découvre depuis le document
.well-known — ne codez pas les endpoints en dur, lisez-les depuis la
découverte.
- Issuer :
https://<EDITEUR>-auth.proxyninja.fr/realms/<EDITEUR> - Découverte :
…/realms/<EDITEUR>/.well-known/openid-configuration - Endpoints (exposés par la découverte) :
authorization_endpoint:…/protocol/openid-connect/authtoken_endpoint:…/protocol/openid-connect/tokenuserinfo_endpoint:…/protocol/openid-connect/userinfoend_session_endpoint:…/protocol/openid-connect/logoutjwks_uri:…/protocol/openid-connect/certs
- Flow :
Authorization Code+ PKCE (S256). - Scopes :
openid(obligatoire)profileemail, plus les scopes d’attributs établissement si activés sur votre realm. - Authentification client :
client_secret_basic(ouclient_secret_post).
ProxyNinja est l’IdP ; votre application est le SP.
- Métadonnées IdP (à consommer) :
https://<EDITEUR>-auth.proxyninja.fr/realms/<EDITEUR>/protocol/saml/descriptor - SSO / SLO :
https://<EDITEUR>-auth.proxyninja.fr/realms/<EDITEUR>/protocol/saml - À fournir (métadonnées SP) :
entityID, URL ACS (bindingHTTP-POST), URL SLO, format deNameIDsouhaité, et le certificat de signature du SP. - Indiquez si vous exigez des assertions signées/chiffrées (recommandé : assertions signées).
ProxyNinja expose le protocole CAS (extension CAS de Keycloak). Vous agissez en client CAS.
- Base CAS :
https://<EDITEUR>-auth.proxyninja.fr/realms/<EDITEUR>/protocol/caslogin:…/protocol/cas/login?service=<votre-service>validate (CAS3, avec attributs):…/protocol/cas/p3/serviceValidatelogout:…/protocol/cas/logout
- À fournir : l’URL
service(votre point d’entrée) et l’URL de logout pour la propagation.
Point d’entrée fonctionnel
Section intitulée « Point d’entrée fonctionnel »Le cœur de l’intégration côté éditeur est un point d’entrée qui implémente une seule décision :
Requête entrante sur le point d'entrée │ ▼Ai-je une session locale valide ? │ ┌────┴─────┐ OUI NON │ │ ▼ ▼Servir la Demander l'authentification à ProxyNinjaressource (OIDC /authorize · SAML AuthnRequest · CAS /login) │ ▼ Retour (code / ticket) → échange jetons → récupérer les attributs │ ▼ Créer la session locale → servir la ressourceComme l’utilisateur arrive déjà authentifié depuis le médiacentre, la demande à ProxyNinja est en général transparente (pas de re-saisie). Voir le parcours de bout en bout.
Exemples de code par stack
Section intitulée « Exemples de code par stack »Bibliothèques recommandées (voir le récapitulatif en bas de page) :
- PHP :
jumbojett/openid-connect-php - Laravel : Socialite +
socialiteproviders/keycloak - Node :
openid-client(certifié OpenID)
OIDC — point d’entrée + récupération des attributs
Section intitulée « OIDC — point d’entrée + récupération des attributs »composer require jumbojett/openid-connect-php<?phprequire 'vendor/autoload.php';use Jumbojett\OpenIDConnectClient;
session_start();
// --- Point d'entrée : ai-je une session locale ? ---if (!empty($_SESSION['user'])) { render_resource($_SESSION['user']); // OUI → on sert la ressource exit;}
// NON → on demande l'identité à ProxyNinja.$oidc = new OpenIDConnectClient( 'https://<EDITEUR>-auth.proxyninja.fr/realms/<EDITEUR>', // issuer (sans slash final) 'votre-client-id', 'votre-client-secret');$oidc->setRedirectURL('https://votre-ressource.fr/callback');$oidc->addScope(['openid', 'profile', 'email']);$oidc->setCodeChallengeMethod('S256'); // PKCE recommandé
$oidc->authenticate(); // redirige vers ProxyNinja, puis revient ici$claims = $oidc->requestUserInfo(); // récupération des attributs (/userinfo)
$_SESSION['user'] = (array) $claims;$_SESSION['id_token'] = $oidc->getIdToken();$_SESSION['access_token'] = $oidc->getAccessToken();$_SESSION['refresh_token'] = $oidc->getRefreshToken();
render_resource($_SESSION['user']);composer require socialiteproviders/keycloak// config/services.php — le provider Keycloak pointe sur votre realm ProxyNinja'keycloak' => [ 'client_id' => env('PROXYNINJA_CLIENT_ID'), 'client_secret' => env('PROXYNINJA_CLIENT_SECRET'), 'redirect' => env('PROXYNINJA_REDIRECT_URI'), 'base_url' => env('PROXYNINJA_BASE_URL'), // p.ex. https://<EDITEUR>-auth.proxyninja.fr (hôte communiqué à l'onboarding) 'realms' => env('PROXYNINJA_REALM'), // <EDITEUR>],// app/Providers/AppServiceProvider.php (Laravel 11/12) — enregistrer le provideruse Illuminate\Support\Facades\Event;use SocialiteProviders\Manager\SocialiteWasCalled;
public function boot(): void{ Event::listen(function (SocialiteWasCalled $event) { $event->extendSocialite('keycloak', \SocialiteProviders\Keycloak\Provider::class); });}use Illuminate\Support\Facades\Route;use Laravel\Socialite\Facades\Socialite;
// --- Point d'entrée : ai-je une session locale ? ---Route::get('/login', function () { if (auth()->check()) { return redirect('/'); // OUI → ressource } // NON → demander l'identité à ProxyNinja (scope openid obligatoire) return Socialite::driver('keycloak')->scopes(['openid', 'profile'])->redirect();});
Route::get('/callback', function () { $gar = Socialite::driver('keycloak')->user(); // attributs + jetons $user = User::updateOrCreate( ['proxyninja_sub' => $gar->getId()], ['name' => $gar->getName(), 'email' => $gar->getEmail()], ); session(['pn_refresh_token' => $gar->refreshToken]); auth()->login($user, remember: true); // session locale return redirect('/');});npm install openid-client express-sessionimport * as client from 'openid-client';import express from 'express';import session from 'express-session';
// Découverte au démarrage (ne codez pas les endpoints en dur)const issuer = new URL('https://<EDITEUR>-auth.proxyninja.fr/realms/<EDITEUR>');const config = await client.discovery(issuer, 'votre-client-id', 'votre-client-secret');
const app = express();app.use(session({ secret: 'changez-moi', resave: false, saveUninitialized: false }));
// --- Point d'entrée : ai-je une session locale ? ---app.get('/', (req, res) => { if (req.session.user) return res.send(`Bonjour ${req.session.user.name}`); // OUI res.redirect('/login'); // NON});
app.get('/login', (req, res) => { const code_verifier = client.randomPKCECodeVerifier(); const state = client.randomState(); req.session.pkce = { code_verifier, state }; client.calculatePKCECodeChallenge(code_verifier).then((code_challenge) => { const url = client.buildAuthorizationUrl(config, { redirect_uri: 'https://votre-ressource.fr/callback', scope: 'openid profile email', code_challenge, code_challenge_method: 'S256', state, }); res.redirect(url.href); });});
app.get('/callback', async (req, res) => { const currentUrl = new URL(req.originalUrl, 'https://votre-ressource.fr'); const tokens = await client.authorizationCodeGrant(config, currentUrl, { pkceCodeVerifier: req.session.pkce.code_verifier, expectedState: req.session.pkce.state, }); const claims = tokens.claims(); // claims id_token const userinfo = await client.fetchUserInfo(config, tokens.access_token, claims.sub);
req.session.user = userinfo; // attributs req.session.tokens = { access_token: tokens.access_token, refresh_token: tokens.refresh_token, expires_at: Date.now() + (tokens.expires_in ?? 0) * 1000, }; res.redirect('/'); // session locale créée});
app.listen(3000);Déconnexion (back-channel logout)
Section intitulée « Déconnexion (back-channel logout) »ProxyNinja notifie votre ressource lorsqu’une session doit être fermée
(POST d’un logout_token JWT). Votre endpoint doit : (1) valider le
logout_token (signature via le jwks_uri du realm), (2) extraire sub
et/ou sid, (3) détruire la session locale correspondante — en quelques
secondes, sans attendre l’expiration d’un jeton.
POST /backchannel_logout HTTP/1.1Host: votre-ressource.frContent-Type: application/x-www-form-urlencoded
logout_token=eyJhbGci....eyJpc3Mi....T3BlbklE..<?phprequire 'vendor/autoload.php';use Jumbojett\OpenIDConnectClient;
// Endpoint POST /backchannel_logout$logoutToken = $_POST['logout_token'] ?? '';
$oidc = new OpenIDConnectClient( 'https://<EDITEUR>-auth.proxyninja.fr/realms/<EDITEUR>', 'votre-client-id', 'votre-client-secret');
// Valide la signature + les claims (iss, aud, events, sid/sub) via le JWKS du realmif ($oidc->verifyLogoutToken($logoutToken)) { $sid = $oidc->getSidFromBackChannel(); // ou claim `sub` destroy_local_sessions_for($sid); // votre logique (table sid → session) http_response_code(200);} else { http_response_code(400);}// routes/web.php — endpoint public (CSRF exempté)use Jumbojett\OpenIDConnectClient;
Route::post('/backchannel-logout', function (\Illuminate\Http\Request $request) { $oidc = new OpenIDConnectClient( config('services.keycloak.base_url').'/realms/'.config('services.keycloak.realms'), config('services.keycloak.client_id'), config('services.keycloak.client_secret'), ); if (! $oidc->verifyLogoutToken($request->input('logout_token'))) { abort(400); } $sid = $oidc->getSidFromBackChannel(); // Invalider la/les session(s) Laravel associées à ce sid (table de correspondance) \DB::table('sessions')->where('pn_sid', $sid)->delete(); return response()->noContent();})->withoutMiddleware(\Illuminate\Foundation\Http\Middleware\VerifyCsrfToken::class);import express from 'express';import * as jose from 'jose';
// JWKS du realm pour vérifier la signature du logout_tokenconst JWKS = jose.createRemoteJWKSet( new URL('https://<EDITEUR>-auth.proxyninja.fr/realms/<EDITEUR>/protocol/openid-connect/certs'),);
app.post('/backchannel-logout', express.urlencoded({ extended: false }), async (req, res) => { try { const { payload } = await jose.jwtVerify(req.body.logout_token, JWKS, { issuer: 'https://<EDITEUR>-auth.proxyninja.fr/realms/<EDITEUR>', audience: 'votre-client-id', }); // Vérifier l'event de logout puis détruire la session liée à sid/sub if (!payload.events?.['http://schemas.openid.net/event/backchannel-logout']) { return res.sendStatus(400); } await destroySessionsFor(payload.sid ?? payload.sub); // votre store de sessions res.sendStatus(200); } catch { res.sendStatus(400); }});Rafraîchissement des jetons
Section intitulée « Rafraîchissement des jetons »Rafraîchissez l’access_token avant son expiration (avec le
refresh_token) plutôt que de forcer une reconnexion.
// Avant un appel protégé, si l'access_token est proche de l'expiration :$oidc->refreshToken($_SESSION['refresh_token']);$_SESSION['access_token'] = $oidc->getAccessToken();$_SESSION['refresh_token'] = $oidc->getRefreshToken(); // rotation éventuelleuse Laravel\Socialite\Facades\Socialite;
// Rejoue le grant refresh_token via le provider Socialite Keycloak$fresh = Socialite::driver('keycloak')->refreshToken(session('pn_refresh_token'));session(['pn_refresh_token' => $fresh->refreshToken]);// $fresh->token = nouvel access_token// Rafraîchit si l'access_token expire bientôtif (Date.now() > req.session.tokens.expires_at - 30_000) { const refreshed = await client.refreshTokenGrant(config, req.session.tokens.refresh_token); req.session.tokens.access_token = refreshed.access_token; req.session.tokens.refresh_token = refreshed.refresh_token ?? req.session.tokens.refresh_token; req.session.tokens.expires_at = Date.now() + (refreshed.expires_in ?? 0) * 1000;}Bibliothèques recommandées (récapitulatif)
Section intitulée « Bibliothèques recommandées (récapitulatif) »| Protocole | PHP | Laravel | Node.js |
|---|---|---|---|
| OIDC | jumbojett/openid-connect-php |
Socialite + socialiteproviders/keycloak (ou Kovah/laravel-socialite-oidc) |
openid-client (+ openid-client/passport) |
| SAML2 | onelogin/php-saml (ou simpleSAMLphp en mode SP) |
aacotroneo/laravel-saml2 (basé sur OneLogin) |
@node-saml/passport-saml (ou samlify) |
| CAS3 | apereo/phpCAS |
apereo/phpCAS (intégré manuellement) |
connect-cas2 / client CAS dédié |
Bonnes pratiques
Section intitulée « Bonnes pratiques »- Stockage sécurisé des jetons : jamais en clair ; cookies de session
serveur (
HttpOnly,Secure,SameSite) ou store serveur. - PKCE (S256) systématique sur le flow OIDC
Authorization Code. - Découverte
.well-known: lisez les endpoints depuis la découverte, ne les codez pas en dur. - Renouvellement automatique : rafraîchissez avant expiration.
- Déconnexion en quelques secondes : propagez le back-channel logout immédiatement ; n’attendez jamais l’expiration d’un jeton.
- Validez toujours la signature des jetons (
id_token,logout_token) via lejwks_uridu realm.
La suite
Section intitulée « La suite »- Quels attributs allez-vous recevoir ? → Référence des attributs GAR
- Besoin d’une recommandation pour votre cas ? → Questionnaire