Peisajul identității digitale italiene

Ecosistemul italian de identitate digitală este unul dintre cele mai complexe din Europa, cu trei principalele sisteme care coexistă și converg: SPID (Sistemul de identitate publică digital), CIE (Carte electronică de identitate) și, pentru plăți, payPA. Din 2023, trecerea la OpenID Connect ca protocol unified a simplificat semnificativ integrarea pentru dezvoltatori.

În calitate de dezvoltator sau arhitect care trebuie să integreze autentificarea guvernamentală într-o aplicație, vă confruntați cu mai multe alegeri tehnice cu implicaţii semnificative. Acest articol vă ghidează prin integrarea AGID la implementarea concretă, comparând SAML 2.0 (protocolul istoric SPID) cu OpenID Connect Federația (viitorul atât al SPID, cât și al CIE) și care arată cum să integreze pagoPA pentru plăți.

Ce vei învăța

  • Arhitectura SPID: Furnizor de identitate, Furnizor de servicii, SAML 2.0 și niveluri de securitate
  • Arhitectura CIE: cip NFC, PIN, backend CIE-ID și mod de autentificare
  • OpenID Connect pentru SPID și CIE: federație, jetoane, revendicări și domenii
  • Procesul de îmbarcare AGID: cerințe tehnice și procedurale
  • SDK-uri oficiale: biblioteci pentru 5 limbaje de programare
  • Implementare practică: Python și TypeScript cu exemple complete
  • pagoPA: integrarea plăților în serviciile PA
  • Medii de testare și staging pentru dezvoltare și validare

SPID: Public Digital Identity System

SPID este sistemul național italian de identitate digitală, înființat de Decretul legislativ 82/2005 (CAD) și reglementate de AgID. Le permite cetățenilor să se autentifice pe orice serviciu PA (și multe private) cu o singură pereche de acreditări, gestionate de unul dintre Furnizor de identitate (IdP) acreditat (Aruba, Infocert, Namirial, Poste, Register, Sielte, SpidItalia, Tim, Intesa).

SPID prezice 3 niveluri de securitate:

  • Nivelul 1: autentificare cu nume de utilizator și parolă. Potrivit pentru servicii cu risc scăzut.
  • Nivelul 2: nume de utilizator/parolă + OTP (SMS sau aplicație). Cel mai utilizat nivel pentru serviciile PA. Necesită un al doilea factor de autentificare (2FA).
  • Nivelul 3: autentificare cu certificat digital sau smart card. Pentru servicii performante risc (acte notariale, semnătură digitală calificată).

CIE: Cartea electronică de identitate

CIE 3.0, emis de Institutul de Tipărit și Monetărie de Stat, conține un cip NFC cu certificate Dispozitive digitale X.509 care permit autentificarea puternică. Sistemul CIE-ID permite autentificarea online prin:

  • Smartphone cu NFC: utilizatorul aduce CIE-ul aproape de smartphone și introduce PIN-ul. Aplicația CIE ID generează o afirmație de autentificare semnată digital.
  • Desktop cu cititor NFC: prin software-ul CIE ID al Ministerului de Interne.
  • Desktop fără NFC: autentificare prin cod QR scanat cu aplicația CIE ID.

Din perspectiva dezvoltatorului, CIE și SPID au acum același protocol OpenID Connect (OIDC). cu diferențe în principal în înregistrarea și metadatele Furnizorului de identitate.

OpenID Connect pentru SPID și CIE: Protocolul unificat

AgID a publicat Reguli tehnice OpenID Connect pentru SPID și CIE, disponibil pe docs.italia.it. Aceste reguli definesc a OpenID Connect Federation bazat pe standardul OIDC Federation 1.0 (proiect IETF), unde:

  • AgID este acolo Ancoră de încredere: nodul rădăcină al federației, sursa adevărului pentru încrederea între toți participanții.
  • Furnizorii de identitate (la fel ca SPID) sunt Noduri de frunze înregistrare la AgID.
  • I Partidul de baza (servicii care doresc să utilizeze SPID/CIE) se înregistrează prin publicare a Configurare entitate (un JWT semnat cu cheile dvs. publice).
# 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

Afirmații SPID și CIE: atribute utilizator

O diferență importantă între SPID și furnizorii tipici de OIDC privați se referă la pretenții (atribute utilizator) disponibil. SPID/CIE oferă un set de atribute certificate de guvernul italian:

Revendicați OIDC Atribut SPID Disponibil în CIE Note
sub ID unic la IdP Si Nu este CF; schimbări între diferiți IdP
fiscal_number Cod de identificare fiscală Si Format: TINIT-XXXXXXXXXXXXXXX
given_name Nume Si Standardele OIDC
family_name Nume Si Standardele OIDC
birthdate Data nașterii Si Format: AAAA-LL-ZZ
place_of_birth Locul nașterii Si Cod cadastral al municipiului
gender Sex Si M/F
email E-mail (necertificat) No Numai SPID, declarat de utilizator
mobile_phone Telefon mobil No Numai SPID, declarat de utilizator
document_details Datele documentului Si Doar CIE: num. CIE, expirare, problemă comună

Avertisment: sub vs fiscal_number

Il sub revendicarea în SPID este identificatorul utilizatorului la un anumit IdP, nu un identificator global. Dacă un utilizator schimbă IdP (de exemplu, de la Aruba la Poste), sub schimba. Codul fiscal (fiscal_number) este singurul identificator stabil și global pentru un cetățean italiană. Utilizați fiscal_number ca cheie primară în baza de date, nu sub.

Incorporarea AGID: a deveni furnizor de servicii

Pentru a integra SPID sau CIE OIDC în serviciul dumneavoastră, trebuie să finalizați procesul de acreditare ca Parte de încredere (RP) la AgID. Procesul este împărțit în:

  1. Înregistrare pe developers.italia.it: creați un cont și conectați-vă la platformă de onboarding SPID/CIE.
  2. Pregătirea tehnică: implementați SP cu unul dintre SDK-urile oficiale sau cu o implementare personalizat. Configurați metadatele părții de baza (Entity Configuration JWT).
  3. Mediu de scenă: AGID oferă un IdP de testare (spid-test.agid.gov.it pentru SAML, demo-oidc.agid.gov.it pentru OIDC) cu utilizatori de testare predefiniti.
  4. Validare tehnică: SP dumneavoastră este testat cu instrumentul oficial de validare AGID. Ei trebuie să treacă toate cazurile de testare obligatorii.
  5. Acord legal: semnarea contractului de membru cu AgID (sau cu agregatorul ales, în cazul apartenenţei prin agregator).
  6. Productie: După aprobare, SP dumneavoastră este înregistrat în producție și utilizatori oamenii reali se pot autentifica.
# 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: Integrarea plăților

payPA este platforma națională de plăți către AP, administrată de PagoPA S.p.A. Permite cetățenilor să plătească impozite, amenzi, taxe și orice alt serviciu PA printr-o rețea vastă de canale (bănci, oficii poștale, aplicații de plată, Satispay etc.).

Din punct de vedere tehnic, integrarea pagoPA pentru o Instituție Creditoare (CE) prevede:

  • Calitatea de membru al nodului pagoPA: prin portalul de membru PagoPA. Organizațiile se pot alătura direct sau prin intermediari tehnologici autorizati.
  • generația IUV (Identificator unic de plată): cod care identifică în mod unic orice plată așteptată. Formatul este definit de Organismul Creditor, dar trebuie să respecte regulile pagoPA.
  • Poziția datoriei: Pe nod se înregistrează fiecare datorie pe care cetăţeanul o are faţă de CE pagoPA ca „poziție de datorie” cu un IUV asociat.
  • Verificare și închidere: pagoPA notifică CE atunci când o plată este finalizată, CE verifică şi închide poziţia datoriei.
# 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
        )

Medii de testare și dezvoltare

AgID și PagoPA oferă medii de testare dedicate pentru dezvoltare și validare:

Sistem Mediu URL Test de acreditări
SPID OIDC Demo/Test demo.spid.gov.it Testați utilizatorii pe developers.italia.it
CIE OIDC Testare preprod.cie.gov.it Solicitare prin portalul MinInterno
SPID SAML Testarea IdP spidtest.agid.gov.it test/test (nivelele 1, 2, 3)
payPA GPD UAT api.uat.platform.pagopa.it Cheia API solicitată pe portalul devOps
pagoPA Checkout UAT uat.checkout.pagopa.it Card de testare: 4242 4242 4242 4242

SDK-uri oficiale și biblioteci recomandate

Proiectul Dezvoltatori Italia (developers.italia.it) menține SDK-uri oficiale pentru integrare de SPID și CIE pentru 5 limbaje de programare:

  • Piton: spid-cie-oidc-django (bazat pe Django), pyspid (SAML)
  • Java: spid-spring-integration (Calci de primăvară)
  • .NET: spid-dotnet-sdk (ASP.NET Core)
  • PHP: spid-php
  • Rubin: spid-ruby

Pentru integrările în contexte care nu sunt acoperite de SDK-urile oficiale, sunt disponibile specificațiile tehnice complete pe docs.italia.it (căutați „SPID CIE OIDC” sau „SPID Technical Rules”). Federația OIDC poate fi implementat cu orice bibliotecă OIDC standard, adăugând suport pentru revendicările specifice SPID/CIE.

Concluzii și pașii următori

Integrarea SPID, CIE și pagoPA în serviciile digitale italiene este o cerință pentru orice PA care doresc să ofere servicii online compatibile cu CAD. Tranziția la OpenID Connect se simplifică semnificativ implementarea în comparație cu istoricul SAML 2.0, iar SDK-urile oficiale ale Developers Italia mai scad bariera de intrare.

În următorul și ultimul articol al acestei serii, explorăm Blocul de construcție GovStack: cadrul sistem modular internațional pentru a construi servicii guvernamentale digitale reutilizabile, adoptat de peste 20 de țări în 2025.

Articole similare din această serie

  • GovTech #00: Infrastructură publică digitală - blocuri de construcție și arhitectură globală
  • GovTech #01: eIDAS 2.0 și EUDI Wallet - identitate digitală europeană
  • GovTech #02: OpenID Connect for Government Identity - lansare completă
  • GovTech #04: GDPR-by-Design - datele utilizatorului și confidențialitatea în serviciile PA