Krajobraz włoskiej tożsamości cyfrowej

Włoski ekosystem tożsamości cyfrowej jest jednym z najbardziej złożonych w Europie i obejmuje trzy główne systemy, które współistnieją i zbiegają się: SPID (System tożsamości publicznej Cyfrowy), CIE (Elektroniczny Dowód Tożsamości), a w przypadku płatności, payPA. Od 2023 roku przejście na Połączenie OpenID jako protokół unified znacznie uprościło integrację dla programistów.

Jako programista lub architekt, który musi zintegrować uwierzytelnianie rządowe z aplikacją, musisz się z tym zmierzyć kilka wyborów technicznych o znaczących implikacjach. Ten artykuł przeprowadzi Cię przez proces wdrażania AGID do konkretnej implementacji, porównując SAML 2.0 (historyczny protokół SPID) z OpenID Connect Federacji (przyszłość zarówno SPID, jak i CIE) oraz pokazanie, jak zintegrować pagoPA dla płatności.

Czego się nauczysz

  • Architektura SPID: dostawca tożsamości, dostawca usług, SAML 2.0 i poziomy bezpieczeństwa
  • Architektura CIE: chip NFC, PIN, backend CIE-ID i tryb uwierzytelniania
  • OpenID Connect dla SPID i CIE: federacja, tokeny, oświadczenia i zakresy
  • Proces onboardingu AGID: wymagania techniczne i proceduralne
  • Oficjalne SDK: biblioteki dla 5 języków programowania
  • Praktyczna implementacja: Python i TypeScript z pełnymi przykładami
  • pagoPA: integracja płatności z usługami PA
  • Środowiska testowe i testowe do celów programistycznych i walidacyjnych

SPID: publiczny system tożsamości cyfrowej

SPID to włoski krajowy system tożsamości cyfrowej, założony przez Dekret legislacyjny 82/2005 (CAD) i regulowane przez AgID. Umożliwia obywatelom uwierzytelnianie w dowolnej usłudze PA (i wielu private) z pojedynczą parą danych uwierzytelniających, zarządzanych przez jeden z nich Dostawca tożsamości (IdP) akredytowane (Aruba, Infocert, Namirial, Poste, Register, Sielte, SpidItalia, Tim, Intesa).

SPID przewiduje 3 poziomy bezpieczeństwa:

  • Poziom 1: uwierzytelnianie za pomocą nazwy użytkownika i hasła. Nadaje się do usług niskiego ryzyka.
  • Poziom 2: nazwa użytkownika/hasło + OTP (SMS lub aplikacja). Najczęściej używany poziom dla usług PA. Wymaga drugiego czynnika uwierzytelniania (2FA).
  • Poziom 3: uwierzytelnianie za pomocą certyfikatu cyfrowego lub karty inteligentnej. Dla usług o wysokiej wydajności ryzyko (akty notarialne, kwalifikowany podpis cyfrowy).

CIE: Elektroniczny dowód tożsamości

Wydawany przez Państwowy Instytut Drukarni i Mennicy CIE 3.0 zawiera chip NFC z certyfikatami Urządzenia cyfrowe X.509 umożliwiające silne uwierzytelnianie. System CIE-ID umożliwia uwierzytelnianie online poprzez:

  • Smartfon z NFC: użytkownik przybliża CIE do smartfona i wprowadza PIN. Aplikacja CIE ID generuje cyfrowo podpisane potwierdzenie uwierzytelnienia.
  • Komputer stacjonarny z czytnikiem NFC: za pośrednictwem oprogramowania CIE ID Ministerstwa Spraw Wewnętrznych.
  • Komputer stacjonarny bez NFC: uwierzytelnianie za pomocą kodu QR zeskanowanego za pomocą aplikacji CIE ID.

Z punktu widzenia programisty CIE i SPID korzystają obecnie z tego samego protokołu OpenID Connect (OIDC), z różnicami głównie w zakresie rejestracji i metadanych Dostawcy Tożsamości.

OpenID Connect dla SPID i CIE: ujednolicony protokół

AgID opublikował Zasady techniczne OpenID Connect dla SPID i CIE, dostępne na docs.italia.it. Zasady te definiują a Federacja OpenID Connect w oparciu o standard OIDC Federation 1.0 (projekt IETF), gdzie:

  • AgID to tam jest Zaufaj kotwicy: węzeł główny federacji, źródłem prawdy zaufania pomiędzy wszystkimi uczestnikami.
  • Dostawcy tożsamości (tak samo jak SPID). Węzły liściowe zarejestruj się w AgID.
  • I Strona Opierająca się (usługi chcące używać SPID/CIE) zarejestruj się poprzez publikację a Konfiguracja jednostki (JWT podpisany kluczami publicznymi).
# 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

Oświadczenia SPID i CIE: atrybuty użytkownika

Ważna różnica między SPID a typowymi prywatnymi dostawcami OIDC dotyczy roszczenia (atrybuty użytkownika) dostępne. SPID/CIE zapewnia zestaw atrybutów certyfikowanych przez rząd włoski:

Złóż wniosek do OIDC Atrybut SPID Dostępne w CIE Notatki
sub Unikalny identyfikator w dostawcy tożsamości Si To nie CF; zmiany pomiędzy różnymi dostawcami tożsamości
fiscal_number Kod identyfikacji podatkowej Si Format: TINIT-XXXXXXXXXXXXXXX
given_name Nazwa Si standardy OIDC
family_name Nazwisko Si standardy OIDC
birthdate Data urodzenia Si Format: RRRR-MM-DD
place_of_birth Miejsce urodzenia Si Kod katastralny gminy
gender Seks Si M/K
email E-mail (niecertyfikowany) No Tylko SPID zadeklarowany przez użytkownika
mobile_phone Telefon komórkowy No Tylko SPID zadeklarowany przez użytkownika
document_details Dane dokumentu Si Tylko CIE: nr. CIE, wygaśnięcie, wspólny problem

Ostrzeżenie: sub vs numer_fiskalny

Il sub roszczenie w SPID jest identyfikatorem użytkownika u określonego dostawcy tożsamości, nie identyfikator globalny. Jeśli użytkownik zmieni dostawcę tożsamości (np. z Aruba na Poste), plik sub zmiana. Ordynacja podatkowa (fiscal_number) jest jedynym stabilnym i globalnym identyfikatorem obywatela włoski. Skorzystaj z fiscal_number jako klucz podstawowy w bazie danych, a nie sub.

Wdrożenie AGID: Zostań dostawcą usług

Aby zintegrować SPID lub CIE OIDC ze swoją usługą, musisz ukończyć proces akredytacji jako Strona uzależniona (RP) w AgID. Proces dzieli się na:

  1. Rejestracja na stronie Developers.italia.it: utwórz konto i zaloguj się na platformie wdrożenia SPID/CIE.
  2. Przygotowanie techniczne: Zaimplementuj swój SP za pomocą jednego z oficjalnych zestawów SDK lub implementacji niestandardowe. Skonfiguruj metadane jednostki zależnej (JWT konfiguracji jednostki).
  3. Środowisko postojowe: AGID udostępnia testowego dostawcę tożsamości (spid-test.agid.gov.it dla SAML, demo-oidc.agid.gov.it dla OIDC) z predefiniowanymi użytkownikami testowymi.
  4. Walidacja techniczna: Twój SP jest testowany za pomocą oficjalnego narzędzia do sprawdzania poprawności AGID. Muszą przejść wszystkie obowiązkowe przypadki testowe.
  5. Umowa prawna: podpisanie umowy członkowskiej z AgID (lub z wybranym agregatorem, w przypadku członkostwa poprzez agregator).
  6. Produkcja: Po zatwierdzeniu Twój SP jest zarejestrowany w produkcji i użytkownikach prawdziwi ludzie mogą uwierzytelnić.
# 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: Integracja płatności

payPA to krajowa platforma płatności dla agencji płatniczych, zarządzana przez PagoPA S.p.A. Umożliwia obywatelom płacenie podatków, kar, opłat i wszelkich innych usług PA za pośrednictwem rozległej sieci kanały (banki, urzędy pocztowe, aplikacje płatnicze, Satispay itp.).

Z technicznego punktu widzenia integracja pagoPA dla instytucji wierzycielskiej (KE) zapewnia:

  • Członkostwo w węźle pagoPA: za pośrednictwem portalu członkowskiego PagoPA. Organizacje mogą się przyłączać bezpośrednio lub poprzez autoryzowanych pośredników technologicznych.
  • Pokolenie IUV (Unikalny identyfikator płatności): kod jednoznacznie identyfikujący jakąkolwiek oczekiwaną płatność. Format jest określony przez Organizację Wierzyciela, ale musi być zgodny z zasadami pagoPA.
  • Pozycja zadłużenia: Każdy dług obywatela wobec Komisji Europejskiej jest rejestrowany w węźle pagoPA jako „pozycję zadłużenia” z powiązanym IUV.
  • Weryfikacja i zamknięcie: pagoPA powiadamia Komisję Europejską o zakończeniu płatności, Komisja Europejska weryfikuje i zamyka pozycję zadłużenia.
# 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
        )

Środowiska testowe i programistyczne

AgID i PagoPA zapewniają dedykowane środowiska testowe do programowania i walidacji:

System Środowisko Adres URL Test poświadczeń
SPID OIDC Demo/Test demo.spid.gov.it Przetestuj użytkowników na stronie Developers.italia.it
CIE OIDC Testowanie preprod.cie.gov.it Zapytanie poprzez portal MinInterno
SPID SAML Testowanie tożsamości spidtest.agid.gov.it test/test (poziomy 1, 2, 3)
PayPA GPD UAT api.uat.platforma.pagopa.it Zażądano klucza API w portalu devOps
pagoPA Zamówienie UAT uat.checkout.pagopa.it Karta testowa: 4242 4242 4242 4242

Oficjalne zestawy SDK i zalecane biblioteki

Projekt Deweloperzy Włochy (developers.italia.it) utrzymuje oficjalne pakiety SDK do integracji SPID i CIE dla 5 języków programowania:

  • Pyton: spid-cie-oidc-django (w oparciu o Django), pyspid (SAML)
  • Jawa: spid-spring-integration (Wiosenny but)
  • .INTERNET: spid-dotnet-sdk (rdzeniowy ASP.NET)
  • PHP: spid-php
  • Rubin: spid-ruby

W przypadku integracji w kontekstach nieobjętych oficjalnymi pakietami SDK dostępne są pełne specyfikacje techniczne na docs.italia.it (wyszukaj „SPID CIE OIDC” lub „Przepisy techniczne SPID”). Federacja OIDC może być zaimplementowane z dowolną standardową biblioteką OIDC, dodając obsługę specyficznych oświadczeń SPID/CIE.

Wnioski i dalsze kroki

Integracja SPID, CIE i pagoPA z włoskimi usługami cyfrowymi jest wymogiem dla każdego PA chcesz oferować usługi online zgodne z CAD. Przejście na OpenID Connect znacznie upraszcza wdrożenie w porównaniu z historycznym SAML 2.0, a oficjalne SDK Developers Italia są jeszcze niższe barierę wejścia.

W następnym i ostatnim artykule z tej serii badamy Blok konstrukcyjny GovStack: ramy międzynarodowy modułowy system budowy cyfrowych usług rządowych wielokrotnego użytku, przyjęty w ponad 20 krajach w 2025 roku.

Powiązane artykuły z tej serii

  • GovTech #00: Cyfrowa infrastruktura publiczna - elementy składowe i architektura globalna
  • GovTech #01: eIDAS 2.0 i portfel EUDI - europejska tożsamość cyfrowa
  • GovTech #02: OpenID Connect dla tożsamości rządowej – pełne wdrożenie
  • GovTech #04: RODO-by-Design - dane użytkownika i prywatność w usługach PA