OpenID Connect Discovery 1.0 Deep Dive: OP의 ‘Self-Introduction’ 및 동적 구성 검색
Source: Dev.to
번역할 텍스트가 제공되지 않았습니다. 번역이 필요한 본문을 알려주시면 한국어로 번역해 드리겠습니다.
Source: …
소개
여러분은 아마 OIDC (OpenID Connect) 를 매일 사용해 Google 로그인이나 기타 인증 흐름을 애플리케이션에 통합하고 있을 것입니다.
그 과정에서 라이브러리 초기화 코드에
issuer: "https://accounts.google.com"
와 같이 Issuer URL만 지정하면 Authorization Endpoint, Token Endpoint, 그리고 공개 키(JWKS) 위치까지 자동으로 찾아주는 것을 경험해 본 적이 있나요?
- “Issuer URL만 제공하면 왜 모든 엔드포인트를 알 수 있는 걸까?”
- “공개 키(JWKS) 회전이 발생해도 다운타임 없이 어떻게 따라갈 수 있을까?”
- “처음부터
alice@example.com같은 이메일 형태의 ID로부터 인증할 제공자를 어떻게 식별하나요?”
이 질문들에 대한 답은 OpenID Connect Discovery 1.0 입니다.
과거 OAuth 2.0 시절에는 개발자들이 문서를 읽고 각 엔드포인트(URL)(예: /authorize, /token)를 클라이언트에 하드코딩하는 경우가 많았습니다.
이 방식은 제공자가 URL을 바꾸거나 공개 키를 회전할 때마다 클라이언트 측 코드를 수정해야 하므로 확장성이 떨어졌습니다.
OIDC Discovery 1.0 은 클라이언트가 OpenID Provider(OP)의 구성 정보(메타데이터)를 동적으로 발견하고 가져올 수 있게 하는 표준화된 메커니즘입니다.
이 글에서는 사양을 기반으로 OIDC Discovery의 두 단계에 대해 깊이 살펴보겠습니다.
| 단계 | 목표 |
|---|---|
| Issuer Discovery (Phase 1) | 사용자가 제공한 식별자(예: 이메일 주소)를 기반으로 누가 인증을 담당할 OpenID Provider(Issuer)인지 찾아냅니다. 이 단계는 Issuer가 이미 알려져 있는 경우(예: “Google로 로그인” 버튼 클릭) 생략될 수 있습니다. |
| Provider Configuration (Phase 2) | 식별된 Issuer에 대해 구성 메타데이터를 조회합니다 – “Authorization Endpoint는 어디인가?” “공개 키(JWKS)는 어디에 있나?” |
Issuer Discovery가 필요한 경우
앱이 Google 로그인 전용이라면 Issuer는 당연히 https://accounts.google.com 입니다.
하지만 사용자의 이메일 도메인(@company.com)에 따라 동적으로 IdP를 전환해야 하는 엔터프라이즈 SaaS를 생각해 보세요.
이때 RFC 7033 WebFinger 가 역할을 합니다.
사용자 제공 식별자 정규화
사용자가 입력한 값은 다음과 같을 수 있습니다:
- 이메일 주소 예:
alice@example.com - URL 예:
https://example.com/alice
OIDC Discovery는 고유하게 결정하기 위한 엄격한 정규화 단계를 정의합니다:
- Host – 연결할 서버
- Resource – WebFinger를 통해 조회할 식별자
정규화 규칙
| Condition | Normalized Form |
|---|---|
스킴 없음이며 @를 포함하는 경우 (예: joe@example.com) | acct: 스킴으로 해석 → acct:joe@example.com |
스킴 없음이며 @를 포함하지 않는 경우 (예: example.com 또는 example.com:8080) | https:// 로 해석 → https://example.com |
명시적 스킴 (https://, acct:, …) | 그대로 사용, 추가 변경 없음 |
프래그먼트 존재 (#…) | 프래그먼트를 완전히 제거 |
Source: …
Phase 1 – WebFinger를 이용한 Issuer Discovery
사용자가 joe@example.com을 입력했다고 가정하면, 이는 acct:joe@example.com으로 정규화됩니다.
- 호스트 식별 – 권한 부분(
example.com)을 추출합니다. - 리소스 지정 – 전체 정규화된 URI(
acct:joe@example.com)를resource쿼리 매개변수로 사용합니다. - WebFinger 엔드포인트 호출 – 추출한 호스트의
/.well-known/webfinger에 대해 HTTP GET 요청을 수행합니다.
GET /.well-known/webfinger?resource=acct%3Ajoe%40example.com&rel=http%3A%2F%2Fopenid.net%2Fspecs%2Fconnect%2F1.0%2Fissuer HTTP/1.1
Host: example.com
resource– 조회할 식별자를 URL‑인코딩한 값.rel– 고정 값http://openid.net/specs/connect/1.0/issuer이며, “OIDC Issuer 정보를 요청한다”는 의미입니다.
예시 응답 (JRD – JSON Resource Descriptor)
HTTP/1.1 200 OK
Content-Type: application/jrd+json
{
"subject": "acct:joe@example.com",
"links": [
{
"rel": "http://openid.net/specs/connect/1.0/issuer",
"href": "https://server.example.com"
}
]
}
href 값(https://server.example.com)이 Issuer URL이며, 클라이언트는 다음 단계에서 이 URL을 사용합니다.
Phase 2 – 제공자 구성
Issuer가 알려지면, 클라이언트는 OP의 메타데이터를 가져옵니다.
OIDC Discovery는 OP가 JSON 메타데이터를 Issuer URL에 /.well-known/openid-configuration을 추가한 경로에 노출하도록 요구합니다.
GET /.well-known/openid-configuration HTTP/1.1
Host: server.example.com
⚠️ 일반적인 함정 – Issuer에 경로가 포함된 경우
Issuer에 경로가 포함되어 있는 경우(예: https://example.com/tenant-1):
- 끝에 있는 슬래시를 제거합니다.
- 경로 바로 뒤에
/.well-known/openid-configuration을 추가합니다.
결과 URL:
https://example.com/tenant-1/.well-known/openid-configuration
많은 구현에서 발생하듯이 구성 파일을 도메인 루트(https://example.com/.well-known/openid-configuration)에 잘못 배치하지 않도록 주의하세요.
OP 메타데이터 예시 응답
HTTP/1.1 200 OK
Content-Type: application/json
{
"issuer": "https://server.example.com",
"authorization_endpoint": "https://server.example.com/oauth2/v1/authorize",
"token_endpoint": "https://server.example.com/oauth2/v1/token",
"userinfo_endpoint": "https://server.example.com/oauth2/v1/userinfo",
"jwks_uri": "https://server.example.com/oauth2/v1/keys",
"registration_endpoint": "https://server.example.com/oauth2/v1/register",
"scopes_supported": ["openid", "profile", "email"],
"response_types_supported": ["code", "id_token", "token id_token"],
"grant_types_supported": ["authorization_code", "refresh_token", "client_credentials"],
"subject_types_supported": ["public", "pairwise"],
"id_token_signing_alg_values_supported": ["RS256"],
"claims_supported": ["sub", "iss", "aud", "exp", "iat", "email", "email_verified", "name"]
}
JSON 객체(OP 메타데이터)는 다음을 나열합니다:
- 엔드포인트 (
authorization_endpoint,token_endpoint,userinfo_endpoint,jwks_uri, …) - 지원되는 기능 (
scopes_supported,grant_types_supported,claims_supported, 등)
이 메타데이터를 사용하면 클라이언트 라이브러리가 자동으로 다음을 수행할 수 있습니다:
- 사용자를 올바른 Authorization Endpoint(인증 엔드포인트)로 안내합니다
- 코드를 적절한 Token Endpoint(토큰 엔드포인트)에서 교환합니다
- JWKS URI에서 공개 키를 가져옵니다(키 회전을 투명하게 처리)
요약
- Issuer Discovery (optional) – WebFinger를 사용하여 사용자 제공 식별자(이메일, URL 등)를 Issuer URL로 변환합니다.
- Provider Configuration – Issuer의
/.well-known/openid-configuration문서를 가져와 모든 필수 엔드포인트와 기능을 확인합니다.
이 두 단계 디스커버리 프로세스는 하드코딩을 없애고, 멀티‑테넌트 시나리오를 지원하며, 다운타임 없이 원활한 키 회전을 보장합니다.
OpenID Connect Provider Configuration (Discovery)
{
"authorization_endpoint": "https://server.example.com/connect/authorize",
"token_endpoint": "https://server.example.com/connect/token",
"userinfo_endpoint": "https://server.example.com/connect/userinfo",
"jwks_uri": "https://server.example.com/jwks.json",
"response_types_supported": [
"code",
"id_token",
"id_token token"
],
"subject_types_supported": [
"public",
"pairwise"
],
"id_token_signing_alg_values_supported": [
"RS256",
"ES256"
],
"token_endpoint_auth_methods_supported": [
"client_secret_basic",
"private_key_jwt"
],
"scopes_supported": [
"openid",
"profile",
"email"
],
"claims_supported": [
"sub",
"iss",
"name",
"email"
],
"registration_endpoint": "https://server.example.com/connect/register"
}
매개변수 개요
| 매개변수 이름 | 필수 / 선택 | 설명 |
|---|---|---|
| issuer | REQUIRED | OP의 Issuer Identifier. TLS 검사와 ID 토큰의 iss 클레임 검증에 사용됩니다. |
| authorization_endpoint | REQUIRED | 사용자가 인증을 위해 리디렉션되는 엔드포인트입니다. |
| token_endpoint | REQUIRED* | 인증 코드를 토큰으로 교환하는 데 사용되는 엔드포인트입니다. Implicit Flow만 지원하는 OP를 제외합니다. |
| jwks_uri | REQUIRED | ID 토큰 서명을 검증하기 위한 공개 키(JWK Set)가 위치한 URL입니다. |
| response_types_supported | REQUIRED | OP가 지원하는 OIDC 인증 흐름(예: code, id_token, id_token token)입니다. |
| subject_types_supported | REQUIRED | 지원되는 sub 식별자 유형 – public(모든 RP에 동일) 또는 pairwise(RP마다 고유)입니다. |
| id_token_signing_alg_values_supported | REQUIRED | OP가 ID 토큰에 사용할 수 있는 서명 알고리즘입니다. RS256은 반드시 포함되어야 합니다. |
| token_endpoint_auth_methods_supported | OPTIONAL | 토큰 엔드포인트에서 허용되는 클라이언트 인증 방법(예: client_secret_basic, private_key_jwt)입니다. |
| scopes_supported | RECOMMENDED | OP가 지원하는 스코프 목록입니다. openid 스코프는 SHOULD 포함되어야 합니다. |
| claims_supported | RECOMMENDED | OP가 반환할 수 있는 클레임 목록(예: name, email, sub, iss)입니다. |
| registration_endpoint | RECOMMENDED | 동적 클라이언트 등록을 위한 엔드포인트입니다. |
Source: …
Deep‑Dive: jwks_uri
- 왜 중요한가 –
jwks_uri는 OP(인증 제공자)의 공개 키에 대한 단일 진실 소스입니다. 이 URL을 가져옴으로써 신뢰당사자(RP)는 수신하는 모든 ID Token의 서명을 검증할 수 있습니다. - 키 회전 – RP는 들어오는 ID Token의
kid(Key ID) 헤더를 확인해야 합니다. 해당 키가 로컬 캐시에서 누락된 경우, RP는jwks_uri에서 JWKS를 다시 가져와야 합니다. 이를 통해 다운타임 없이 원활한 키 회전이 가능합니다. - 보안 고려 사항 –
-
위장 공격: 공격자가 RP가 악성 JWKS를 다운로드하도록 만들면 위조된 ID Token이 가능해집니다.
-
Discovery 사양은 엄격한 검사를 강제합니다:
“반환된
issuer값은/.well-known/openid-configuration앞에 사용된 Issuer URL과 동일해야 합니다.” (OIDC Discovery §4.3)“구현은 TLS를 지원해야 합니다.” (OIDC Discovery §7.1)
-
모든 통신(WebFinger 및 Provider Configuration)은 HTTPS를 통해 이루어져야 하며, RP는 서버의 TLS 인증서(RFC 6125)를 검증해야 합니다. 평문 전송은 MITM이
jwks_uri를 공격자가 제어하는 서버로 바꾸는 것을 허용하게 됩니다.
-
OIDC Discovery 1.0의 핵심 정리
- 하드코딩된 URL 금지 –
/.well-known/openid-configuration을 사용하면 RP가 런타임에 OP의 엔드포인트와 기능을 발견할 수 있습니다. - WebFinger 통합 – 사용자의 이메일과 같은 식별자를 올바른 Issuer로 해석할 수 있습니다(Discovery Phase 1).
- 자동 키 회전 – JWKS를 동적으로 가져오면 견고하고 원활한 보안 운영이 가능합니다.
다음에 OIDC 라이브러리를 사용할 때, 위 모든 기능을 구동하는 숨은 요청
/.well-known/openid-configuration을 떠올려 보세요.
References
- OpenID Connect Discovery 1.0 – 공급자 메타데이터에 대한 핵심 사양.
- RFC 7033 – WebFinger – 리소스(예: 이메일 주소)에 대한 정보를 발견하는 메커니즘.
- RFC 5785 – Well‑Known URIs – 발견 엔드포인트에 사용되는
/.well-known/네임스페이스를 정의합니다.