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, CAS3 ou LTI 1.3), 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.
Ce que vous fournissez
Section intitulée « Ce que vous fournissez »| Élément | OIDC | SAML2 | CAS3 | LTI 1.3 |
|---|---|---|---|---|
| Identifiant client | client_id + client_secret |
entityID du SP + certificat |
nom de service | client_id de l’outil (+ clé publique / JWKS si services LTI) |
| URL de retour | redirect_uri (callback) |
ACS (Assertion Consumer Service) | service URL |
target_link_uri (URL de lancement) |
| URL de login (point d’entrée) | ✅ | ✅ | ✅ | ✅ initiate_login_uri (initiation OIDC tierce) |
| URL de logout (back-channel / SLO) | ✅ | ✅ | ✅ | ✅ (back-channel OIDC) |
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.
ProxyNinja est la plateforme LTI (rôle Platform, celui d’un LMS) ; votre
ressource est l’outil (rôle Tool). L’utilisateur est authentifié en amont
(GAR, ENT…) par ProxyNinja, puis lancé vers votre outil avec un id_token
signé qui porte les revendications LTI.
Choisissez LTI 1.3 si votre ressource est déjà un outil LTI (elle est distribuée via Moodle, Brightspace, Canvas…) : vous réutilisez la brique que vous avez, au lieu d’ajouter un client OIDC.
Nous vous fournissons :
- Issuer de la plateforme (
iss) :https://<EDITEUR>-auth.proxyninja.fr/realms/<EDITEUR> client_idde votre outil etdeployment_id(identifiant de déploiement)- Endpoint d’autorisation (lancement OIDC) :
…/realms/<EDITEUR>/protocol/openid-connect/auth - JWKS de la plateforme (pour valider le
id_tokende lancement) :…/realms/<EDITEUR>/protocol/openid-connect/certs - Endpoint de lancement de la plateforme :
…/realms/<EDITEUR>/lti-platform/launch
Vous nous fournissez :
initiate_login_uri— votre endpoint d’initiation OIDC tierce (third-party initiated login), appelé en premier par la plateforme.target_link_uri— l’URL de lancement de la ressource (elle doit aussi figurer dans vos redirect URIs valides).- La clé publique / l’URL JWKS de votre outil, uniquement si vous consommez
les services LTI Advantage (voir plus bas) : ils exigent que l’outil signe
ses propres jetons
client_credentials.
Paramètres du flux (ce sont ceux de la spécification LTI 1.3, pas un choix
ProxyNinja) : response_type=id_token, response_mode=form_post,
scope=openid, prompt=none, plus les state / nonce que vous devez
valider. Côté client Keycloak, cela se traduit par un flux implicite activé
et le flux standard désactivé — donc pas d’échange de code ni d’appel
/token pour le lancement : tout arrive dans le id_token posté sur votre
target_link_uri.
Revendications reçues dans le id_token (préfixe
https://purl.imsglobal.org/spec/lti/claim/) :
| Revendication | Contenu |
|---|---|
message_type · version |
LtiResourceLinkRequest · 1.3.0 |
deployment_id |
Le deployment_id que nous vous avons attribué — à vérifier |
target_link_uri |
L’URL de lancement demandée |
roles |
Rôles IMS, dérivés de l’attribut GAR profil (voir ci-dessous) |
context |
Contexte du lancement (id, label, title, type) |
resource_link |
Lien de ressource (id, title, description) |
tool_platform |
Identité de la plateforme (name, version, guid, product_family_code) |
custom |
Paramètres personnalisés autorisés : id_ressource, id_etab, sso_id |
Rôles. Le profil national GAR est traduit en URI de rôle IMS avant
émission — votre outil lit roles, pas profil :
| Profil GAR | Rôle LTI |
|---|---|
National_1, Enseignant |
…/vocab/lis/v2/membership#Instructor |
National_2, Eleve |
…/vocab/lis/v2/membership#Learner |
National_3 |
…/vocab/lis/v2/membership/Instructor#TeachingAssistant |
National_ENTDirecteur |
…/vocab/lis/v2/institution/person#Administrator |
National_ENTParent |
…/vocab/lis/v2/membership#Mentor |
Le rôle par défaut, quand le profil est absent ou inconnu, est Learner.
Traitez donc roles comme potentiellement dégradé et n’accordez jamais de
privilège d’enseignant sur la seule absence de valeur.
Services LTI Advantage exposés par la plateforme (optionnels, activés au cas par cas) :
| Service | Endpoint |
|---|---|
| NRPS (Names and Role Provisioning) | …/realms/<EDITEUR>/lti-services/memberships |
| AGS (Assignment and Grade Services) | …/realms/<EDITEUR>/lti-services/lineitems |
| Deep Linking | …/realms/<EDITEUR>/lti-services/deep-linking |
Votre LMS comme source d’identité
Section intitulée « Votre LMS comme source d’identité »Ce cas est l’inverse du précédent et ne change rien à votre intégration : c’est une question de source d’identité, pas de protocole côté ressource.
Quand les utilisateurs n’arrivent pas du GAR mais d’un LMS (Brightspace/D2L, Moodle, ENT compatible LTI), ProxyNinja se déclare comme outil LTI auprès de ce LMS et le rebranche comme fournisseur d’identité du realm — exactement comme il branche le GAR, École Directe ou Pronote. Votre ressource, elle, continue de parler OIDC / SAML2 / CAS3 (ou LTI) sans modification.
À enregistrer dans le LMS (ProxyNinja, côté outil) — <alias> est l’alias de
l’IDP, lti13 par défaut :
| Élément | Valeur |
|---|---|
| Login / OIDC initiation URL | https://<EDITEUR>-auth.proxyninja.fr/realms/<EDITEUR>/broker/<alias>/initiate |
| Redirect / launch URL | https://<EDITEUR>-auth.proxyninja.fr/realms/<EDITEUR>/broker/<alias>/endpoint |
| Public keyset (JWKS) | https://<EDITEUR>-auth.proxyninja.fr/realms/<EDITEUR>/protocol/openid-connect/certs |
À nous transmettre (côté plateforme, fourni par le LMS) : son issuer
(iss), son endpoint d’autorisation, son URL JWKS, le client_id
qu’il attribue à ProxyNinja et le deployment_id. Brightspace / D2L
bénéficie d’un traitement dédié (paramètres d2l_user_id, d2l_org_unit_id,
d2l_role).
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();
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()], ); 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, 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); }});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é |
| LTI 1.3 | packbackbooks/lti-1p3-tool (ou celtic-lti/lti) |
packbackbooks/lti-1p3-tool (intégré manuellement) |
ltijs |
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. - 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 — et, en LTI 1.3, également lenonce(usage unique) et ledeployment_id.
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