Guida tecnica
Integrare SPID in Symfony: bundle, Security Component e pattern enterprise
Symfony e' la scelta privilegiata per portali della PA con esigenze enterprise: amministrazione regionale, gestionali sanitari, piattaforme universitarie. Questa guida mostra come integrare SPID nel Security Component di Symfony usando il Custom Authenticator, gestire i providers utente e definire firewalls con livelli SPID differenziati.
Approccio: Symfony Security + Custom Authenticator
Da Symfony 5.3+ il sistema raccomandato per autenticatori custom e' il nuovo Authenticator basato su AbstractAuthenticator. Per SPID conviene:
- Implementare un
SpidAuthenticatorche gestisca il return dall'IdP (ACS endpoint). - Mantenere il SAML basso livello su una libreria collaudata come
onelogin/php-samlolightsaml/lightsaml. - Esporre il SP via Controller dedicato (login init, ACS, metadata, SLO).
- Configurare firewalls multipli con livelli SPID diversi se l'applicazione ha aree a sensibilita' diversa.
Pacchetti disponibili nell'ecosistema
Composer offre alcuni bundle specifici:
italia/spid-symfony: non sempre allineato a Symfony 6+, ma utile come riferimento di pattern.nbgrp/onelogin-saml-bundle: bundle generico SAML 2.0, ben mantenuto. Va personalizzato con i requisiti SPID (namespace, attributi, regole firma).hslavich/oneloginsaml-bundle: predecessore storico.
Per CIE non esiste un bundle ufficiale: in genere si adatta lightsaml o si interfaccia a un gateway esterno via JWT.
Struttura cartelle
src/
Controller/
Spid/
LoginController.php
AcsController.php
MetadataController.php
LogoutController.php
Security/
SpidAuthenticator.php
SpidUserProvider.php
Service/
SpidSamlService.php
SpidAttributeMapper.php
Entity/
User.php (con codice_fiscale come UNIQUE)
config/
packages/
security.yaml
spid.yaml
Configurazione security.yaml
security:
providers:
spid_users:
entity:
class: App\Entity\User
property: codiceFiscale
firewalls:
spid:
pattern: ^/(area-riservata|servizi-l2)
stateless: false
provider: spid_users
custom_authenticators:
- App\Security\SpidAuthenticator
logout:
path: spid_logout
spid_l3:
pattern: ^/(area-sanitaria|atti-firmati)
stateless: false
provider: spid_users
custom_authenticators:
- App\Security\SpidL3Authenticator
access_control:
- { path: ^/area-riservata, roles: ROLE_SPID_L2 }
- { path: ^/area-sanitaria, roles: ROLE_SPID_L3 }
SpidAuthenticator: la classe core
class SpidAuthenticator extends AbstractAuthenticator {
public function __construct(private SpidSamlService $saml,
private SpidAttributeMapper $mapper,
private EntityManagerInterface $em) {}
public function supports(Request $request): ?bool {
return $request->attributes->get('_route') === 'spid_acs';
}
public function authenticate(Request $request): Passport {
$auth = $this->saml->authForRequest($request);
$auth->processResponse();
if (!$auth->isAuthenticated()) {
throw new CustomUserMessageAuthenticationException('SPID auth failed');
}
$attrs = $auth->getAttributes();
$cf = $attrs['fiscalNumber'][0] ?? null;
$user = $this->mapper->firstOrCreate($attrs);
return new SelfValidatingPassport(
new UserBadge($cf, fn() => $user),
[new RememberMeBadge()]
);
}
// ... onAuthenticationSuccess, onAuthenticationFailure
}
Step-up authentication tra livelli
Pattern raccomandato: se l'utente e' loggato in L2 e tenta di accedere a una pagina L3, intercettare con un EventListener e fare AuthnRequest con AuthnContextClassRef di livello superiore:
class SpidLevelListener {
public function onAccessDenied(AccessDeniedEvent $event) {
if (str_contains((string)$event->getRequest()->getPathInfo(), 'sanitaria')) {
// L3 richiesto
$event->setResponse(new RedirectResponse($this->urlGen->generate(
'spid_step_up', ['level' => 'L3', 'return' => $event->getRequest()->getPathInfo()]
)));
}
}
}
Dependency injection del SAML service
Il SpidSamlService riceve config injection dal services.yaml:
parameters:
spid.entity_id: '%env(SPID_ENTITY_ID)%'
spid.cert_path: '%kernel.project_dir%/config/spid/sp.crt'
spid.key_path: '%kernel.project_dir%/config/spid/sp.key'
services:
App\Service\SpidSamlService:
arguments:
$entityId: '%spid.entity_id%'
$certPath: '%spid.cert_path%'
$keyPath: '%spid.key_path%'
$idpRegistry: '@App\Service\SpidIdpRegistry'
Testing in CI
Per testare il flusso SAML in CI senza un IdP reale, si crea un IdP stub:
- Genera assertion SAML pre-firmate per utenti di test.
- Il SpidAuthenticator accetta una flag
test_modeche bypassa la verifica metadata IdP e accetta certificati di test. - Test functional con WebTestCase:
$client->request('POST', '/spid/acs', ['SAMLResponse' => $encodedAssertion]).
Profilazione e monitoring
Il SAML response processing puo' essere costoso (validazione firma XML, decifratura assertion). Per app ad alto traffico:
- Cache delle public key IdP (con TTL allineato alla validita' del metadata IdP).
- Profilazione del tempo di authenticate(): tipicamente 80-200 ms ben configurato. Sopra 500 ms indica problemi di firma o IO.
- Metric Prometheus: tempo medio per IdP, success/failure rate, distribuzione per livello.
Symfony enterprise con SPID e CIE pronto in poche ore?
Bundle Symfony nativo che si interfaccia al nostro gateway: niente SAML interno, Security Component standard, multi-livello.