政府 API の統合: SPID、CIE、IT デジタル サービス
イタリアのデジタル サービスにおける SPID、CIE、pagoPA の統合に関する実践的なガイド: SAML 2.0、 OpenID Connect フェデレーション、AGID オンボーディング、公式 SDK、および統合パターン PA ソリューションの開発者およびアーキテクト。
イタリアのデジタル アイデンティティの風景
イタリアのデジタル ID エコシステムは、ヨーロッパで最も複雑なエコシステムの 1 つであり、次の 3 つがあります。 共存および統合される主要システム: SPID (パブリックアイデンティティシステム デジタル)、 CIE (電子 ID カード)、および支払いの場合、 PayPA。 2023年からは、 OpenID コネクト プロトコルとして 統合により、開発者にとって統合が大幅に簡素化されました。
政府認証をアプリケーションに統合する必要がある開発者またはアーキテクトは、次のような問題に直面します。 重大な影響をもたらすいくつかの技術的な選択。この記事では、AGID のオンボーディングについて説明します SAML 2.0 (歴史的な SPID プロトコル) と OpenID Connect を比較しながら、具体的な実装まで説明します。 フェデレーション (SPID と CIE 両方の将来)、および支払いのために pagoPA を統合する方法を示します。
何を学ぶか
- SPID アーキテクチャ: ID プロバイダー、サービス プロバイダー、SAML 2.0、およびセキュリティ レベル
- CIE アーキテクチャ: NFC チップ、PIN、CIE-ID バックエンドおよび認証モード
- SPID および CIE 用の OpenID Connect: フェデレーション、トークン、クレーム、およびスコープ
- AGID オンボーディング プロセス: 技術的および手順上の要件
- 公式SDK: 5つのプログラミング言語のライブラリ
- 実際の実装: Python と TypeScript と完全な例
- pagoPA: PA サービスへの支払いの統合
- 開発と検証のためのテスト環境とステージング環境
SPID: パブリックデジタルアイデンティティシステム
SPID は、イタリア国家デジタル ID システムであり、 法令 82/2005 (CAD) AgID によって規制されています。これにより、国民はあらゆる PA サービス (および多くの PA サービス) で認証できるようになります。 private) の 1 組の資格情報を使用し、次のいずれかによって管理されます。 アイデンティティプロバイダー (IdP) 認定済み (Aruba、Infocert、Namirial、Poste、Register、Sielte、SpidItalia、Tim、Intesa)。
SPID 予測 3レベルのセキュリティ:
- レベル1: ユーザー名とパスワードによる認証。リスクの低いサービスに適しています。
- レベル2: ユーザー名/パスワード + OTP (SMS またはアプリ)。 PA サービスで最もよく使用されるレベル。 認証の第 2 要素 (2FA) が必要です。
- レベル3: デジタル証明書またはスマート カードによる認証。高性能なサービスを目指して リスク(公正証書、適格なデジタル署名)。
CIE: 電子 ID カード
米国立印刷造幣局が発行した CIE 3.0 には、証明書付きの NFC チップが含まれています 強力な認証を可能にする X.509 デジタル デバイス。 CIE-IDシステムによりオンライン認証が可能 経由:
- NFC搭載スマートフォン: ユーザーは CIE をスマートフォンに近づけて PIN を入力します。 CIE ID アプリは、デジタル署名された認証アサーションを生成します。
- NFCリーダーを備えたデスクトップ: 内務省の CIE ID ソフトウェア経由。
- NFC非搭載のデスクトップ: CIE ID アプリでスキャンした QR コードによる認証。
開発者の観点から見ると、CIE と SPID は同じ OpenID Connect (OIDC) プロトコルを共有するようになりました。 相違点は主にアイデンティティ プロバイダーの登録とメタデータです。
SPID および CIE 用の OpenID Connect: 統一プロトコル
AgID が公開した SPID および CIE に関する OpenID Connect 技術ルール、 docs.italia.it で入手できます。これらのルールは、 OpenID Connect フェデレーション OIDC Federation 1.0 標準 (IETF ドラフト) に基づいており、以下のとおりです。
- AgID そこにあります トラストアンカー: フェデレーションのルート ノード、 すべての参加者間の信頼の信頼の源。
- アイデンティティプロバイダー (SPIDと同じ) リーフノード 登録する AgIDで。
- I 依拠当事者 (SPID/CIEを使用したいサービス)公開による登録 ある エンティティの構成 (公開鍵で署名された JWT)。
# 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 および CIE クレーム: ユーザー属性
SPID と一般的なプライベート OIDC プロバイダーの重要な違いは、 主張 (属性 ユーザー) が利用可能です。 SPID/CIE は、イタリア政府によって認定された一連の属性を提供します。
| OIDC を請求する | SPID 属性 | CIEで入手可能 | 注意事項 |
|---|---|---|---|
sub |
IdP での一意の ID | Si | それはCFではありません。異なる IdP 間の変更 |
fiscal_number |
納税者IDコード | Si | 形式: TINIT-XXXXXXXXXXXXXX |
given_name |
名前 | Si | OIDC規格 |
family_name |
Cognome | Si | OIDC規格 |
birthdate |
生年月日 | Si | 形式: YYYY-MM-DD |
place_of_birth |
出生地 | Si | 市区町村の地籍コード |
gender |
セックス | Si | 男/女 |
email |
電子メール (認証されていません) | No | ユーザーが宣言した SPID のみ |
mobile_phone |
携帯電話 | No | ユーザーが宣言した SPID のみ |
document_details |
文書データ | Si | CIE のみ: 数値。 CIE、有効期限、一般的な問題 |
警告: sub と Financial_number
Il sub SPID のクレームはユーザー識別子です 特定の IdP で、
グローバル識別子ではありません。ユーザーが IdP を変更した場合 (Aruba から Poste など)、 sub 変化。
税法 (fiscal_number) は国民の唯一の安定したグローバルな識別子です
イタリア人。を使用します。 fiscal_number データベースの主キーとしてではなく、 sub.
AGID のオンボーディング: サービス プロバイダーになる
SPID または CIE OIDC をサービスに統合するには、次のとおり認定プロセスを完了する必要があります。 依拠当事者 (RP) AgIDで。プロセスは次のように分かれています。
- developers.italia.it への登録: アカウントを作成してプラットフォームにログインします SPID/CIE オンボーディングの。
- 技術的な準備: 公式 SDK または実装のいずれかを使用して SP を実装します。 カスタム。証明書利用者のメタデータ (エンティティ構成 JWT) を構成します。
- ステージング環境: AGID はテスト IdP (SAML の場合は spid-test.agid.gov.it、 Demo-oidc.agid.gov.it (OIDC 用) と事前定義されたテスト ユーザー。
- 技術的検証: SP は公式 AGID 検証ツールでテストされています。 すべての必須テスト ケースに合格する必要があります。
- 法的合意: AgID (または選択したアグリゲーターとのメンバーシップ契約の署名) アグリゲータ経由のメンバーシップの場合)。
- 生産: 承認後、SP は本番環境とユーザーに登録されます。 実在の人物を認証できます。
# 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: 支払いの統合
PayPA は、PagoPA S.p.A が管理する PA への支払いのための全国的なプラットフォームです。 これにより、国民は、広大なネットワークを通じて税金、罰金、料金、その他の PA サービスを支払うことができます。 チャネル (銀行、郵便局、支払いアプリ、Satispay など)。
技術的な観点から見ると、債権者機関 (EC) 向けの pagoPA 統合は以下を提供します。
- pagoPA ノードのメンバーシップ: PagoPA メンバーシップ ポータル経由。参加できる組織 直接または認可された技術仲介者を通じて。
- IUVの生成 (Unique Payment Identifier): 一意に識別するコード 予想される支払い。形式は債権者団体によって定義されますが、pagoPA 規則に準拠する必要があります。
- 負債の状況: 国民が EC に対して負っているすべての負債はノードに記録されます 関連する IUV を伴う「負債ポジション」としての pagoPA。
- 検証と閉鎖: pagoPA は支払いが完了すると EC に通知します。 債務ポジションを確認してクローズします。
# 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
)
テストおよび開発環境
AgID と PagoPA は、開発と検証のための専用のテスト環境を提供します。
| システム | 環境 | URL | 資格証明テスト |
|---|---|---|---|
| SPID OIDC | デモ/テスト | デモ.spid.gov.it | Developers.italia.it でユーザーをテストする |
| CIE OIDC | テスト | preprod.cie.gov.it | MinInterno ポータル経由でリクエストする |
| SPID SAML | IdP テスト | spidtest.agid.gov.it | テスト/テスト (レベル 1、2、3) |
| PayPA GPD | UAT | api.uat.platform.pagopa.it | devOps ポータルでリクエストされた API キー |
| パゴPAチェックアウト | UAT | uat.checkout.pagopa.it | テストカード: 4242 4242 4242 4242 |
公式SDKと推奨ライブラリ
プロジェクト 開発者イタリア (developers.italia.it) は統合用の公式 SDK を維持します 5 つのプログラミング言語の SPID と CIE:
- パイソン:
spid-cie-oidc-django(Django に基づく)、pyspid(SAML) - ジャワ:
spid-spring-integration(スプリングブーツ) - 。ネット:
spid-dotnet-sdk(ASP.NETコア) - PHP:
spid-php - ルビー:
spid-ruby
公式 SDK でカバーされていないコンテキストでの統合については、完全な技術仕様が利用可能です docs.italia.it (「SPID CIE OIDC」または「SPID Technical Rules」を検索)。 OIDC フェデレーションは次のことができます。 任意の標準 OIDC ライブラリで実装され、SPID/CIE 固有のクレームのサポートが追加されます。
結論と次のステップ
SPID、CIE、pagoPA をイタリアのデジタル サービスに統合することは、次のような PA の要件です。 CAD 準拠のオンライン サービスを提供したいと考えています。 OpenID Connect への移行が大幅に簡素化 過去の SAML 2.0 と比較した実装、および Developers Italia の公式 SDK はさらに低い 参入障壁。
このシリーズの次の最後の記事では、 GovStack ビルディング ブロック: フレームワーク 再利用可能なデジタル政府サービスを構築するための国際モジュラー システム。2025 年までに 20 か国以上で採用される。
このシリーズの関連記事
- ガバテック #00: デジタル公共インフラ - ビルディングブロックとグローバルアーキテクチャ
- ガバメントテック #01: eIDAS 2.0 および EUDI ウォレット - ヨーロッパのデジタル ID
- ガバメントテック #02: OpenID Connect for Government Identity - 完全展開
- ガバメントテック #04: GDPR-by-Design - PA サービスにおけるユーザー データとプライバシー







