학습 목표
OpenID Connect(OIDC) 가 OAuth 2.0 위 identity layer임을 설명할 수 있다.
ID token과 access token의 목적·검증 차이를 구분할 수 있다.
Authorization Code + PKCE로 OIDC 로그인 흐름을 단계별로 설명할 수 있다.
nonce·iss·aud·exp 검증과 UserInfo endpoint 역할을 설명할 수 있다.
문제 상황
- OAuth access token만 받고
sub를 로그인 ID로 썼다- access token은 API 위임용 — 신원 증명 표준이 아님
- Google "로그인" 후 이메일을 UserInfo 없이 access token payload에서 읽었다
- provider마다 형식이 다르고 검증 규칙도 없다
- ID token 서명 검증 없이 Base64 디코딩만 했다
- JWT를 Base64 디코딩만 했다 — 위조·algorithm confusion 위험 (앞서 JWT)
- SPA가 ID token을 localStorage에 저장했다
- XSS 시 로그인 세션 탈취
- Authorization 요청에 nonce가 없어 replay를 막지 못했다
- 동일 ID token 재전송 공격
OAuth 2.0에서 위임·PKCE를 봤다. OIDC는 같은 흐름에 "누가 로그인했는가" 를 표준 JWT(ID token)로 추가한다.
본 글의 흐름·예제는 방어·구현 학습 목적이다. 타인 계정·토큰에 대한 무단 접근은 불법이다.
1. OpenID Connect란
OpenID Connect — OAuth 2.0 위에 올린 인증(authentication) 프로토콜.
| OAuth 2.0 | OpenID Connect | |
|---|---|---|
| 목적 | 접근 위임 — API scope | 신원 확인 — 로그인 |
| 핵심 토큰 | access token | ID token (+ access token) |
| "누구" | Resource Owner 동의로 간접 | ID token claims로 표준화 |
| discovery | (없음) | /.well-known/openid-configuration |

- Relying Party(RP) — 로그인을 받는 앱 (Client와 동일 주체)
- OpenID Provider(OP) — Google, Keycloak, Auth0 등 — Authorization Server + identity
- OIDC는 OAuth를 대체하지 않음 — openid scope를 추가한 확장
2. ID token vs access token

| ID token | access token | |
|---|---|---|
| 형식 | JWT (JWS) — signed | JWT 또는 opaque |
| 용도 | 인증 — "이 사용자가 로그인함" | 인가 — API 호출 |
| 검증 | RP가 서명·iss·aud·exp·nonce 검증 | Resource Server가 scope·exp 검증 |
| 전달 | RP 백엔드에서 검증 후 세션 생성 | Authorization: Bearer |
| API 호출 | UserInfo 등 일부 endpoint | Resource Server 일반 API |
{
"iss": "https://accounts.example.com",
"sub": "user-8f3a",
"aud": "shop-web-client",
"exp": 1719003600,
"iat": 1719000000,
"nonce": "n-0d8f2c1a",
"email": "user@example.com",
"email_verified": true
}
- sub — OP 내 사용자 고유 ID (앱 간 안정적 식별자)
- nonce — authorize 요청의 nonce와 일치해야 함 (replay 방지)
- email 등은 scope·OP 정책에 따라 ID token 또는 UserInfo에만 올 수 있음
access token으로 "로그인" 판단하지 말 것 — OIDC는 ID token 또는 UserInfo + access token 조합을 쓴다.
예시 claims는 code/oidc-id-token-claims.json 참고.
3. Discovery와 JWKS
OP 메타데이터로 endpoint·키를 자동 발견.
GET https://accounts.example.com/.well-known/openid-configuration
| 필드 | 용도 |
|---|---|
authorization_endpoint |
/authorize |
token_endpoint |
code → token |
userinfo_endpoint |
access token으로 프로필 |
jwks_uri |
ID token 서명 검증용 공개키 |
GET https://accounts.example.com/.well-known/jwks.json
- JWT RS256이면 JWKS에서
kid에 맞는 키 선택 (앞서 JWT) - iss는 discovery URL의 issuer와 exact match
- 키 로테이션 — JWKS 캐시 TTL·실패 시 재fetch
4. Authorization Code + PKCE (로그인)
웹·SPA·모바일 소셜 로그인 표준 — 앞서 본 PKCE에 OIDC 파라미터 추가.

authorize (브라우저)
GET /authorize?
response_type=code
&client_id=shop-web
&redirect_uri=https://shop.example/callback
&scope=openid profile email
&state=RANDOM_CSRF
&nonce=RANDOM_NONCE
&code_challenge=E9Mel...
&code_challenge_method=S256
| 파라미터 | 의미 |
|---|---|
| scope=openid | OIDC 요청 — 필수 |
| profile email | 선택 claims / UserInfo scope |
| state | CSRF 방지 (OAuth와 동일) |
| nonce | ID token replay 방지 — 필수 권장 |
token (백채널)
POST /token
grant_type=authorization_code
&code=AUTH_CODE
&redirect_uri=https://shop.example/callback
&client_id=shop-web
&code_verifier=ORIGINAL_VERIFIER
응답:
{
"access_token": "eyJhbG...",
"token_type": "Bearer",
"expires_in": 3600,
"id_token": "eyJhbG...",
"refresh_token": "..."
}
RP 처리 순서
- state 검증
- ID token — 서명,
exp,iss,aud(client_id), nonce - (선택) UserInfo — access token으로 추가 claims
- RP 세션 생성 — HttpOnly cookie 등 (앞서 인증 기초)
- ID token은 브라우저 장기 저장 금지 — 검증 후 폐기 또는 BFF 내부만
전체 HTTP 예시는 code/oidc-requests.http 참고.
5. UserInfo endpoint
ID token만으로 부족한 claims — access token으로 OP에 질의.
GET /userinfo HTTP/1.1
Authorization: Bearer eyJhbG...
{
"sub": "user-8f3a",
"name": "Kim",
"email": "user@example.com",
"email_verified": true
}
| ID token | UserInfo | |
|---|---|---|
| 시점 | token 응답 즉시 | 필요 시 추가 호출 |
| 검증 | JWT 서명 + nonce | access token 유효 + HTTPS |
| 용도 | 로그인 핵심 신원 | 프로필·이메일 보강 |
- sub는 ID token과 UserInfo에서 동일해야 함 — 불일치면 거부
- access token scope에
profile·email없으면 필드 미반환
6. 보안 체크리스트
| 항목 | 내용 |
|---|---|
| scope | openid 포함 — 없으면 OAuth만 |
| nonce | authorize·ID token 매칭 |
| ID token 검증 | alg allowlist, JWKS, iss, aud, exp |
| access token | API·UserInfo만 — 세션 키로 쓰지 않음 |
| 저장 | ID token localStorage 금지 — RP 세션·BFF |
| HTTPS | authorize·token·UserInfo 전 구간 |
| logout | OP end_session_endpoint + RP 세션 삭제 |
| mix-up | 여러 OP 사용 시 iss·redirect_uri 엄격 분리 |
| 안티패턴 | 이유 |
|---|---|
| access token = login proof | 표준 아님, scope만 증명 |
| ID token을 API Bearer로 | audience·용도 불일치 |
| nonce 생략 | ID token replay |
| implicit flow (id_token fragment) | URL·히스토리 유출 — code flow 사용 |
7. 정리
- OIDC = OAuth 2.0 + identity layer — 소셜 로그인 표준
- ID token(JWT)으로 신원, access token으로 API·UserInfo
- Authorization Code + PKCE + openid + nonce가 웹·SPA 기본
- discovery·JWKS로 OP endpoint·키 자동화
- 검증 후 RP 세션 — ID token 장기 보관하지 않음
다음에 다룰 것
- 인가 모델
- RBAC, ABAC, policy engine
해당 내용은 OpenID Connect Core 1.0, RFC 6749 (OAuth 2.0), OWASP OAuth2 Cheat Sheet 의 내용을 기반으로 합니다.