정부 API 통합: SPID, CIE 및 IT 디지털 서비스
이탈리아 디지털 서비스의 SPID, CIE 및 pagoPA 통합에 대한 실용 가이드: SAML 2.0, OpenID Connect Federation, AGID 온보딩, 공식 SDK 및 통합 패턴 PA 솔루션의 개발자 및 설계자입니다.
이탈리아 디지털 정체성의 풍경
이탈리아의 디지털 신원 생태계는 유럽에서 가장 복잡한 것 중 하나입니다. 공존하고 수렴하는 주요 시스템: SPID (공인 신원 시스템 디지털), CIE (전자 신분증) 및 결제의 경우 페이파. 2023년부터 전환 오픈아이디 커넥트 프로토콜로 통합은 개발자를 위한 통합을 크게 단순화했습니다.
정부 인증을 애플리케이션에 통합해야 하는 개발자 또는 설계자는 다음과 같은 문제에 직면하게 됩니다. 중요한 의미를 지닌 몇 가지 기술적 선택. 이 문서에서는 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는 이탈리아 국가 디지털 신원 시스템으로, 입법령 82/2005 (캐나다) AgID에 의해 규제됩니다. 이를 통해 시민들은 모든 PA 서비스(및 많은 PA 서비스)에서 인증을 받을 수 있습니다. 비공개) 다음 중 하나에 의해 관리되는 단일 자격 증명 쌍을 사용합니다. ID 공급자(IdP) 인증을 받았습니다(Aruba, Infocert, Namirial, Poste, Register, Sielte, SpidItalia, Tim, Intesa).
SPID는 예측합니다 3가지 보안 수준:
- 레벨 1: 사용자 이름과 비밀번호로 인증합니다. 위험도가 낮은 서비스에 적합합니다.
- 레벨 2: 사용자 이름/비밀번호 + OTP(SMS 또는 앱). PA 서비스에 가장 많이 사용되는 수준입니다. 두 번째 인증 요소(2FA)가 필요합니다.
- 레벨 3: 디지털 인증서나 스마트카드로 인증합니다. 고성능 서비스를 위해 위험(공증인 증서, 적격 디지털 서명).
CIE: 전자 신분증
State Printing and Mint Institute에서 발행한 CIE 3.0에는 인증서가 포함된 NFC 칩이 포함되어 있습니다. 강력한 인증을 가능하게 하는 X.509 디지털 장치입니다. CIE-ID 시스템을 통해 온라인 인증이 가능합니다. 다음을 통해:
- NFC를 탑재한 스마트폰: 사용자가 CIE를 스마트폰에 가까이 대고 PIN을 입력합니다. CIE ID 앱은 디지털 서명된 인증 어설션을 생성합니다.
- NFC 리더가 탑재된 데스크탑: 내무부의 CIE ID 소프트웨어를 통해.
- NFC가 없는 데스크탑: CIE ID 앱으로 스캔한 QR코드를 통해 인증합니다.
개발자 관점에서 CIE와 SPID는 이제 동일한 OIDC(OpenID Connect) 프로토콜을 공유합니다. 주로 아이덴티티 공급자의 등록 및 메타데이터에 차이가 있습니다.
SPID 및 CIE용 OpenID Connect: 통합 프로토콜
AgID는 SPID 및 CIE에 대한 OpenID Connect 기술 규칙, docs.italia.it에서 볼 수 있습니다. 이 규칙은 OpenID Connect 페더레이션 OIDC Federation 1.0 표준(IETF 초안)을 기반으로 합니다.
- AgID 거기 있어요 트러스트 앵커: 연합의 루트 노드, 모든 참가자 간의 신뢰를 위한 진실의 원천입니다.
- ID 제공자 (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, 만료, 일반적인 문제 |
경고: 하위 대 회계_번호
Il sub SPID의 클레임은 사용자 식별자입니다. 특정 IdP에서,
전역 식별자가 아닙니다. 사용자가 IdP를 변경하는 경우(예: Aruba에서 Poste로) sub 변화.
세금 코드(fiscal_number)은 시민을 위한 유일하고 안정적인 글로벌 식별자입니다.
이탈리아어. 사용 fiscal_number 데이터베이스의 기본 키로 사용하는 것이 아니라 sub.
AGID 온보딩: 서비스 제공업체 되기
SPID 또는 CIE OIDC를 서비스에 통합하려면 다음과 같이 인증 프로세스를 완료해야 합니다. 신뢰당사자(RP) AgID에서. 프로세스는 다음과 같이 나뉩니다.
- 개발자.italia.it에 등록: 계정을 만들고 플랫폼에 로그인하세요. SPID/CIE 온보딩.
- 기술적인 준비: 공식 SDK 중 하나 또는 구현을 사용하여 SP를 구현합니다. 관습. 신뢰 당사자 메타데이터(엔터티 구성 JWT)를 구성합니다.
- 스테이징 환경: AGID는 테스트 IdP(SAML의 경우 spid-test.agid.gov.it, 데모-oidc.agid.gov.it(OIDC용)) 사전 정의된 테스트 사용자가 포함됩니다.
- 기술 검증: SP는 공식 AGID 검증 도구를 사용하여 테스트됩니다. 모든 필수 테스트 케이스를 통과해야 합니다.
- 법적 합의: AgID(또는 선택한 애그리게이터, in the case of membership via aggregator).
- 생산: 승인 후 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: 결제 통합
페이파 PagoPA S.p.A가 관리하는 PA 결제를 위한 전국 플랫폼입니다. 이를 통해 시민들은 광범위한 네트워크를 통해 세금, 벌금, 수수료 및 기타 PA 서비스를 지불할 수 있습니다. 채널(은행, 우체국, 결제 앱, Satispay 등)
기술적 관점에서 EC(채권 기관)를 위한 pagoPA 통합은 다음을 제공합니다.
- pagoPA 노드의 멤버십: PagoPA 멤버십 포털을 통해. 조직이 가입할 수 있습니다. 직접 또는 공인된 기술 중개자를 통해.
- IUV 세대 (고유 결제 식별자): 고유하게 식별하는 코드 예상되는 지불. 형식은 채권자 기관에 의해 정의되지만 pagoPA 규칙을 준수해야 합니다.
- 부채 위치: 시민이 EC에 대해 가지고 있는 모든 부채는 노드에 기록됩니다. pagoPA는 관련 IUV가 있는 "부채 포지션"입니다.
- 확인 및 종료: pagoPA는 결제가 완료되면 EC에 통보하고, 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 | 개발자.italia.it에서 사용자를 테스트하세요. |
| CIE OIDC | 테스트 | preprod.cie.gov.it | MinInterno 포털을 통해 요청 |
| SPID SAML | IdP 테스트 | spidtest.agid.gov.it | 테스트/테스트(레벨 1, 2, 3) |
| 페이파 GPD | UAT | api.uat.platform.pagopa.it | devOps 포털에서 요청된 API 키 |
| pagoPA 체크아웃 | UAT | uat.checkout.pagopa.it | 테스트 카드: 4242 4242 4242 4242 |
공식 SDK 및 권장 라이브러리
프로젝트 개발자 이탈리아 (developers.italia.it)는 통합을 위한 공식 SDK를 유지 관리합니다. 5가지 프로그래밍 언어에 대한 SPID 및 CIE:
- 파이썬:
spid-cie-oidc-django(장고 기반),pyspid(SAML) - 자바:
spid-spring-integration(스프링 부트) - .그물:
spid-dotnet-sdk(ASP.NET 코어) - PHP:
spid-php - 루비:
spid-ruby
공식 SDK에서 다루지 않는 상황에서의 통합을 위해 전체 기술 사양을 사용할 수 있습니다. docs.italia.it에서("SPID CIE OIDC" 또는 "SPID 기술 규칙" 검색) OIDC 연합은 다음과 같습니다. 표준 OIDC 라이브러리로 구현되어 SPID/CIE 특정 클레임에 대한 지원을 추가합니다.
결론 및 다음 단계
SPID, CIE 및 pagoPA를 이탈리아 디지털 서비스에 통합하는 것은 다음을 수행하는 모든 PA의 요구 사항입니다. CAD 호환 온라인 서비스를 제공하고 싶습니다. OpenID Connect로의 전환이 크게 단순화되었습니다. 기존 SAML 2.0과 비교하여 구현되었으며 Developers Italia의 공식 SDK는 더욱 낮아졌습니다. 진입장벽.
이 시리즈의 다음이자 마지막 기사에서 우리는 GovStack 빌딩 블록: 프레임워크 재사용 가능한 디지털 정부 서비스를 구축하기 위한 국제 모듈식 시스템은 2025년에 20개국 이상에서 채택됩니다.
이 시리즈의 관련 기사
- 정부 기술 #00: 디지털 공공 인프라 - 빌딩 블록 및 글로벌 아키텍처
- GovTech #01: eIDAS 2.0 및 EUDI Wallet - 유럽 디지털 ID
- GovTech #02: 정부 ID용 OpenID Connect - 전체 출시
- 고브테크 #04: GDPR-by-Design - PA 서비스의 사용자 데이터 및 개인 정보 보호







