본문으로 건너뛰기

GesundheitsID 명세

개요

이 문서는 독일 GesundheitsID(건강ID) 시스템과의 연동을 위한 기술적 명세를 정의합니다. GesundheitsID는 독일의 디지털 의료 생태계에서 표준화된 사용자 인증 및 본인 확인 시스템으로, OpenID Connect 프로토콜을 기반으로 구현되어 있습니다.

GesundheitsID 시스템 개요

GesundheitsID는 다음과 같은 주요 기능을 제공합니다:

  1. 사용자 인증 (Authentication)
  2. 본인 확인 (Identity Verification)
  3. 속성 제공 (Attribute Provision)
  4. 동의 관리 (Consent Management)

OpenID Connect 프로토콜

GesundheitsID는 OpenID Connect 1.0 프로토콜을 구현하고 있으며, 다음과 같은 흐름을 지원합니다:

  1. 인증 코드 흐름 (Authorization Code Flow)
  2. 암시적 흐름 (Implicit Flow)
  3. 하이브리드 흐름 (Hybrid Flow)

당사 시스템에서는 보안상의 이유로 인증 코드 흐름만 사용합니다.

엔드포인트 명세

디스커버리 엔드포인트

GET https://gesundheitsid.example.de/.well-known/openid-configuration

이 엔드포인트를 통해 GesundheitsID 서비스의 엔드포인트 정보를 자동으로 조회할 수 있습니다.

인증 엔드포인트

GET https://gesundheitsid.example.de/auth

이 엔드포인트는 사용자를 GesundheitsID 인증 페이지로 리다이렉트하는 데 사용됩니다.

토큰 엔드포인트

POST https://gesundheitsid.example.de/token

이 엔드포인트는 인증 코드를 액세스 토큰, ID 토큰, 리프레시 토큰으로 교환하는 데 사용됩니다.

사용자 정보 엔드포인트

GET https://gesundheitsid.example.de/userinfo

이 엔드포인트는 사용자 정보를 조회하는 데 사용됩니다.

JWK 세트 엔드포인트

GET https://gesundheitsid.example.de/jwks

이 엔드포인트는 ID 토큰 검증에 사용되는 공개 키를 조회하는 데 사용됩니다.

클라이언트 등록

GesundheitsID 시스템과 연동하기 위해서는 사전에 클라이언트 등록이 필요합니다. 등록 시 다음 정보가 필요합니다:

  1. 클라이언트 이름
  2. 리다이렉트 URI
  3. 로고 이미지
  4. 개인정보처리방침 URL
  5. 요청 스코프

등록이 완료되면 다음 정보가 발급됩니다:

  1. 클라이언트 ID
  2. 클라이언트 시크릿

인증 요청 파라미터

GesundheitsID 인증 요청 시 다음 파라미터를 포함해야 합니다:

파라미터설명필수 여부
client_id클라이언트 ID필수
redirect_uri인증 완료 후 리다이렉트할 URI필수
response_type'code' (인증 코드 흐름)필수
scope요청 스코프 (최소 'openid')필수
stateCSRF 방지를 위한 상태값필수
nonce재생 공격 방지를 위한 임의 값필수
acr_values인증 컨텍스트 참조 값선택
prompt인증 프롬프트 동작 ('none', 'login', 'consent')선택
max_age최대 인증 기간 (초)선택
ui_localesUI 언어 설정 (예: 'de', 'en')선택

토큰 응답 형식

토큰 엔드포인트 응답은 다음과 같은 형식입니다:

{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"id_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."
}

ID 토큰 형식

ID 토큰은 JWT(JSON Web Token) 형식으로, 다음과 같은 클레임을 포함합니다:

클레임설명필수 여부
iss발급자 (issuer) - GesundheitsID 인증 서버 URL필수
sub주체 (subject) - 사용자의 GesundheitsID필수
aud대상 (audience) - 클라이언트 ID필수
exp만료 시간 (expiration time)필수
iat발급 시간 (issued at)필수
auth_time사용자 인증 시간필수
nonce요청 시 전달한 nonce 값필수
acr인증 컨텍스트 참조 값조건부
amr인증 방법 참조 값조건부
azp권한 부여 당사자조건부
name사용자 이름선택
given_name사용자 이름 (first name)선택
family_name사용자 성 (last name)선택
email사용자 이메일선택
email_verified이메일 검증 여부선택
birthdate생년월일선택

지원 스코프

GesundheitsID는 다음 스코프를 지원합니다:

스코프설명
openidOpenID Connect 기본 스코프 (필수)
profile사용자 프로필 정보 (이름, 성별, 생년월일 등)
email사용자 이메일 주소
phone사용자 전화번호
address사용자 주소
health.insurance사용자 건강보험 정보

오류 코드

GesundheitsID 인증 및 토큰 요청 시 발생할 수 있는 주요 오류 코드:

오류 코드설명
invalid_request잘못된 요청 파라미터
unauthorized_client승인되지 않은 클라이언트
access_denied사용자가 접근을 거부함
unsupported_response_type지원하지 않는 응답 타입
invalid_scope잘못된 스코프
server_error서버 오류
temporarily_unavailable일시적으로 서비스 사용 불가
invalid_grant잘못된 인증 코드 또는 리프레시 토큰
invalid_client클라이언트 인증 실패

보안 고려사항

인증 코드 흐름

인증 코드 흐름은 다음과 같은 이유로 가장 안전한 방식입니다:

  1. 클라이언트 시크릿이 백엔드에서만 사용됨
  2. 액세스 토큰이 프론트엔드로 노출되지 않음
  3. PKCE(Proof Key for Code Exchange) 확장을 지원하여 추가 보안 제공

PKCE 구현

PKCE를 구현하려면 다음 단계를 따라야 합니다:

  1. 코드 인증(code_verifier) 생성: 최소 43자 이상의 무작위 문자열
  2. 코드 챌린지(code_challenge) 생성: code_verifier를 SHA-256으로 해싱하고 Base64-URL로 인코딩
  3. 인증 요청 시 code_challenge 및 code_challenge_method 파라미터 포함
  4. 토큰 요청 시 code_verifier 파라미터 포함

토큰 검증

ID 토큰 검증 시 다음 사항을 확인해야 합니다:

  1. 서명 검증 (JWK 세트 엔드포인트에서 공개 키 조회)
  2. 발급자(iss) 검증
  3. 대상(aud) 검증
  4. 만료 시간(exp) 검증
  5. 발급 시간(iat) 검증
  6. nonce 검증

구현 예시

인증 요청 URL 생성

function generateAuthUrl(state: string, nonce: string): string {
const params = new URLSearchParams({
client_id: 'YOUR_CLIENT_ID',
redirect_uri: 'YOUR_REDIRECT_URI',
response_type: 'code',
scope: 'openid profile email',
state,
nonce,
prompt: 'consent'
});

return `https://gesundheitsid.example.de/auth?${params.toString()}`;
}

토큰 교환

async function exchangeCodeForTokens(code: string, state: string, nonce: string): Promise<TokenSet> {
const response = await fetch('https://gesundheitsid.example.de/token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Authorization': `Basic ${btoa(`${CLIENT_ID}:${CLIENT_SECRET}`)}`
},
body: new URLSearchParams({
grant_type: 'authorization_code',
code,
redirect_uri: 'YOUR_REDIRECT_URI'
}).toString()
});

if (!response.ok) {
throw new Error('Token exchange failed');
}

const tokenSet = await response.json();

// ID 토큰 검증
validateIdToken(tokenSet.id_token, nonce);

return tokenSet;
}

ID 토큰 검증

async function validateIdToken(idToken: string, expectedNonce: string): Promise<void> {
// JWK 세트 가져오기
const jwksResponse = await fetch('https://gesundheitsid.example.de/jwks');
const jwks = await jwksResponse.json();

// 토큰 디코딩 및 검증
const decodedToken = jwt.decode(idToken, { complete: true });
const kid = decodedToken.header.kid;

// 키 찾기
const key = jwks.keys.find(k => k.kid === kid);
if (!key) {
throw new Error('Signing key not found');
}

// 서명 검증
const isValid = jwt.verify(idToken, key, {
algorithms: ['RS256'],
audience: CLIENT_ID,
issuer: 'https://gesundheitsid.example.de'
});

if (!isValid) {
throw new Error('ID token validation failed');
}

const payload = jwt.decode(idToken);

// nonce 검증
if (payload.nonce !== expectedNonce) {
throw new Error('Invalid nonce');
}

// 만료 시간 검증
if (Date.now() / 1000 > payload.exp) {
throw new Error('Token expired');
}
}