ajoute Moodle LTI 1.3 (V2), une 6e façon de rejoindre une classe
LTI authentifie l'élève et le fait rejoindre sa classe (même code que le code/QR existant), pas lancer un lab précis - relecture du CLAUDE.md qui liste LTI avec les autres façons d'ajouter des élèves. packbackbooks/lti-1p3-tool (Apache-2.0, recommandé par le CLAUDE.md), trois ponts vers Redis/cookies/MariaDB, élève retrouvé par identité Moodle stable pour ne jamais créer de doublon. Enregistrement d'une plateforme/d'un déploiement par commande Artisan (opération rare, faite à la main par Adam), jamais une page d'administration. Corrige au passage un vrai bug préexistant trouvé en vérifiant en réel sur le serveur : withoutMiddleware(ValidateCsrfToken::class) sur une route ne retire jamais le vrai middleware CSRF (ValidateCsrfToken est un alias déprécié de PreventRequestForgery, pas la classe réellement enregistrée) - /csp-rapport avait le même défaut depuis l'étape 14, invisible faute de surveillance. Corrigé pour les deux par la bonne méthode (validateCsrfTokens except, dans bootstrap/app.php). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
777dd1cbc8
commit
91d7cde4c1
27 changed files with 1293 additions and 9 deletions
26
portail/app/Http/Controllers/LtiClesController.php
Normal file
26
portail/app/Http/Controllers/LtiClesController.php
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\LtiPlateforme;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Packback\Lti1p3\JwksEndpoint;
|
||||
|
||||
/**
|
||||
* Clés publiques du portail (V2, LTI 1.3) : chaque plateforme Moodle
|
||||
* enregistrée a sa propre paire de clés (une ligne de lti_plateformes),
|
||||
* toutes publiées ici — Moodle n'en retient que celle dont le « kid »
|
||||
* correspond à ses propres jetons signés.
|
||||
*/
|
||||
class LtiClesController extends Controller
|
||||
{
|
||||
public function cles(): JsonResponse
|
||||
{
|
||||
$cles = LtiPlateforme::query()
|
||||
->whereNotNull('identifiant_cle')
|
||||
->pluck('cle_privee_outil', 'identifiant_cle')
|
||||
->all();
|
||||
|
||||
return response()->json(JwksEndpoint::new($cles)->getPublicJwks());
|
||||
}
|
||||
}
|
||||
37
portail/app/Http/Controllers/LtiConnexionController.php
Normal file
37
portail/app/Http/Controllers/LtiConnexionController.php
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Packback\Lti1p3\Interfaces\ICache;
|
||||
use Packback\Lti1p3\Interfaces\ICookie;
|
||||
use Packback\Lti1p3\Interfaces\IDatabase;
|
||||
use Packback\Lti1p3\LtiOidcLogin;
|
||||
use Packback\Lti1p3\OidcException;
|
||||
|
||||
/**
|
||||
* Première étape du lancement LTI 1.3 (V2) : Moodle appelle cette URL
|
||||
* (« initiate_login_url », enregistrée une fois par plateforme) pour
|
||||
* démarrer la connexion OIDC, avant de rediriger l'utilisateur vers la
|
||||
* vraie page de lancement — voir LtiLancementController.
|
||||
*/
|
||||
class LtiConnexionController extends Controller
|
||||
{
|
||||
public function connecter(Request $request, IDatabase $db, ICache $cache, ICookie $cookie): RedirectResponse
|
||||
{
|
||||
$donnees = $request->all();
|
||||
|
||||
if (! isset($donnees['target_link_uri'])) {
|
||||
abort(400, 'target_link_uri manquant.');
|
||||
}
|
||||
|
||||
try {
|
||||
$url = LtiOidcLogin::new($db, $cache, $cookie)->getRedirectUrl($donnees['target_link_uri'], $donnees);
|
||||
} catch (OidcException $e) {
|
||||
abort(400, $e->getMessage());
|
||||
}
|
||||
|
||||
return redirect()->away($url);
|
||||
}
|
||||
}
|
||||
98
portail/app/Http/Controllers/LtiLancementController.php
Normal file
98
portail/app/Http/Controllers/LtiLancementController.php
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Classe;
|
||||
use App\Models\Eleve;
|
||||
use App\Services\GenerateurCompteEleve;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\View\View;
|
||||
use Packback\Lti1p3\Factories\MessageFactory;
|
||||
use Packback\Lti1p3\LtiException;
|
||||
use Packback\Lti1p3\OidcException;
|
||||
|
||||
/**
|
||||
* Deuxième étape du lancement LTI 1.3 (V2, CLAUDE.md section 6) : Moodle
|
||||
* poste ici le jeton signé après la connexion OIDC
|
||||
* (LtiConnexionController). L'URL (une par classe, affichée dans « Faire
|
||||
* rejoindre tes élèves ») est à coller comme adresse de l'outil externe
|
||||
* dans l'activité Moodle du prof.
|
||||
*/
|
||||
class LtiLancementController extends Controller
|
||||
{
|
||||
public function lancer(Request $request, string $codeClasse, MessageFactory $factory): View|RedirectResponse
|
||||
{
|
||||
$classe = Classe::where('code_classe', $codeClasse)->first();
|
||||
|
||||
if ($classe === null || $classe->estArchivee()) {
|
||||
return view('lti.erreur', ['message' => 'Classe introuvable ou archivée.']);
|
||||
}
|
||||
|
||||
try {
|
||||
$lancement = $factory->create($request->all());
|
||||
} catch (LtiException|OidcException $e) {
|
||||
return view('lti.erreur', ['message' => $e->getMessage()]);
|
||||
}
|
||||
|
||||
$donnees = $lancement->getBody();
|
||||
$eleve = $this->trouverOuCreerEleve($classe, (string) $donnees['iss'], (string) $donnees['sub'], $donnees);
|
||||
|
||||
Auth::guard('eleve')->login($eleve);
|
||||
$request->session()->regenerate();
|
||||
$eleve->forceFill(['derniere_connexion_a' => now()])->save();
|
||||
$request->session()->put('jeton_session_eleve', $eleve->demarreNouvelleSession());
|
||||
|
||||
return redirect()->route('eleve');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $claims
|
||||
*/
|
||||
private function trouverOuCreerEleve(Classe $classe, string $emetteur, string $sub, array $claims): Eleve
|
||||
{
|
||||
$eleve = Eleve::where('classe_id', $classe->id)
|
||||
->where('lti_emetteur', $emetteur)
|
||||
->where('lti_sub', $sub)
|
||||
->first();
|
||||
|
||||
if ($eleve !== null) {
|
||||
return $eleve;
|
||||
}
|
||||
|
||||
[$prenom, $nom] = $this->nomDepuisClaims($claims);
|
||||
|
||||
$generateur = app(GenerateurCompteEleve::class);
|
||||
|
||||
return $classe->eleves()->create([
|
||||
'prenom' => $prenom,
|
||||
'nom' => $nom,
|
||||
'identifiant' => $generateur->genererIdentifiant($prenom, $nom),
|
||||
// Jamais utilisé : cet élève se connecte toujours via Moodle.
|
||||
'code_secret' => Hash::make($generateur->genererCodeSecret()),
|
||||
'lti_emetteur' => $emetteur,
|
||||
'lti_sub' => $sub,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $claims
|
||||
* @return array{0: string, 1: ?string}
|
||||
*/
|
||||
private function nomDepuisClaims(array $claims): array
|
||||
{
|
||||
if (filled($claims['given_name'] ?? null)) {
|
||||
return [$claims['given_name'], $claims['family_name'] ?? null];
|
||||
}
|
||||
|
||||
if (filled($claims['name'] ?? null)) {
|
||||
$parties = explode(' ', trim($claims['name']), 2);
|
||||
|
||||
return [$parties[0], $parties[1] ?? null];
|
||||
}
|
||||
|
||||
return ['Élève', 'Moodle'];
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue