Skip to Content

Chapitre 10.1 — LTI Advantage : vue d’ensemble

⏱️ TL;DRLTI Advantage = LTI 1.3 Core (le lancement de la Partie 9) plus trois services : Deep Linking, AGS (notes) et NRPS (roster). Les endpoints de ces services n’arrivent pas par magie : ils sont posés dans le id_token du lancement, sous forme de claims (.../lti-ags/claim/endpoint, .../lti-nrps/claim/namesroleservice). Pour appeler un service, ton outil ne réutilise pas l’id_token : il obtient un jeton d’accès OAuth2 auprès du token endpoint de la plateforme, via le grant client_credentials et un JWT client_assertion signé avec sa propre clé privée. Le jeton renvoyé est un bearer que tu présentes aux endpoints de service. Le scope demandé borne ce que ce jeton a le droit de faire.

🎯 Objectifs

  • Situer LTI Advantage : Core + Deep Linking + AGS + NRPS.
  • Repérer d’où viennent les endpoints de service (les claims du id_token).
  • Obtenir un jeton d’accès via client_credentials + client_assertion.
  • Comprendre le rôle du scope et demander le minimum nécessaire.

Core, plus trois services

La Partie 9 t’a appris le lancement : l’élève clique, ton outil valide le id_token, affiche son contenu. C’est LTI 1.3 Core. Utile, mais unidirectionnel : l’information va de la plateforme vers ton outil, et s’arrête là.

LTI Advantage ajoute la conversation retour : ton outil peut, à son tour, appeler la plateforme. Trois services, trois usages :

ServiceÀ quoi ça sertSens du flux
Deep LinkingL’enseignant choisit quel contenu de ton outil placer dans le cours (au lieu d’une URL en dur).Outil ⟶ Plateforme (renvoie un choix)
AGS (Assignment and Grade Services)Ton outil renvoie des notes dans le carnet (colonnes, scores, résultats).Outil ⟶ Plateforme (écrit/lit des notes)
NRPS (Names and Role Provisioning Services)Ton outil lit la liste des membres du cours et leurs rôles.Outil ⟵ Plateforme (lit le roster)

📚 La spec — « LTI Advantage » n’est pas une version : c’est un regroupement de LTI 1.3 Core + les trois services, chacun spécifié à part par 1EdTech (Deep Linking, Assignment and Grade Services, Names and Role Provisioning Services). Un produit peut n’en implémenter qu’un ; la certification couvre chaque service séparément (chapitre 10.5). Détails sur 1edtech.org.

D’où viennent les endpoints de service

Tu ne « devines » jamais l’URL d’un service. La plateforme te la donne à chaque lancement, dans un claim du id_token (celui que tu as validé au chapitre 9.4). Deux claims de service à connaître :

{ "https://purl.imsglobal.org/spec/lti-ags/claim/endpoint": { "scope": [ "https://purl.imsglobal.org/spec/lti-ags/scope/lineitem", "https://purl.imsglobal.org/spec/lti-ags/scope/result.readonly", "https://purl.imsglobal.org/spec/lti-ags/scope/score" ], "lineitems": "https://moodle.ecole.fr/mod/lti/services.php/2/lineitems", "lineitem": "https://moodle.ecole.fr/mod/lti/services.php/2/lineitems/5/lineitem" }, "https://purl.imsglobal.org/spec/lti-nrps/claim/namesroleservice": { "context_memberships_url": "https://moodle.ecole.fr/mod/lti/services.php/CtxMemberships.php/2", "service_versions": ["2.0"] } }
  • Le claim AGS (.../lti-ags/claim/endpoint) porte scope (les scopes que cette plateforme t’autorise), lineitems (l’URL du conteneur de colonnes du cours) et lineitem (l’URL de la colonne liée à cette activité, si elle existe déjà). On l’exploite au chapitre 10.3.
  • Le claim NRPS (.../lti-nrps/claim/namesroleservice) porte context_memberships_url (l’URL du roster de ce cours) et service_versions. On l’exploite au chapitre 10.4.

💡 RéflexePersiste ces URLs au moment du lancement, indexées par sub + iss + context.id. Un renvoi de note peut arriver plus tard (l’élève finit l’exercice à 22 h, ton job asynchrone poste le score à 22 h 05) : tu n’auras plus l’id_token sous la main. Range l’endpoint AGS avec le contexte, pas dans la requête HTTP en cours.

⚠️ Piège — Croire que l’id_token du lancement est le jeton pour appeler les services. Non. L’id_token prouve qui est l’élève et livre les URLs de service. Pour appeler un service, il te faut un autre jeton — un access_token OAuth2 que tu vas chercher toi-même. C’est l’objet de la section suivante.

Obtenir un jeton d’accès

Appeler AGS ou NRPS, c’est appeler une API REST protégée. Il te faut donc un access_token OAuth2 (de type bearer). Tu l’obtiens auprès du token endpoint de la plateforme (reçu à l’enregistrement, chapitre 9.5) avec le grant client_credentials — mais pas avec un secret partagé : avec un JWT client_assertion que ton outil signe avec sa clé privée.

Voici la requête complète :

POST /mod/lti/token.php HTTP/1.1 Host: moodle.ecole.fr Content-Type: application/x-www-form-urlencoded grant_type=client_credentials &client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer &client_assertion=eyJhbGciOiJSUzI1NiIsImtpZCI6InN0dWRpby0yMDI2In0.eyJpc3M... &scope=https://purl.imsglobal.org/spec/lti-ags/scope/score%20https://purl.imsglobal.org/spec/lti-nrps/scope/contextmembership.readonly

Chaque paramètre a un rôle précis :

  • grant_type=client_credentials : « je m’authentifie en tant qu’application », pas au nom d’un utilisateur.
  • client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer : « ma preuve d’identité est un JWT ».
  • client_assertion : le JWT signé par ta clé privée. C’est lui qui prouve que tu es bien ton outil. La plateforme le vérifie avec ton jwks_uri (celui que tu lui as donné à l’enregistrement).
  • scope : la liste des scopes souhaités, séparés par des espaces (encodés %20 dans le corps). Tu ne demandes que ce dont tu as besoin.

Le client_assertion, décodé, est un JWT très court :

{ "iss": "formacampus-studio-client-id", "sub": "formacampus-studio-client-id", "aud": "https://moodle.ecole.fr/mod/lti/token.php", "iat": 1893455940, "exp": 1893456240, "jti": "req-a1b2c3-unique" }
  • iss et sub valent ton client_id : l’émetteur du jeton, c’est toi, l’outil.
  • aud : le token endpoint visé (l’audience de cette assertion).
  • iat / exp : émis maintenant, très courte durée de vie (quelques minutes).
  • jti : un identifiant unique par requête (anti-rejeu).

Le header du JWT porte l’alg (RS256) et le kid de ta clé — pour que la plateforme choisisse la bonne clé publique dans ton JWKS.

La plateforme répond avec le jeton :

HTTP/1.1 200 OK Content-Type: application/json { "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9...", "token_type": "Bearer", "expires_in": 3600, "scope": "https://purl.imsglobal.org/spec/lti-ags/scope/score" }

Note deux choses : token_type est Bearer (tu le mettras dans l’en-tête Authorization), et le scope renvoyé peut être plus restreint que celui demandé (la plateforme ne t’accorde que ce à quoi tu as droit). Ensuite, tu appelles le service :

POST /mod/lti/services.php/2/lineitems/5/scores HTTP/1.1 Host: moodle.ecole.fr Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9... Content-Type: application/vnd.ims.lis.v1.score+json { "...": "objet Score, voir chapitre 10.3" }

💡 RéflexeMets en cache l’access_token jusqu’à son expires_in (moins une marge de sécurité) et réutilise-le pour toutes tes requêtes vers cette plateforme. Redemander un jeton à chaque appel HTTP, c’est doubler les allers-retours et marteler le token endpoint pour rien. Un jeton par plateforme (voire par jeu de scopes), rafraîchi à l’expiration.

Les scopes

Un scope est une IRI qui nomme une permission. Le jeton que tu obtiens ne peut faire que ce que ses scopes autorisent. Ceux de LTI Advantage :

Scope (IRI)Ce qu’il autoriseService
https://purl.imsglobal.org/spec/lti-ags/scope/lineitemGérer les colonnes du carnet (créer, lire, modifier).AGS
https://purl.imsglobal.org/spec/lti-ags/scope/scorePublier des scores dans une colonne.AGS
https://purl.imsglobal.org/spec/lti-ags/scope/result.readonlyLire les résultats d’une colonne.AGS
https://purl.imsglobal.org/spec/lti-nrps/scope/contextmembership.readonlyLire le roster du cours.NRPS

Deep Linking, lui, ne passe pas par un scope de service : il fonctionne par lancement dédié (LtiDeepLinkingRequest) et réponse signée, sans jeton d’accès (chapitre 10.2).

⚠️ Piège — Demander tous les scopes « au cas où ». Si tu ne fais que poster des notes, ne demande que .../lti-ags/scope/score. Un jeton sur-scopé est une surface d’attaque inutile, et certaines plateformes refusent un scope non prévu à ta configuration — la requête token échoue alors avec une erreur invalid_scope déroutante. Principe du moindre privilège (on y revient en 10.5).

Le flux, en un diagramme

Retiens la séquence : forger l’assertion → échanger contre un access_token → appeler le service avec le bearer. Les trois services partagent exactement ce préambule ; seule change l’URL appelée et le scope demandé.

🔌 Côté intégration — Ce mécanisme de jeton est le même pour AGS et NRPS. Quand tu industrialises ton outil, factorise-le : une fonction getServiceToken(platform, scopes) qui forge l’assertion, appelle le token endpoint, met en cache et renvoie le bearer. Tes implémentations AGS et NRPS ne font ensuite que consommer ce bearer. C’est ce niveau de propreté que vérifient les tests de certification.

🧭 Sur FormaCampus — Le studio expose déjà un jwks_uri (donné aux écoles à l’enregistrement). Pour LTI Advantage, l’équipe ajoute un module services/token.ts qui, à partir de l’iss d’une école, forge un client_assertion (signé par la clé privée du studio), appelle le token endpoint de cette école et cache le access_token par (iss, scope). Les modules AGS (renvoi de note) et NRPS (roster) s’appuient dessus. Premier jalon franchi : le studio sait parler à la plateforme, plus seulement l’écouter.

📚 La spec — L’obtention du jeton suit OAuth 2.0 Client Credentials (RFC 6749) avec l’authentification client par JWT Bearer (RFC 7523) : client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer. 1EdTech en fixe le profil dans LTI 1.3 Security Framework et les IRIs de scope dans chaque spec de service. Les types de média (application/vnd.ims.lis.v1.score+json, etc.) sont normés — vois 1edtech.org.

✏️ Exercices

Exercice 1 — Retrouve l’endpoint. Ton outil reçoit un lancement validé. Tu veux poster une note. Dans quel claim du id_token trouves-tu l’URL où poster, et quel jeton utilises-tu pour appeler cette URL ?

✅ Solution

L’URL est dans le claim https://purl.imsglobal.org/spec/lti-ags/claim/endpoint, champ lineitem (la colonne liée à l’activité) — on postera sur {lineitem}/scores. Le jeton n’est pas l’id_token : c’est un access_token OAuth2 obtenu au token endpoint via grant_type=client_credentials + un client_assertion (JWT signé par ta clé privée), avec le scope .../lti-ags/scope/score. On présente ce bearer dans l’en-tête Authorization.

Exercice 2 — Corrige la requête. Un dev poste ceci pour obtenir un jeton et reçoit une erreur invalid_scope :

POST /mod/lti/token.php HTTP/1.1 Content-Type: application/x-www-form-urlencoded grant_type=client_credentials &client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer &client_assertion=eyJ... &scope=https://purl.imsglobal.org/spec/lti-ags/scope/score,https://purl.imsglobal.org/spec/lti-nrps/scope/contextmembership.readonly

Qu’est-ce qui cloche ?

✅ Solution

Les scopes sont séparés par une virgule. Le paramètre scope OAuth2 attend des scopes séparés par des espaces (encodés %20 dans un corps x-www-form-urlencoded), pas par des virgules. La plateforme lit alors un « scope » unique invalide .../score,.../contextmembership.readonly et répond invalid_scope. Corriger : remplacer la virgule par %20. (Vérifier aussi que ces scopes sont bien autorisés pour ce client_id dans la config de la plateforme.)

🧠 Quiz de révision

1. Que regroupe « LTI Advantage » ?

LTI 1.3 Core (le lancement) plus trois services : Deep Linking (choix de contenu), AGS (notes) et NRPS (roster). Ce n’est pas une version, c’est un ensemble de specs certifiables séparément.

2. Où trouve-t-on les URLs des services AGS et NRPS ?

Dans les claims du id_token du lancement : .../lti-ags/claim/endpoint (champs lineitems, lineitem, scope) pour AGS, et .../lti-nrps/claim/namesroleservice (champ context_memberships_url) pour NRPS.

3. Comment un outil obtient-il un jeton pour appeler un service ?

Il POST au token endpoint de la plateforme avec grant_type=client_credentials, client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer et un client_assertion = JWT signé par sa clé privée, plus le scope voulu. Il reçoit un access_token de type Bearer.

4. Peut-on réutiliser l’id_token du lancement pour appeler AGS ?

Non. L’id_token identifie l’utilisateur et livre les URLs de service, mais n’autorise pas les appels d’API. Il faut un access_token OAuth2 distinct, obtenu séparément.

5. À quoi sert le scope, et quelle règle appliquer ?

Le scope borne ce que le jeton peut faire (poster un score, lire le roster…). Règle : demander le minimum nécessaire (moindre privilège). La plateforme peut accorder un scope plus restreint que celui demandé, et refuse (invalid_scope) un scope non prévu à ta config.


Chapitre suivant : Deep Linking — comment l’enseignant choisit, dans ton outil, le contenu à placer dans son cours.

Last updated on