Integracja rządowych interfejsów API: usługi cyfrowe SPID, CIE i IT
Praktyczny przewodnik po integracji SPID, CIE i pagoPA we włoskich usługach cyfrowych: SAML 2.0, Federacja OpenID Connect, wdrażanie AGID, oficjalne zestawy SDK i wzorce integracji dla programiści i architekci rozwiązań PA.
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:
- Rejestracja na stronie Developers.italia.it: utwórz konto i zaloguj się na platformie wdrożenia SPID/CIE.
- 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).
- Ś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.
- Walidacja techniczna: Twój SP jest testowany za pomocą oficjalnego narzędzia do sprawdzania poprawności AGID. Muszą przejść wszystkie obowiązkowe przypadki testowe.
- Umowa prawna: podpisanie umowy członkowskiej z AgID (lub z wybranym agregatorem, w przypadku członkostwa poprzez agregator).
- 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







