Guida tecnica
Integrare SPID in Laravel: pacchetti, middleware e pattern produzione
Laravel e' uno dei framework PHP piu' usati per realizzare portali della Pubblica Amministrazione e servizi privati italiani. Questa guida confronta i package SPID disponibili nell'ecosistema Laravel, mostra come strutturare il middleware di autenticazione e affronta i pattern produzione: scaling, sessioni, refresh sicuro, audit log.
Panoramica dei pacchetti disponibili
L'ecosistema Composer offre alcuni pacchetti per SPID in Laravel. I principali:
italia/spid-laravel: pacchetto di riferimento storico del Team Digitale. Implementa il SP SAML SPID secondo le Linee Guida AgID. Manutenzione attiva ma con lag rispetto agli avvisi tecnici piu' recenti.onelogin/php-samlwrappato manualmente in service provider Laravel: massimo controllo ma 1-2 settimane di lavoro per coprire le specificita' SPID (firma metadata, namespace spid:, attributi italiani).simplesamlphp/simplesamlphpcome servizio esterno + Laravel come client OAuth/JWT verso SSP: piu' robusto in produzione ma due processi PHP da gestire.
Per CIE l'ecosistema e' piu' scarno: italia/cie-php e' la libreria storica, da adattare a Laravel.
Struttura del progetto Laravel SPID-ready
app/
Http/
Controllers/
Auth/
SpidController.php # avvia AuthnRequest
SpidCallbackController.php # gestisce ACS
SpidLogoutController.php
Middleware/
EnsureSpidAuth.php # protegge rotte
Services/
SpidService.php # wrap onelogin/php-saml
AttributeMapper.php # SAML attrs -> User model
config/
spid.php # config certs, EntityID, IdP list
routes/
web.php
Implementazione del service e routing
Definizione delle rotte minime:
// routes/web.php
Route::prefix('spid')->group(function () {
Route::get('login/{idp}', [SpidController::class, 'login'])->name('spid.login');
Route::post('acs', [SpidCallbackController::class, 'handle'])->name('spid.acs');
Route::get('metadata', [SpidController::class, 'metadata'])->name('spid.metadata');
Route::get('logout', [SpidLogoutController::class, 'logout'])->name('spid.logout');
Route::get('sls', [SpidLogoutController::class, 'sls'])->name('spid.sls');
});
Il SpidService incapsula la creazione del settings array per onelogin/php-saml partendo dal config:
public function settingsForIdp(string $idpId): array {
return [
'sp' => [
'entityId' => config('spid.entity_id'),
'assertionConsumerService' => [
'url' => route('spid.acs'),
'binding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST',
],
'singleLogoutService' => [
'url' => route('spid.sls'),
'binding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-REDIRECT',
],
'NameIDFormat' => 'urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified',
'x509cert' => file_get_contents(storage_path('spid/sp.crt')),
'privateKey' => file_get_contents(storage_path('spid/sp.key')),
],
'idp' => config("spid.idps.$idpId"),
// ...settings security AgID specifici
];
}
Mapping SAML attributes -> User model
Dopo il successo dell'autenticazione, l'ACS riceve un array di attributi SAML. Va creato un User o aggiornato uno esistente:
public function handle(Request $request, SpidService $spid) {
$auth = $spid->authForResponse($request);
$auth->processResponse();
if (!$auth->isAuthenticated()) abort(401);
$attrs = $auth->getAttributes();
$cf = $attrs['fiscalNumber'][0] ?? null;
if (!$cf) abort(422, 'Codice fiscale mancante');
$user = User::firstOrCreate(
['codice_fiscale' => $cf],
[
'name' => ($attrs['name'][0] ?? '').' '.($attrs['familyName'][0] ?? ''),
'email' => $attrs['email'][0] ?? null,
]
);
Auth::login($user);
$request->session()->put('spid_session_index', $auth->getSessionIndex());
return redirect()->intended('/area-riservata');
}
Middleware EnsureSpidAuth
Per proteggere rotte che richiedono uno specifico livello SPID:
public function handle(Request $request, Closure $next, string $livelloMinimo = 'L2') {
if (!Auth::check()) {
return redirect()->route('spid.login.choice', ['return'=>$request->fullUrl()]);
}
$livelloAttuale = session('spid_level');
if ($this->compareLivelli($livelloAttuale, $livelloMinimo) < 0) {
// step-up auth: riautentica con livello superiore
return redirect()->route('spid.login.step_up', ['target'=>$livelloMinimo]);
}
return $next($request);
}
Sessioni e scaling
SPID ha session timeout brevi rispetto alle Linee Guida. Patterns produzione:
- Session driver Redis: per scaling orizzontale, le sessioni Laravel devono essere su Redis. File session driver non funziona dietro load balancer.
- Validita' assertion vs validita' sessione: l'assertion SAML ha NotOnOrAfter breve (5-10 min), ma una volta processata, la sessione applicativa puo' durare di piu' (l'IdP ha gia' fatto il suo lavoro). Tuttavia AgID raccomanda sessioni non superiori a 60 minuti per L2 e 30 minuti per L3.
- Refresh con re-auth: scaduta la sessione applicativa, il SP fa nuovamente AuthnRequest. Se l'IdP ha ancora sessione attiva, riautentica trasparentemente (IsPassive opzionale).
Logging e audit
Le Linee Guida AgID impongono al SP di mantenere un audit log delle autenticazioni che includa: timestamp, IP, IdP, esito, livello SPID, codice fiscale (o suo hash), session index. Retention minima 24 mesi.
In Laravel, una soluzione e' un model SpidAccessLog con write asincrono via queue per non rallentare il login:
SpidAccessLog::create([
'cf_hash' => hash('sha256', $cf.config('app.audit_salt')),
'idp' => $idpId,
'level' => $livello,
'esito' => 'success',
'session_index' => $auth->getSessionIndex(),
'ip' => $request->ip(),
'ua' => $request->userAgent(),
]);
L'hash del CF anziche' il CF in chiaro consente di tracciare gli accessi senza esporre PII non strettamente necessaria.
Test in locale con IdP demo
Per testare in dev senza avere l'accreditamento, usare il validator AgID demo: https://demo.spid.gov.it/validator. Genera utenze di test al volo e ritorna assertion conformi.
Laravel + SPID/CIE in poche righe?
Forniamo un package Laravel che si appoggia al nostro gateway: nessun SAML interno, solo route protette e User auto-mappato.