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.

Tempo di lettura: 11 minuti

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-saml wrappato 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/simplesamlphp come 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.

Cerchi un gestionale Ordix completo che integra già SPID e CIE? Esplora i gestionali Ordix