Krajina italské digitální identity

Italský ekosystém digitální identity je jedním z nejsložitějších v Evropě, má tři hlavní systémy, které koexistují a konvergují: SPID (Systém veřejné identity digitální), CIE (elektronický průkaz totožnosti) a pro platby payPA. Od roku 2023 přechod na OpenID Connect jako protokol unified výrazně zjednodušil integraci pro vývojáře.

Jako vývojář nebo architekt, který potřebuje integrovat vládní ověřování do aplikace, stojíte před vámi několik technických možností s významnými důsledky. Tento článek vás provede onboardingem AGID ke konkrétní implementaci, srovnání SAML 2.0 (historický protokol SPID) s OpenID Connect Federation (budoucnost SPID i CIE) a ukazuje, jak integrovat pagoPA pro platby.

Co se naučíte

  • Architektura SPID: Poskytovatel identity, Poskytovatel služeb, SAML 2.0 a úrovně zabezpečení
  • Architektura CIE: čip NFC, PIN, backend CIE-ID a režim ověřování
  • OpenID Connect pro SPID a CIE: federace, tokeny, nároky a rozsahy
  • Proces registrace AGID: technické a procedurální požadavky
  • Oficiální sady SDK: knihovny pro 5 programovacích jazyků
  • Praktická implementace: Python a TypeScript s kompletními příklady
  • pagoPA: integrace plateb do služeb PA
  • Testovací a pracovní prostředí pro vývoj a ověřování

SPID: Systém veřejné digitální identity

SPID je italský národní systém digitální identity, založený společností Legislativní vyhláška 82/2005 (CAD) a regulované AgID. Umožňuje občanům autentizovat se na jakékoli službě PA (a mnoha private) s jedním párem přihlašovacích údajů spravovaných jedním z Poskytovatel identity (IdP) akreditované (Aruba, Infocert, Namirial, Poste, Register, Sielte, SpidItalia, Tim, Intesa).

SPID předpovídá 3 úrovně zabezpečení:

  • Úroveň 1: ověření pomocí uživatelského jména a hesla. Vhodné pro služby s nízkým rizikem.
  • Úroveň 2: uživatelské jméno/heslo + OTP (SMS nebo aplikace). Nejpoužívanější úroveň pro služby PA. Vyžaduje druhý faktor ověřování (2FA).
  • Úroveň 3: ověření digitálním certifikátem nebo čipovou kartou. Pro vysoce výkonné služby riziko (notářské zápisy, kvalifikovaný digitální podpis).

CIE: Elektronický průkaz totožnosti

CIE 3.0, vydaný Státním tiskařským a mincovním ústavem, obsahuje NFC čip s certifikáty Digitální zařízení X.509, která umožňují silné ověřování. Systém CIE-ID umožňuje online autentizaci přes:

  • Smartphone s NFC: uživatel přiblíží CIE ke smartphonu a zadá PIN. Aplikace CIE ID vygeneruje digitálně podepsané ověření.
  • Desktop s NFC čtečkou: prostřednictvím softwaru CIE ID Ministerstva vnitra.
  • Desktop bez NFC: ověření pomocí QR kódu naskenovaného pomocí aplikace CIE ID.

Z pohledu vývojáře nyní CIE a SPID sdílejí stejný protokol OpenID Connect (OIDC), s rozdíly zejména v registraci a metadatech Poskytovatele identity.

OpenID Connect pro SPID a CIE: Unified Protocol

AgID zveřejnilo Technická pravidla OpenID Connect pro SPID a CIE, k dispozici na docs.italia.it. Tato pravidla definují a OpenID Connect Federation založené na standardu OIDC Federation 1.0 (návrh IETF), kde:

  • AgID je to tam Trust Anchor: kořenový uzel federace, zdroj pravdy pro důvěru mezi všemi účastníky.
  • Poskytovatelé identity (stejné jako SPID). Listové uzly registrovat ve společnosti AgID.
  • I Spoléhající se strana (služby, které chtějí používat SPID/CIE) registrovat zveřejněním a Konfigurace entity (JWT podepsaný vašimi veřejnými klíči).
# 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

Nároky SPID a CIE: Uživatelské atributy

Důležitý rozdíl mezi SPID a typickými soukromými poskytovateli OIDC se týká nároky (atributy uživatel) k dispozici. SPID/CIE poskytují sadu atributů certifikovaných italskou vládou:

Nárokovat OIDC Atribut SPID K dispozici v CIE Poznámky
sub Jedinečné ID u IdP Si Není to CF; změny mezi různými IdP
fiscal_number DIČ Si Formát: TINIT-XXXXXXXXXXXXXX
given_name Jméno Si standardy OIDC
family_name Příjmení Si standardy OIDC
birthdate Datum narození Si Formát: RRRR-MM-DD
place_of_birth Místo narození Si Katastrální řád obce
gender Sex Si M/F
email E-mail (necertifikovaný) No Pouze SPID, deklarované uživatelem
mobile_phone Mobilní telefon No Pouze SPID, deklarované uživatelem
document_details Data dokumentu Si Pouze CIE: num. CIE, expirace, běžný problém

Upozornění: dílčí vs fiskální_číslo

Il sub nárok v SPID je identifikátor uživatele u konkrétního IdP, není globální identifikátor. Pokud uživatel změní IdP (např. z Aruby na Poste), sub přeměna. daňový řád (fiscal_number) je jediným stabilním a globálním identifikátorem pro občana italsky. Použijte fiscal_number jako primární klíč ve vaší databázi, nikoli sub.

Onboarding AGID: Stát se poskytovatelem služeb

Pro integraci SPID nebo CIE OIDC do vaší služby musíte dokončit akreditační proces jako Spoléhající se strana (RP) ve společnosti AgID. Proces se dělí na:

  1. Registrace na developers.italia.it: vytvořte si účet a přihlaste se k platformě SPID/CIE onboarding.
  2. Technická příprava: Implementujte svůj SP pomocí jedné z oficiálních sad SDK nebo implementace vlastní. Nakonfigurujte metadata předávající strany (Konfigurace entity JWT).
  3. Inscenační prostředí: AGID poskytuje testovací IdP (spid-test.agid.gov.it pro SAML, demo-oidc.agid.gov.it pro OIDC) s předdefinovanými testovacími uživateli.
  4. Technické ověření: váš SP je testován oficiálním validačním nástrojem AGID. Musí projít všemi povinnými testovacími případy.
  5. Právní dohoda: podpis smlouvy o členství s AgID (nebo s vybraným agregátorem, v případě členství přes agregátor).
  6. Výroba: Po schválení je váš SP registrován v produkci a uživatelích skuteční lidé se mohou ověřit.
# 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: Integrace plateb

payPA je národní platforma pro platby platebním agenturám spravovaná společností PagoPA S.p.A. Umožňuje občanům platit daně, pokuty, poplatky a jakékoli další služby PA prostřednictvím rozsáhlé sítě kanály (banky, pošty, platební aplikace, Satispay atd.).

Z technického hlediska integrace pagoPA pro věřitelskou instituci (EC) poskytuje:

  • Členství v uzlu pagoPA: přes členský portál PagoPA. Organizace se mohou připojit přímo nebo prostřednictvím autorizovaných technologických zprostředkovatelů.
  • IUV generace (Unique Payment Identifier): kód, který jednoznačně identifikuje jakákoli očekávaná platba. Formát je definován věřitelským orgánem, ale musí odpovídat pravidlům pagoPA.
  • Dluhová pozice: Na uzlu je evidován každý dluh, který má občan vůči EK pagoPA jako „dluhová pozice“ s přidruženým IUV.
  • Ověření a uzavření: pagoPA oznámí EC, když je platba dokončena, EC ověří a uzavře dluhovou pozici.
# 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
        )

Testovací a vývojová prostředí

AgID a PagoPA poskytují vyhrazená testovací prostředí pro vývoj a ověřování:

Systém Prostředí URL Test pověření
SPID OIDC Demo/Test demo.spid.gov.it Testujte uživatele na developers.italia.it
CIE OIDC Testování preprod.cie.gov.it Žádost přes portál MinInterno
SPID SAML Testování IdP spidtest.agid.gov.it test/test (úrovně 1, 2, 3)
payPA GPD UAT api.uat.platform.pagopa.it Klíč API byl požadován na portálu devOps
Pokladna pagoPA UAT uat.checkout.pagopa.it Testovací karta: 4242 4242 4242 4242

Oficiální sady SDK a doporučené knihovny

Projekt Vývojáři Itálie (developers.italia.it) udržuje oficiální sady SDK pro integraci SPID a CIE pro 5 programovacích jazyků:

  • Krajta: spid-cie-oidc-django (založené na Django), pyspid (SAML)
  • Jáva: spid-spring-integration (jarní bota)
  • .SÍŤ: spid-dotnet-sdk (ASP.NET Core)
  • PHP: spid-php
  • Rubín: spid-ruby

Pro integrace v kontextech, které nepokrývají oficiální sady SDK, jsou k dispozici úplné technické specifikace na docs.italia.it (hledejte „SPID CIE OIDC“ nebo „SPID Technical Rules“). Federace OIDC může být implementováno s libovolnou standardní knihovnou OIDC, přidáním podpory pro specifické nároky SPID/CIE.

Závěry a další kroky

Integrace SPID, CIE a pagoPA do italských digitálních služeb je požadavkem pro všechny PA chcete nabízet online služby kompatibilní s CAD. Přechod na OpenID Connect výrazně zjednodušuje implementace ve srovnání s historickým SAML 2.0 a oficiální sady SDK společnosti Developers Italia dále nižší vstupní bariéra.

V dalším a posledním článku této série prozkoumáme Stavební blok GovStack: rámec mezinárodní modulární systém pro budování opakovaně použitelných digitálních vládních služeb, který v roce 2025 přijalo více než 20 zemí.

Související články v této sérii

  • GovTech #00: Digitální veřejná infrastruktura - stavební kameny a globální architektura
  • GovTech #01: eIDAS 2.0 a EUDI Wallet – evropská digitální identita
  • GovTech #02: OpenID Connect for Government Identity – úplné zavedení
  • GovTech #04: GDPR-by-Design - uživatelská data a soukromí ve službách PA