API-integratie van de overheid: SPID, CIE en IT Digital Services
Praktische gids voor de integratie van SPID, CIE en pagoPA in Italiaanse digitale diensten: SAML 2.0, OpenID Connect Federation, AGID onboarding, officiële SDK's en integratiepatronen voor ontwikkelaars en architecten van PA-oplossingen.
Het landschap van de Italiaanse digitale identiteit
Het Italiaanse ecosysteem voor digitale identiteit is met drie een van de meest complexe in Europa belangrijkste systemen die naast elkaar bestaan en convergeren: SPID (Openbaar identiteitssysteem digitaal), CIE (Elektronische identiteitskaart) en, voor betalingen, betaalPA. Vanaf 2023 zal de transitie naar OpenID Connect als protocol unified heeft de integratie voor ontwikkelaars aanzienlijk vereenvoudigd.
Als ontwikkelaar of architect die overheidsauthenticatie in een applicatie moet integreren, krijgt u te maken met verschillende technische keuzes met aanzienlijke gevolgen. Dit artikel begeleidt u bij de AGID-onboarding tot de concrete implementatie, waarbij SAML 2.0 (het historische SPID-protocol) wordt vergeleken met OpenID Connect Federatie (de toekomst van zowel SPID als CIE), en laat zien hoe pagoPA voor betalingen kan worden geïntegreerd.
Wat je gaat leren
- SPID-architectuur: Identity Provider, Service Provider, SAML 2.0 en beveiligingsniveaus
- CIE-architectuur: NFC-chip, PIN, CIE-ID-backend en authenticatiemodus
- OpenID Connect voor SPID en CIE: federatie, tokens, claims en scopes
- AGID-onboardingproces: technische en procedurele vereisten
- Officiële SDK's: bibliotheken voor 5 programmeertalen
- Praktische implementatie: Python en TypeScript met volledige voorbeelden
- pagoPA: betalingsintegratie in PA-diensten
- Test- en faseringsomgevingen voor ontwikkeling en validatie
SPID: Openbaar digitaal identiteitssysteem
SPID is het Italiaanse nationale digitale identiteitssysteem, opgericht door Wetgevend besluit 82/2005 (CAD) en gereguleerd door AgID. Hiermee kunnen burgers zich authenticeren op elke PA-dienst (en vele private) met één paar inloggegevens, beheerd door een van de Identiteitsprovider (IdP) geaccrediteerd (Aruba, Infocert, Namirial, Poste, Register, Sielte, SpidItalia, Tim, Intesa).
SPID voorspelt 3 beveiligingsniveaus:
- Niveau 1: authenticatie met gebruikersnaam en wachtwoord. Geschikt voor diensten met een laag risico.
- Niveau 2: gebruikersnaam/wachtwoord + OTP (SMS of app). Het meest gebruikte niveau voor PA-diensten. Vereist een tweede authenticatiefactor (2FA).
- Niveau 3: authenticatie met digitaal certificaat of smartcard. Voor hoogwaardige diensten risico (notariële akten, gekwalificeerde digitale handtekening).
CIE: Elektronische Identiteitskaart
De CIE 3.0, uitgegeven door het State Printing and Mint Institute, bevat een NFC-chip met certificaten X.509 digitale apparaten die sterke authenticatie mogelijk maken. Het CIE-ID-systeem maakt online authenticatie mogelijk via:
- Smartphone met NFC: de gebruiker brengt de CIE dicht bij de smartphone en voert de pincode in. De CIE ID-app genereert een digitaal ondertekende authenticatiebewering.
- Desktop met NFC-lezer: via de CIE ID-software van het Ministerie van Binnenlandse Zaken.
- Desktop zonder NFC: authenticatie via QR-code gescand met de CIE ID-app.
Vanuit ontwikkelaarsperspectief delen CIE en SPID nu hetzelfde OpenID Connect (OIDC)-protocol, met verschillen voornamelijk in de registratie en metadata van de Identity Provider.
OpenID Connect voor SPID en CIE: het Unified Protocol
AgID publiceerde de OpenID Connect Technische regels voor SPID en CIE, beschikbaar op docs.italia.it. Deze regels definiëren a OpenID Connect-federatie gebaseerd op de OIDC Federation 1.0-standaard (IETF-concept), waarbij:
- AgID het is er Vertrouwen anker: het hoofdknooppunt van de federatie, de bron van waarheid voor vertrouwen tussen alle deelnemers.
- Identiteitsproviders (hetzelfde als SPID) zijn Bladknooppunten registreren bij AgID.
- I Vertrouwende partij (diensten die SPID/CIE willen gebruiken) registreren door te publiceren een Entiteitsconfiguratie (een JWT ondertekend met uw openbare sleutels).
# 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
SPID- en CIE-claims: gebruikerskenmerken
Een belangrijk verschil tussen SPID en typische particuliere OIDC-aanbieders betreft de beweringen (attributen gebruiker) beschikbaar. SPID/CIE biedt een reeks kenmerken die zijn gecertificeerd door de Italiaanse overheid:
| OIDC claimen | SPID-kenmerk | Verkrijgbaar in CIE | Opmerkingen |
|---|---|---|---|
sub |
Unieke ID bij de IdP | Si | Het is niet de CF; veranderingen tussen verschillende IdP’s |
fiscal_number |
Belasting-ID-code | Si | Formaat: TINIT-XXXXXXXXXXXXXX |
given_name |
Naam | Si | OIDC-normen |
family_name |
Achternaam | Si | OIDC-normen |
birthdate |
Geboortedatum | Si | Formaat: JJJJ-MM-DD |
place_of_birth |
Geboorteplaats | Si | Kadastrale code van de gemeente |
gender |
Seks | Si | M/V |
email |
E-mail (niet gecertificeerd) | No | Alleen SPID, aangegeven door de gebruiker |
mobile_phone |
Mobiele telefoon | No | Alleen SPID, aangegeven door de gebruiker |
document_details |
Documentgegevens | Si | Alleen CIE: num. CIE, vervaldatum, veelvoorkomend probleem |
Waarschuwing: sub versus fiscaal_nummer
Il sub claim in SPID is de gebruikers-ID bij een specifieke IdP,
geen globale identificatie. Als een gebruiker van IdP verandert (bijvoorbeeld van Aruba naar Poste), wordt de sub wijziging.
De belastingwet (fiscal_number) is de enige stabiele en mondiale identificatie voor een burger
Italiaans. Gebruik de fiscal_number als de primaire sleutel in uw database, niet de sub.
AGID onboarden: een dienstverlener worden
Om SPID of CIE OIDC in uw dienst te integreren, moet u het accreditatieproces voltooien als Vertrouwende Partij (RP) bij AgID. Het proces is onderverdeeld in:
- Registratie op ontwikkelaars.italia.it: maak een account aan en log in op het platform van SPID/CIE-onboarding.
- Technische voorbereiding: Implementeer uw SP met een van de officiële SDK's of een implementatie gewoonte. Configureer de metagegevens van de Relying Party (Entiteitsconfiguratie JWT).
- Staging-omgeving: AGID biedt een test-IdP (spid-test.agid.gov.it voor SAML, demo-oidc.agid.gov.it voor OIDC) met vooraf gedefinieerde testgebruikers.
- Technische validatie: uw SP wordt getest met de officiële AGID-validatietool. Ze moeten alle verplichte testgevallen doorstaan.
- Juridische overeenkomst: ondertekening van de lidmaatschapsovereenkomst met AgID (of met de gekozen aggregator, in het geval van lidmaatschap via aggregator).
- Productie: Na goedkeuring wordt uw SP geregistreerd in productie en gebruikers echte mensen kunnen zich authenticeren.
# 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: Betalingsintegratie
betaalPA is het nationale platform voor betalingen aan PA’s, beheerd door PagoPA S.p.A. Het stelt burgers in staat belastingen, boetes, vergoedingen en andere PA-diensten te betalen via een uitgebreid netwerk van kanalen (banken, postkantoren, betaalapps, Satispay, etc.).
Vanuit technisch oogpunt biedt de pagoPA-integratie voor een Creditor Institution (EC) het volgende:
- Lidmaatschap van het pagoPA-knooppunt: via het PagoPA-lidmaatschapsportaal. Organisaties kunnen zich aansluiten rechtstreeks of via geautoriseerde technologische tussenpersonen.
- IUV-generatie (Unique Payment Identifier): code die op unieke wijze identificeert eventuele verwachte betaling. Het formaat wordt gedefinieerd door de Creditor Body, maar moet voldoen aan de pagoPA-regels.
- Schuldpositie: Elke schuld die de burger heeft jegens de EC wordt op het knooppunt geregistreerd pagoPA als een ‘schuldpositie’ met een bijbehorende IUV.
- Verificatie en afsluiting: pagoPA informeert de EC wanneer een betaling is voltooid, de EC verifieert en sluit de schuldpositie af.
# 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
)
Test- en ontwikkelomgevingen
AgID en PagoPA bieden speciale testomgevingen voor ontwikkeling en validatie:
| Systeem | Omgeving | URL | Referenties testen |
|---|---|---|---|
| SPID OIDC | Demo/test | demo.spid.gov.it | Test gebruikers op developers.italia.it |
| CIE OIDC | Testen | preprod.cie.gov.it | Aanvraag via MinInterno portal |
| SPID-SAML | IdP-testen | spidtest.agid.gov.it | toets/toets (niveaus 1, 2, 3) |
| payPA GPD | UAT | api.uat.platform.pagopa.it | API-sleutel aangevraagd op devOps-portal |
| pagoPA Afrekenen | UAT | uat.checkout.pagopa.it | Testkaart: 4242 4242 4242 4242 |
Officiële SDK's en aanbevolen bibliotheken
Het project Ontwikkelaars Italië (developers.italia.it) onderhoudt officiële SDK's voor integratie van SPID en CIE voor 5 programmeertalen:
- Python:
spid-cie-oidc-django(gebaseerd op Django),pyspid(SAML) - Java:
spid-spring-integration(Lentelaars) - .NETTO:
spid-dotnet-sdk(ASP.NET kern) - PHP:
spid-php - Robijn:
spid-ruby
Voor integraties in contexten die niet onder de officiële SDK's vallen, zijn volledige technische specificaties beschikbaar op docs.italia.it (zoek naar "SPID CIE OIDC" of "SPID Technical Rules"). De OIDC-federatie kan dat wel zijn geïmplementeerd met elke standaard OIDC-bibliotheek, waardoor ondersteuning wordt toegevoegd voor SPID/CIE-specifieke claims.
Conclusies en volgende stappen
De integratie van SPID, CIE en pagoPA in Italiaanse digitale diensten is een vereiste voor elke PA die dat doet CAD-compatibele onlinediensten willen aanbieden. De overstap naar OpenID Connect vereenvoudigt aanzienlijk de implementatie vergeleken met de historische SAML 2.0, en de officiële SDK's van Developers Italia zijn verder lager de toegangsbarrière.
In het volgende en laatste artikel van deze serie verkennen we de GovStack-bouwsteen: het raamwerk internationaal modulair systeem om herbruikbare digitale overheidsdiensten te bouwen, dat in 2025 door meer dan 20 landen is ingevoerd.
Gerelateerde artikelen in deze serie
- GovTech #00: Digitale publieke infrastructuur - bouwstenen en mondiale architectuur
- GovTech #01: eIDAS 2.0 en EUDI Wallet - Europese digitale identiteit
- GovTech #02: OpenID Connect voor overheidsidentiteit - volledige uitrol
- GovTech #04: GDPR-by-Design - gebruikersgegevens en privacy in PA-services







