이탈리아 정부 정체성에서 OIDC로의 진화

10년 넘게 이탈리아의 디지털 신원은 프로토콜을 기반으로 해왔습니다. SAML 2.0: 견고하고 표준화가 잘 되어 있으며 하지만 모바일 이전 시대에 설계되었습니다. 스마트폰용 네이티브 애플리케이션의 폭발적인 증가와 단일 페이지 애플리케이션의 확산으로 SAML은 부피가 큰 XML 교환, 열악한 모바일 지원, SPA와의 복잡한 통합 등의 한계를 보여주었습니다.

로의 전환 오픈ID 커넥트(OIDC) 그것은 불가피했다. 2023년 1월 AgID는 SPID의 OpenID Connect 지침 그리고 CIE의 OpenID Connect 지침, OAuth 2.0 기반 표준을 이탈리아 정부 정체성의 진화 경로로 공식화했습니다. PNRR 목표는 2026년 3월까지 모든 16,500개의 이탈리아 PA가 SPID 또는 CIE를 통한 전자 식별을 채택하도록 요구합니다.

OpenID Connect와 PA용 SAML을 비교하는 이유

  • 경량 토큰: XML SAML 어설션 대신 JWT — 페이로드가 60-70% 감소했습니다.
  • 모바일 우선: 기본 앱 및 SPA용으로 설계된 PKCE가 포함된 OAuth 2.0
  • 현대 생태계: 모든 언어/프레임워크에 사용 가능한 라이브러리
  • 표준화된 연합: 자동 신뢰 체인을 위한 OpenID Federation 1.0
  • EUDI 지갑을 향하여: OpenID4VP와 OpenID4VCI는 동일한 OIDC 프리미티브를 사용합니다.

SPID/CIE OIDC 페더레이션 아키텍처

SPID 및 CIE에 대한 OIDC 연합은 다음을 기반으로 합니다. OpenID 페더레이션 1.0, 체인 구성을 자동화하는 표준 개체 간의 신뢰. 이는 각 SP가 수동으로 메타데이터를 교환해야 했던 SAML 메타데이터 등록의 수동 모델을 능가합니다. 각 IdP가 포함된 XML입니다.

연맹의 단체

  • 트러스트 앵커(TA): AgID는 이탈리아 연방의 트러스트 앵커입니다. 잘 알려진 엔드포인트에 엔터티 구성을 게시합니다.
  • 중급: 여러 RP를 연합하는 AgID 승인 수집자(예: 지역 PA, 지방자치 수집자).
  • 신뢰당사자(RP): SPID/CIE OIDC를 통해 사용자를 인증하는 애플리케이션입니다.
  • OP(ID 제공자): SPID 공급자(Poste Italiane, Aruba, InfoCert 등) 또는 내무부의 CIE IdP.

엔터티 구성 및 신뢰 체인

// Entity Configuration di un Relying Party
// Disponibile al well-known endpoint:
// GET https://my-service.comune.it/.well-known/openid-federation

{
  "iss": "https://my-service.comune.it",
  "sub": "https://my-service.comune.it",
  "iat": 1710000000,
  "exp": 1741536000,
  "jwks": {
    "keys": [
      {
        "kty": "EC",
        "kid": "rp-sig-key-2025",
        "crv": "P-256",
        "x": "...",
        "y": "...",
        "use": "sig"
      }
    ]
  },
  "metadata": {
    "openid_relying_party": {
      "application_type": "web",
      "client_id": "https://my-service.comune.it",
      "client_registration_types": ["automatic"],
      "redirect_uris": [
        "https://my-service.comune.it/callback"
      ],
      "response_types": ["code"],
      "grant_types": ["authorization_code"],
      "scope": "openid profile email",
      "token_endpoint_auth_method": "private_key_jwt",
      "id_token_signed_response_alg": "ES256",
      "subject_type": "pairwise",
      "contacts": ["tech-team@comune.it"]
    }
  },
  "authority_hints": [
    "https://registry.spid.gov.it"
  ]
}

PKCE를 사용하여 인증 코드 흐름 구현

SPID/CIE OIDC에 권장되는 흐름은 다음과 같습니다.PKCE를 사용한 인증 코드 흐름 (코드 교환을 위한 증명 키, RFC 7636) PKCE는 가로채는 인증 코드로부터 보호하며 모바일 앱에서는 필수이고 웹 앱에서는 권장됩니다.

1단계: PKCE 생성 및 승인 요청

// TypeScript - Authorization Request con PKCE

import crypto from "crypto";

function generatePKCE() {
  // code_verifier: stringa casuale 43-128 caratteri
  const codeVerifier = crypto.randomBytes(32)
    .toString("base64url");

  // code_challenge: SHA256 del verifier, base64url encoded
  const codeChallenge = crypto.createHash("sha256")
    .update(codeVerifier)
    .digest("base64url");

  return { codeVerifier, codeChallenge };
}

async function initiateLogin(req: Request, res: Response) {
  const { codeVerifier, codeChallenge } = generatePKCE();
  const state = crypto.randomBytes(16).toString("base64url");
  const nonce = crypto.randomBytes(16).toString("base64url");

  // Salva in sessione (server-side)
  req.session.pkce = { codeVerifier, state, nonce };

  // Costruisci l'Authorization Request
  const params = new URLSearchParams({
    response_type: "code",
    client_id: "https://my-service.comune.it",
    redirect_uri: "https://my-service.comune.it/callback",
    scope: "openid profile",
    // Per SPID LoA 2 (identità verificata con password forte)
    acr_values: "https://www.spid.gov.it/SpidL2",
    state,
    nonce,
    code_challenge: codeChallenge,
    code_challenge_method: "S256",
    // Parametri SPID specifici
    prompt: "login",
  });

  // Scopri il provider selezionato tramite OP Selector o discovery
  const opAuthEndpoint = await discoverOPEndpoint("https://idp.poste.it");

  res.redirect(`${opAuthEndpoint}?${params.toString()}`);
}

2단계: 콜백 및 토큰 교환

// TypeScript - Gestione callback e token exchange

async function handleCallback(req: Request, res: Response) {
  const { code, state, error } = req.query;
  const session = req.session.pkce;

  // Verifica state per prevenire CSRF
  if (state !== session.state) {
    return res.status(400).json({ error: "State mismatch" });
  }

  if (error) {
    return res.status(400).json({ error });
  }

  // Token Request - usa private_key_jwt per autenticarsi al token endpoint
  const clientAssertion = await createClientAssertion({
    iss: "https://my-service.comune.it",
    sub: "https://my-service.comune.it",
    aud: "https://idp.poste.it/oauth/token",
    jti: crypto.randomBytes(16).toString("hex"),
    iat: Math.floor(Date.now() / 1000),
    exp: Math.floor(Date.now() / 1000) + 60,
  });

  const tokenResponse = await fetch("https://idp.poste.it/oauth/token", {
    method: "POST",
    headers: { "Content-Type": "application/x-www-form-urlencoded" },
    body: new URLSearchParams({
      grant_type: "authorization_code",
      code: code as string,
      redirect_uri: "https://my-service.comune.it/callback",
      client_id: "https://my-service.comune.it",
      client_assertion_type: "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
      client_assertion: clientAssertion,
      code_verifier: session.codeVerifier,
    }),
  });

  const tokens = await tokenResponse.json();

  // Verifica e decodifica l'ID Token
  const idToken = await verifyIdToken(tokens.id_token, {
    expectedNonce: session.nonce,
    expectedAudience: "https://my-service.comune.it",
  });

  // idToken.sub = identificatore opaco pairwise dell'utente
  // idToken.acr = livello di garanzia (LoA) ottenuto
  console.log("User authenticated:", idToken.sub);
  console.log("LoA achieved:", idToken.acr);
}

SPID 및 CIE의 LoA(보증 수준)

정부 정체성의 가장 중요한 측면 중 하나는 보증 수준(LoA), 이는 인증 프로세스의 신뢰성을 나타냅니다. SPID는 세 가지 수준을 정의합니다.

수준 ACR URI 방법 일반적인 사용
SpidL1 https://www.spid.gov.it/SpidL1 사용자 이름 + 비밀번호 저위험 서비스, 민감하지 않은 데이터 상담
SpidL2 https://www.spid.gov.it/SpidL2 사용자 이름 + 비밀번호 + OTP/푸시 표준 PA 서비스(신고, 신청, 결제)
SpidL3 https://www.spid.gov.it/SpidL3 인증된 물리적 장치 고위험 작업, 적격 전자 서명

을 위한 CIE(전자 신분증), 보증 수준은 항상 LoA3(최대)입니다. 문서와 PIN을 물리적으로 소유해야 하기 때문에 CIE NFC 칩을 사용했습니다. 이는 CIE를 확실한 선택으로 만듭니다. 강력한 인증이 필요한 서비스의 경우.

UserInfo 끝점 및 SPID/CIE 특성

로그인 후 신뢰 당사자는 액세스 토큰을 사용하여 UserInfo 끝점에서 사용자 속성을 검색할 수 있습니다. SPID OIDC에서 사용할 수 있는 속성은 기술 프로필에 정의되어 있습니다.

// GET /userinfo
// Authorization: Bearer <access_token>

// Risposta UserInfo SPID
{
  "sub": "c5d9b2f3-1a4e-4b8d-9f2a-3e5c7d8f9b1a",  // pairwise pseudonimo
  "https://attributes.spid.gov.it/name": "Mario",
  "https://attributes.spid.gov.it/familyName": "Rossi",
  "https://attributes.spid.gov.it/fiscalNumber": "RSSMRA90A15H501Z",
  "https://attributes.spid.gov.it/dateOfBirth": "1990-01-15",
  "https://attributes.spid.gov.it/placeOfBirth": "Roma",
  "https://attributes.spid.gov.it/countyOfBirth": "RM",
  "https://attributes.spid.gov.it/idCard": "MRTMRA90A15H501Z",
  "https://attributes.spid.gov.it/address": {
    "formatted": "Via Roma 1, 00100 Roma RM"
  },
  "https://attributes.spid.gov.it/email": "mario.rossi@example.com",
  "https://attributes.spid.gov.it/mobilePhone": "+39 333 1234567",
  "acr": "https://www.spid.gov.it/SpidL2"
}

// ATTENZIONE: richiedere solo gli attributi strettamente necessari
// nell'Authorization Request tramite il parametro 'claims':
// "claims": {
//   "userinfo": {
//     "https://attributes.spid.gov.it/fiscalNumber": {"essential": true},
//     "https://attributes.spid.gov.it/name": null
//   }
// }

SPID 속성의 개인정보 보호 및 최소화

실제 필요 없이 사용자의 세금 코드를 요청하는 것은 GDPR 최소화 원칙을 위반하는 것입니다. AgID 지침은 요청된 각 속성이 서비스의 특정 기능에 의해 동기가 부여되어야 한다고 명시합니다. 개인정보 보호정책에 명시되어 있습니다. AgID 감사 중에 요청된 속성을 정당화하지 못할 경우 인증취소로 이어지게 됩니다.

SPID/CIE OIDC용 SDK 및 공식 라이브러리

프로젝트 개발자 이탈리아 (developers.italia.it)는 다양한 언어로 공식 SDK를 제공합니다. SPID/CIE OIDC 통합을 단순화하려면:

## SDK Ufficiali Developers Italia - SPID/CIE OIDC Federation

# Python (Django)
pip install spid-cie-oidc

# Configurazione in Django settings.py
INSTALLED_APPS = [
    ...
    "spid_cie_oidc.entity",
    "spid_cie_oidc.relying_party",
]

OIDCFED_DEFAULT_TRUST_ANCHOR = "https://registry.spid.gov.it"

OIDCFED_IDENTITY_PROVIDERS = {
    "spid": "https://registry.spid.gov.it",
    "cie": "https://idserver.servizicie.interno.gov.it"
}

# Configura automaticamente gli endpoint di federazione
# e gestisce la trust chain dinamicamente

# Java
# Maven: eu.italia.gov:spid-cie-oidc-java:latest

# .NET
# NuGet: AGID.SPID.OIDC

# Node.js (Express)
npm install spid-cie-oidc-node

# PHP
composer require italia/spid-php-lib

JARM: JWT 기반 인증 응답 모드

SPID/CIE OIDC 지침에서는 다음을 사용하도록 권장합니다. JARM(JWT 인증 응답 모드) 보호하기 위해 승인 응답. 통과하는 대신 code e state 쿼리 매개변수로 일반 텍스트로, JARM은 아이덴티티 공급자가 서명한 JWT에 응답을 캡슐화합니다.

// Authorization Request con JARM
{
  "response_type": "code",
  "response_mode": "form_post.jwt",  // JARM mode
  "client_id": "https://my-service.comune.it",
  ...
}

// Risposta JARM dal IdP (form POST al redirect_uri)
// Il parametro 'response' contiene un JWT firmato:
// Header:
{ "alg": "ES256", "kid": "idp-key-2025" }
// Payload:
{
  "iss": "https://idp.poste.it",
  "aud": "https://my-service.comune.it",
  "code": "SplxlOBeZQQYbYS6WxSbIA",
  "state": "af0ifjsldkj",
  "iat": 1710000000,
  "exp": 1710000180
}

// Vantaggi JARM:
// 1. Autenticità: garantisce che la risposta venga dall'IdP corretto
// 2. Integrità: impossibile modificare code/state in transito
// 3. Non-repudiation: log auditabili con prova crittografica

세션 관리 및 토큰 새로 고침

정부 정체성의 중요한 측면은 세션의 올바른 관리입니다. 민간부문과 달리, PA 서비스는 세션 기간 및 로그아웃에 대한 엄격한 제약을 준수해야 합니다.

// Gestione sessione sicura per servizi PA

interface SessionConfig {
  maxAge: number;           // durata massima sessione in secondi
  inactivityTimeout: number; // timeout per inattività
  requireReauth: boolean;    // richiede riautenticazione per operazioni critiche
}

// Configurazione raccomandata per servizi PA
const SESSION_CONFIG: SessionConfig = {
  maxAge: 3600,          // 1 ora massimo (SPID raccomanda max 30 min per L2)
  inactivityTimeout: 900, // 15 min di inattività
  requireReauth: true     // operazioni di firma richiedono nuovo login
};

// Logout corretto: Single Logout (SLO) obbligatorio per SPID
async function handleLogout(req: Request, res: Response) {
  const session = req.session;

  // 1. Costruisci logout request per l'IdP
  const logoutRequest = createSignedLogoutRequest({
    iss: "https://my-service.comune.it",
    sub: session.userId,
    sid: session.idTokenHint,  // session ID dal ID Token
    aud: session.idpLogoutEndpoint,
    post_logout_redirect_uri: "https://my-service.comune.it/logout-complete",
  });

  // 2. Distruggi la sessione locale prima del redirect
  await req.session.destroy();

  // 3. Redirect all'IdP per SLO
  res.redirect(
    `${session.idpLogoutEndpoint}?` +
    `id_token_hint=${session.idToken}&` +
    `post_logout_redirect_uri=https://my-service.comune.it/logout-complete&` +
    `state=${generateState()}`
  );
}

AgID 인증 및 규정 준수

SPID를 해당 서비스에 통합하려면 신뢰 당사자가 AgID 인증 프로세스를 통과해야 합니다. OIDC의 단순화된 절차는 2023년부터 제공됩니다.

  1. 기술등록: 엔터티 구성 구성 및 엔드포인트 확인 /.well-known/openid-federation. AgID는 자동 도구를 통해 규정 준수 여부를 확인합니다.
  2. 기능 테스트: spid-sp-test(오픈 소스 Python 도구)를 사용하여 올바른 구현을 확인합니다. 이 도구는 아이덴티티 공급자의 동작을 시뮬레이션하고 200개 이상의 테스트 사례를 확인합니다.
  3. GDPR 준수: 각 필수 SPID 특성에 대한 데이터 처리 문서입니다.
  4. 계약서 서명: SPID 기술 규칙 및 회원 계약에 동의합니다.
## Uso di spid-sp-test per verificare l'implementazione

# Installazione
pip install spid_sp_test

# Verifica metadata OIDC
spid_sp_test \
  --metadata-url https://my-service.comune.it/.well-known/openid-federation \
  --profile oidc_op

# Test flusso completo (richiede credenziali di test)
spid_sp_test \
  --metadata-url https://my-service.comune.it/.well-known/openid-federation \
  --authn-url https://my-service.comune.it/login/spid \
  --profile oidc_sp_public \
  --extra \
  --debug

# Output: report HTML con dettagli dei 200+ test
# File: report_<timestamp>.html

융합을 향하여: SPID/CIE OIDC 및 EUDI Wallet

SAML에서 OIDC로의 전환이 최종 목적지는 아닙니다. 생태계는 완전한 통합을 향해 나아가고 있습니다. 와 EUDI 지갑 OpenID4VP를 통해. 프로젝트 iam-프록시-이탈리아 GitHub에서는 이미 시연했습니다. IAM 프록시가 SAML 2.0, OIDC 및 OpenID4VC(IT-Wallet/EUDI Wallet용)를 동시에 지원할 수 있는 방법.

현대 PA 서비스를 위한 이상적인 아키텍처는 세 가지 흐름을 모두 투명하게 지원해야 합니다.

// Middleware di autenticazione multi-protocollo

interface AuthProvider {
  protocol: "saml2" | "oidc" | "openid4vp";
  name: string;
  endpoint: string;
}

const AUTH_PROVIDERS: AuthProvider[] = [
  {
    protocol: "oidc",
    name: "SPID",
    endpoint: "https://registry.spid.gov.it"
  },
  {
    protocol: "oidc",
    name: "CIE",
    endpoint: "https://idserver.servizicie.interno.gov.it"
  },
  {
    protocol: "openid4vp",
    name: "IT Wallet / EUDI Wallet",
    endpoint: "eudi-openid4vp://"
  }
];

function selectAuthFlow(userAgent: string, userPreference?: string): AuthProvider {
  // Logica di selezione basata su contesto
  if (userPreference === "wallet" || hasWalletApp(userAgent)) {
    return AUTH_PROVIDERS.find(p => p.protocol === "openid4vp")!;
  }
  // Default: CIE OIDC (LoA3 per impostazione predefinita)
  return AUTH_PROVIDERS.find(p => p.name === "CIE")!;
}

모범 사례 및 안티 패턴

SPID/CIE OIDC 모범 사례

  • 웹 애플리케이션에도 항상 PKCE를 사용합니다(네이티브 앱의 경우 필수).
  • 승인 응답을 보호하기 위해 JARM을 구현합니다.
  • 12개월마다(또는 더 자주) RP 서명 키 교체
  • 미국 sub 내부 식별자로 쌍으로 사용 — CF나 이메일과 같은 속성은 사용하지 않음
  • 싱글 로그아웃(SLO) 구현 - SPID 인증에 필수
  • 최대 24시간 동안 신뢰 체인을 캐시(그런 다음 업데이트)
  • 엔터티 문 만료 ​​모니터링(일반적으로 24시간)

피해야 할 안티패턴

  • ID 토큰을 저장하지 마십시오. localStorage 또는 비HttpOnly 쿠키
  • 사용하지 마십시오 response_mode=query (미국 form_post o form_post.jwt)
  • 정당한 이유 없이 액세스 토큰을 프런트엔드에 노출하지 마세요.
  • 불필요한 SPID 속성을 요청하지 마세요(인증 위험).
  • 확인을 무시하지 마십시오 nonce 토큰 ID에서
  • 만료되지 않는 세션을 사용하지 마세요: PA 세션에는 시간 초과가 있어야 합니다.

결론

OpenID Connect는 이탈리아와 유럽 정부 인증의 미래를 나타냅니다. SAML에서 OIDC로의 전환 이는 기술적일 뿐만 아니라 보다 현대적인 생태계, 더 나은 도구 및 자연스러운 경로를 제공합니다. EUDI 지갑을 통해 자주적 신원을 지향합니다.

오늘부터 개발자는 올바른 패턴(PKCE, JARM, 자동 페더레이션, 쌍별 주제 — OpenID4VP 및 EUDI Wallet과의 후속 통합을 위한 최적의 위치에 있습니다. 오늘 잘 작성된 코드는 내일 다시 작성할 필요가 없습니다. 이는 공공 디지털 서비스의 연속성에 대한 투자입니다.

GovTech 시리즈는 계속됩니다

다음 기사에서는 개방형 데이터 API의 설계와 공공 데이터를 효율적으로 게시하고 사용하는 방법을 살펴보세요.