Salta al contenuto principale

Integrazione API Governative: SPID, CIE e Servizi Digitali IT

Guida pratica all'integrazione di SPID, CIE e pagoPA nei servizi digitali italiani: SAML 2.0, OpenID Connect Federation, onboarding AGID…

Cosa ne pensi di questo articolo?

Integrazione API Governative: SPID, CIE e Servizi Digitali IT
GovTechFoto:Daniil KomovPexels

In Breve

Guida pratica all'integrazione di SPID, CIE e pagoPA nei servizi digitali italiani: SAML 2.0, OpenID Connect Federation, onboarding AGID…

GovTech 26 min

Integrazione API Governative: SPID, CIE e Servizi Digitali IT

Guida pratica all'integrazione di SPID, CIE e pagoPA nei servizi digitali italiani: SAML 2.0, OpenID Connect Federation, onboarding AGID, SDK ufficiali e pattern di integrazione per developer e architetti di soluzioni PA.

SPID CIE pagoPA OpenID Connect SAML Italian Digital

Il Panorama dell'Identità Digitale Italiana

L'ecosistema dell'identità digitale italiana è uno dei più articolati d'Europa, con tre sistemi principali che coesistono e convergono: SPID (Sistema Pubblico di Identità Digitale), CIE (Carta d'Identità Elettronica) e, per i pagamenti, pagoPA. Dal 2023, la transizione verso OpenID Connect come protocollo unificato ha semplificato significativamente l'integrazione per i developer.

Come developer o architect che deve integrare l'autenticazione governativa in un'applicazione, hai davanti diverse scelte tecniche con implicazioni significative. Questo articolo ti guida dall'onboarding AGID all'implementazione concreta, confrontando SAML 2.0 (il protocollo storico di SPID) con OpenID Connect Federation (il futuro di entrambi SPID e CIE), e mostrando come integrare pagoPA per i pagamenti.

Cosa Imparerai

  • Architettura di SPID: Identity Provider, Service Provider, SAML 2.0 e livelli di sicurezza
  • Architettura di CIE: chip NFC, PIN, backend CIE-ID e modalità di autenticazione
  • OpenID Connect per SPID e CIE: federazione, token, claims e scopes
  • Processo di onboarding AGID: requisiti tecnici e procedurali
  • SDK ufficiali: librerie per 5 linguaggi di programmazione
  • Implementazione pratica: Python e TypeScript con esempi completi
  • pagoPA: integrazione pagamenti nei servizi PA
  • Testing e ambienti di staging per sviluppo e validazione

SPID: Sistema Pubblico di Identità Digitale

SPID è il sistema nazionale di identità digitale italiano, istituito dal D.Lgs. 82/2005 (CAD) e regolato da AgID. Permette ai cittadini di autenticarsi su qualsiasi servizio PA (e molti privati) con un'unica coppia di credenziali, gestita da uno degli Identity Provider (IdP) accreditati (Aruba, Infocert, Namirial, Poste, Register, Sielte, SpidItalia, Tim, Intesa).

SPID prevede 3 livelli di sicurezza:

  • Livello 1: autenticazione con username e password. Adatto per servizi a basso rischio.
  • Livello 2: username/password + OTP (SMS o app). Il livello più usato per servizi PA. Richiede un secondo fattore di autenticazione (2FA).
  • Livello 3: autenticazione con certificato digitale o smart card. Per servizi ad alto rischio (atti notarili, firma digitale qualificata).

CIE: Carta d'Identità Elettronica

La CIE 3.0, emessa dall'Istituto Poligrafico e Zecca dello Stato, contiene un chip NFC con certificati digitali X.509 che consentono l'autenticazione forte. Il sistema CIE-ID permette l'autenticazione online tramite:

  • Smartphone con NFC: l'utente avvicina la CIE allo smartphone e inserisce il PIN. L'app CIE ID genera un'asserzione di autenticazione firmata digitalmente.
  • Desktop con lettore NFC: tramite il software CIE ID del Ministero dell'Interno.
  • Desktop senza NFC: autenticazione tramite QR code scansionato con l'app CIE ID.

Dal punto di vista del developer, CIE e SPID condividono ora lo stesso protocollo OpenID Connect (OIDC), con differenze principalmente nella registrazione e nei metadata dell'Identity Provider.

OpenID Connect per SPID e CIE: Il Protocollo Unificato

AgID ha pubblicato nel gennaio 2023 le Regole Tecniche OpenID Connect per SPID e CIE, disponibili su docs.italia.it. Queste regole definiscono una Federazione OpenID Connect basata sullo standard OIDC Federation 1.0 (draft IETF), dove:

  • AgID è la Trust Anchor: il nodo radice della federazione, la fonte di verità per la fiducia tra tutti i partecipanti.
  • Identity Provider (gli stessi di SPID) sono Leaf Nodes registrati presso AgID.
  • I Relying Party (i servizi che vogliono usare SPID/CIE) si registrano pubblicando un Entity Configuration (un JWT firmato con le proprie chiavi pubbliche).
# Implementazione OpenID Connect per SPID/CIE in Python
# Usa la libreria spid-cie-oidc-django o implementazione custom

import httpx
import jwt
import json
import secrets
import hashlib
import base64
from datetime import datetime, timedelta
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.backends import default_backend

class SPIDCIEOIDCClient:
    """
    Client OIDC per integrazione SPID/CIE.
    Implementa il flow Authorization Code con PKCE (obbligatorio per SPID/CIE OIDC).
    """

    def __init__(
        self,
        client_id: str,       # URI del Relying Party (il tuo servizio)
        redirect_uri: str,    # URI di callback
        private_key_path: str, # Chiave privata RSA/EC per firma JWT
        trust_anchor: str = "https://registry.agid.gov.it"
    ):
        self.client_id = client_id
        self.redirect_uri = redirect_uri
        self.trust_anchor = trust_anchor

        # Carica chiave privata per firma
        with open(private_key_path, "rb") as f:
            self.private_key = serialization.load_pem_private_key(
                f.read(), password=None, backend=default_backend()
            )

    def generate_pkce(self) -> tuple[str, str]:
        """
        Genera code_verifier e code_challenge per PKCE.
        PKCE è OBBLIGATORIO nelle specifiche SPID/CIE OIDC.
        """
        # code_verifier: stringa casuale di 43-128 caratteri
        code_verifier = secrets.token_urlsafe(64)

        # code_challenge = BASE64URL(SHA256(code_verifier))
        code_challenge = base64.urlsafe_b64encode(
            hashlib.sha256(code_verifier.encode()).digest()
        ).rstrip(b"=").decode()

        return code_verifier, code_challenge

    def build_authorization_url(
        self,
        idp_authorization_endpoint: str,
        scope: list[str] = None,
        acr_values: str = "https://www.spid.gov.it/SpidL2",  # Livello 2 default
        state: str = None,
        nonce: str = None,
        ui_locales: str = "it",
        claims: dict = None
    ) -> tuple[str, dict]:
        """
        Costruisce l'URL di autorizzazione per SPID/CIE OIDC.
        Ritorna (authorization_url, session_data) dove session_data va salvato in sessione.
        """
        if scope is None:
            scope = ["openid", "profile"]

        if state is None:
            state = secrets.token_urlsafe(32)

        if nonce is None:
            nonce = secrets.token_urlsafe(32)

        code_verifier, code_challenge = self.generate_pkce()

        # Request Object: JWT firmato con claims della request
        # Obbligatorio in SPID/CIE OIDC per sicurezza end-to-end
        request_object_claims = {
            "iss": self.client_id,
            "aud": idp_authorization_endpoint,
            "iat": int(datetime.utcnow().timestamp()),
            "exp": int((datetime.utcnow() + timedelta(minutes=5)).timestamp()),
            "jti": secrets.token_urlsafe(16),
            "response_type": "code",
            "client_id": self.client_id,
            "redirect_uri": self.redirect_uri,
            "scope": " ".join(scope),
            "state": state,
            "nonce": nonce,
            "code_challenge": code_challenge,
            "code_challenge_method": "S256",
            "acr_values": acr_values,
            "ui_locales": ui_locales,
        }

        if claims:
            request_object_claims["claims"] = claims

        # Firma il Request Object con la chiave privata del RP
        request_object = jwt.encode(
            request_object_claims,
            self.private_key,
            algorithm="RS256",
            headers={"kid": "rp-signing-key-2024"}
        )

        # Costruisci URL autorizzazione
        import urllib.parse
        params = {
            "client_id": self.client_id,
            "response_type": "code",
            "scope": " ".join(scope),
            "redirect_uri": self.redirect_uri,
            "state": state,
            "code_challenge": code_challenge,
            "code_challenge_method": "S256",
            "request": request_object,  # Request Object firmato
        }

        auth_url = f"{idp_authorization_endpoint}?{urllib.parse.urlencode(params)}"

        session_data = {
            "state": state,
            "nonce": nonce,
            "code_verifier": code_verifier,
        }

        return auth_url, session_data

    async def exchange_code_for_tokens(
        self,
        authorization_code: str,
        code_verifier: str,
        idp_token_endpoint: str
    ) -> dict:
        """
        Scambia il codice di autorizzazione per i token (access_token, id_token).
        Usa Client Authentication con private_key_jwt (obbligatorio in SPID/CIE).
        """
        now = int(datetime.utcnow().timestamp())

        # client_assertion: JWT firmato per autenticare il RP al token endpoint
        client_assertion = jwt.encode(
            {
                "iss": self.client_id,
                "sub": self.client_id,
                "aud": idp_token_endpoint,
                "iat": now,
                "exp": now + 300,
                "jti": secrets.token_urlsafe(16),
            },
            self.private_key,
            algorithm="RS256",
            headers={"kid": "rp-signing-key-2024"}
        )

        async with httpx.AsyncClient() as client:
            response = await client.post(
                idp_token_endpoint,
                data={
                    "grant_type": "authorization_code",
                    "code": authorization_code,
                    "redirect_uri": self.redirect_uri,
                    "code_verifier": code_verifier,
                    "client_assertion_type": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
                    "client_assertion": client_assertion,
                    "client_id": self.client_id,
                }
            )
            response.raise_for_status()
            return response.json()

    def validate_id_token(
        self,
        id_token: str,
        idp_jwks_uri: str,
        nonce: str,
        expected_acr: str = None
    ) -> dict:
        """
        Valida l'ID Token ricevuto dall'IdP.
        Verifica firma, nonce, audience, e livello di autenticazione (acr).
        """
        # Recupera le chiavi pubbliche dell'IdP
        import httpx
        jwks = httpx.get(idp_jwks_uri).json()

        # Decodifica e valida il JWT
        claims = jwt.decode(
            id_token,
            jwks,
            algorithms=["RS256", "ES256"],
            audience=self.client_id,
            options={"require": ["nonce", "acr"]}
        )

        # Verifica nonce (anti-replay)
        if claims.get("nonce") != nonce:
            raise ValueError("Invalid nonce in ID Token")

        # Verifica livello di autenticazione se richiesto
        if expected_acr and claims.get("acr") != expected_acr:
            raise ValueError(f"ACR mismatch: expected {expected_acr}, got {claims.get('acr')}")

        return claims

Claims SPID e CIE: Attributi dell'Utente

Una differenza importante tra SPID e i tipici OIDC provider privati riguarda i claims (attributi dell'utente) disponibili. SPID/CIE forniscono un set di attributi certificati dal governo italiano:

Claim OIDC Attributo SPID Disponibile in CIE Note
sub ID univoco presso l'IdP Si Non è il CF; cambia tra IdP diversi
fiscal_number Codice Fiscale Si Formato: TINIT-XXXXXXXXXXXXXX
given_name Nome Si Standard OIDC
family_name Cognome Si Standard OIDC
birthdate Data di nascita Si Formato: YYYY-MM-DD
place_of_birth Luogo di nascita Si Codice catastale del comune
gender Sesso Si M/F
email Email (non certificata) No Solo SPID, dichiarata dall'utente
mobile_phone Telefono mobile No Solo SPID, dichiarato dall'utente
document_details Dati documento Si Solo CIE: num. CIE, scadenza, comune emissione

Attenzione: sub vs fiscal_number

Il sub claim in SPID è l'identificativo dell'utente presso uno specifico IdP, non un identificativo globale. Se un utente cambia IdP (es. da Aruba a Poste), il sub cambia. Il Codice Fiscale (fiscal_number) è l'unico identificativo stabile e globale per un cittadino italiano. Usa il fiscal_number come chiave primaria nel tuo database, non il sub.

Onboarding AGID: Diventare Service Provider

Per integrare SPID o CIE OIDC nel tuo servizio devi completare il processo di accreditamento come Relying Party (RP) presso AgID. Il processo si divide in:

  1. Registrazione su developers.italia.it: crea un'account e accedi alla piattaforma di onboarding SPID/CIE.
  2. Preparazione tecnica: implementa il tuo SP con uno degli SDK ufficiali o un'implementazione custom. Configura il metadata del Relying Party (Entity Configuration JWT).
  3. Ambiente di staging: AGID fornisce un IdP di test (spid-test.agid.gov.it per SAML, demo-oidc.agid.gov.it per OIDC) con utenti di test predefiniti.
  4. Validazione tecnica: il tuo SP viene testato con il tool di validazione ufficiale AGID. Devono passare tutti i test case obbligatori.
  5. Accordo legale: firma dell'accordo di adesione con AgID (o con l'aggregatore scelto, nel caso di adesione tramite aggregatore).
  6. Produzione: dopo approvazione, il tuo SP viene registrato in produzione e gli utenti reali possono autenticarsi.
# Entity Configuration del Relying Party (JWT firmato)
# Questo documento deve essere pubblicato all'URL: {client_id}/.well-known/openid-federation

import jwt
import json
from datetime import datetime, timedelta

def generate_entity_configuration(
    client_id: str,  # URI del tuo servizio, es: https://servizi.miocomune.it
    private_key,
    public_key_jwk: dict,
    redirect_uris: list,
    organization_name: str,
    contacts: list
) -> str:
    """
    Genera l'Entity Configuration JWT per la registrazione OIDC Federation.
    Deve essere publicata a: {client_id}/.well-known/openid-federation
    """
    now = int(datetime.utcnow().timestamp())

    payload = {
        # Claims standard OIDC Federation
        "iss": client_id,
        "sub": client_id,
        "iat": now,
        "exp": now + 86400 * 365,  # Valida 1 anno (da aggiornare)
        "jwks": {"keys": [public_key_jwk]},  # Chiave pubblica del RP

        # Metadata del Relying Party
        "metadata": {
            "openid_relying_party": {
                "application_type": "web",
                "client_id": client_id,
                "client_registration_types": ["automatic"],
                "redirect_uris": redirect_uris,
                "response_types": ["code"],
                "grant_types": ["authorization_code"],
                "id_token_signed_response_alg": "RS256",
                "userinfo_signed_response_alg": "RS256",
                "token_endpoint_auth_method": "private_key_jwt",
                "token_endpoint_auth_signing_alg": "RS256",
                "scope": ["openid", "profile", "email", "offline_access"],
                "client_name": organization_name,
                "contacts": contacts,
                # Parametri obbligatori per SPID/CIE
                "policy_uri": f"{client_id}/privacy",
                "logo_uri": f"{client_id}/logo.png",
                "subject_type": "pairwise",
                "request_object_signing_alg": "RS256",
            }
        },

        # Trust chain verso la Trust Anchor di AgID
        "authority_hints": ["https://registry.agid.gov.it"],
    }

    return jwt.encode(payload, private_key, algorithm="RS256", headers={"kid": "rp-signing-key-2024"})

# Endpoint FastAPI per esporre l'Entity Configuration
from fastapi import FastAPI
from fastapi.responses import Response

app = FastAPI()

@app.get("/.well-known/openid-federation")
async def entity_configuration():
    ec_jwt = generate_entity_configuration(
        client_id="https://servizi.miocomune.it",
        private_key=private_key,  # Caricata da HSM o file sicuro
        public_key_jwk=public_key_jwk,
        redirect_uris=["https://servizi.miocomune.it/auth/callback"],
        organization_name="Comune di Esempio",
        contacts=["tech@miocomune.it"]
    )

    return Response(
        content=ec_jwt,
        media_type="application/entity-statement+jwt"
    )

pagoPA: Integrazione Pagamenti

pagoPA è la piattaforma nazionale per i pagamenti verso le PA, gestita da PagoPA S.p.A. Permette ai cittadini di pagare tributi, multe, rette e qualsiasi altra prestazione PA tramite una vasta rete di canali (banche, uffici postali, app di pagamento, Satispay, etc.).

Dal punto di vista tecnico, l'integrazione pagoPA per un'Ente Creditore (EC) prevede:

  • Adesione al nodo pagoPA: tramite il portale di adesione PagoPA. Gli Enti possono aderire direttamente o tramite intermediari tecnologici abilitati.
  • Generazione IUV (Identificativo Univoco Versamento): codice che identifica univocamente ogni pagamento atteso. Il formato è definito dall'Ente Creditore ma deve rispettare le regole pagoPA.
  • Posizione Debitoria: ogni debit che il cittadino ha verso l'EC viene registrato sul nodo pagoPA come "posizione debitoria" con associato un IUV.
  • Verifica e chiusura: pagoPA notifica l'EC quando un pagamento è completato, l'EC verifica e chiude la posizione debitoria.
# Integrazione pagoPA - Generazione posizione debitoria
# API SOAP/REST verso il Nodo pagoPA

import httpx
import uuid
from datetime import datetime, timedelta
from dataclasses import dataclass

@dataclass
class PaymentPosition:
    iuv: str               # Identificativo Univoco Versamento
    amount_cents: int      # Importo in centesimi di euro
    description: str       # Causale del pagamento
    citizen_fiscal_code: str
    due_date: datetime
    company_name: str      # Denominazione dell'Ente Creditore

class PagoPAClient:
    """
    Client per le API del Nodo pagoPA (versione REST/JSON).
    Supporta le API GPD (Gestione Posizioni Debitorie).
    """

    def __init__(self, organization_fiscal_code: str, api_key: str, base_url: str):
        self.org_fc = organization_fiscal_code
        self.api_key = api_key
        self.base_url = base_url

    def generate_iuv(self) -> str:
        """
        Genera un IUV conforme alle specifiche pagoPA.
        Struttura per Enti con aux digit 3 (applicativo gestionale):
        - 17 caratteri numerici
        - Deve essere univoco per l'Ente Creditore
        """
        # Componente temporale: YYMMDDHHMM (10 cifre)
        time_component = datetime.utcnow().strftime("%y%m%d%H%M")
        # Componente random: 7 cifre
        random_component = str(uuid.uuid4().int)[:7]
        iuv = f"{time_component}{random_component}"
        return iuv[:17]  # Tronca a 17 caratteri

    async def create_payment_position(self, position: PaymentPosition) -> dict:
        """
        Crea una posizione debitoria sul nodo pagoPA.
        """
        async with httpx.AsyncClient() as client:
            response = await client.post(
                f"{self.base_url}/organizations/{self.org_fc}/debtpositions",
                headers={
                    "Ocp-Apim-Subscription-Key": self.api_key,
                    "Content-Type": "application/json"
                },
                json={
                    "iupd": f"{self.org_fc}-{position.iuv}",  # Identificativo Univoco Posizione Debitoria
                    "type": "F",                          # F = Persona fisica
                    "fiscalCode": position.citizen_fiscal_code,
                    "companyName": position.company_name,
                    "validityDate": position.due_date.isoformat(),
                    "paymentOption": [
                        {
                            "iuv": position.iuv,
                            "amount": position.amount_cents,
                            "description": position.description,
                            "isPartialPayment": False,
                            "dueDate": (position.due_date + timedelta(days=30)).isoformat(),
                            "fee": 100,  # Commissione in centesimi (1 euro)
                            "transfer": [
                                {
                                    "idTransfer": "1",
                                    "amount": position.amount_cents,
                                    "organizationFiscalCode": self.org_fc,
                                    "remittanceInformation": position.description,
                                    "category": "0201102IM",  # Codice tassonomia
                                }
                            ]
                        }
                    ]
                }
            )
            response.raise_for_status()
            return response.json()

    def generate_payment_notice_url(self, iuv: str) -> str:
        """
        Genera il link di pagamento che il cittadino può usare.
        Formato standard: https://checkout.pagopa.it/pay?...
        """
        notice_number = f"3{self.org_fc}{iuv}"  # Numero Avviso pagoPA
        return (
            f"https://checkout.pagopa.it/pay"
            f"?rptId={self.org_fc}{notice_number}"
            f"&amount={100}"  # Amount in centesimi
        )

Testing e Ambienti di Sviluppo

AGID e PagoPA mettono a disposizione ambienti di test dedicati per lo sviluppo e la validazione:

Sistema Ambiente URL Credenziali Test
SPID OIDC Demo/Test demo.spid.gov.it Utenti test su developers.italia.it
CIE OIDC Collaudo preprod.cie.gov.it Richiesta tramite portale MinInterno
SPID SAML Test IdP spidtest.agid.gov.it test/test (livelli 1, 2, 3)
pagoPA GPD UAT api.uat.platform.pagopa.it API Key richiesta su portale devOps
pagoPA Checkout UAT uat.checkout.pagopa.it Carta test: 4242 4242 4242 4242

SDK Ufficiali e Librerie Consigliate

Il progetto Developers Italia (developers.italia.it) mantiene SDK ufficiali per l'integrazione di SPID e CIE per 5 linguaggi di programmazione:

  • Python: spid-cie-oidc-django (basato su Django), pyspid (SAML)
  • Java: spid-spring-integration (Spring Boot)
  • .NET: spid-dotnet-sdk (ASP.NET Core)
  • PHP: spid-php
  • Ruby: spid-ruby

Per integrazioni in contesti non coperti dagli SDK ufficiali, le specifiche tecniche complete sono disponibili su docs.italia.it (cerca "SPID CIE OIDC" o "Regole Tecniche SPID"). La federazione OIDC può essere implementata con qualsiasi libreria OIDC standard, aggiungendo il supporto ai claim specifici di SPID/CIE.

Conclusioni e Prossimi Passi

L'integrazione di SPID, CIE e pagoPA nei servizi digitali italiani è un requisito per qualsiasi PA che voglia offrire servizi online conformi al CAD. La transizione verso OpenID Connect semplifica significativamente l'implementazione rispetto al SAML 2.0 storico, e gli SDK ufficiali di Developers Italia abbassano ulteriormente la barriera d'ingresso.

Nel prossimo e ultimo articolo di questa serie esploriamo i GovStack Building Block: il framework modulare internazionale per costruire servizi di governo digitale riutilizzabili, adottato da oltre 20 paesi nel 2025.

Articoli Correlati in Questa Serie

  • GovTech #00: Digital Public Infrastructure - building block e architettura globale
  • GovTech #01: eIDAS 2.0 e EUDI Wallet - identità digitale europea
  • GovTech #02: OpenID Connect per identità governativa - implementazione completa
  • GovTech #04: GDPR-by-Design - dati degli utenti e privacy nei servizi PA

Commenti

Caricamento commenti...

Accedi per commentare

Discuti questo articolo nella community

Unisciti al gruppo Telegram per discutere con altri sviluppatori, fare domande e condividere le tue esperienze.

Ti è piaciuto questo articolo?

Esplora altri contenuti sul blog o scopri i miei progetti