이 문서를 통해 아래 질문들에 답할 수 있게 됩니다.
- ID Token과 access token은 왜 서로 다른 목적으로 검증해야 하는가?
- OIDC 로그인에서 issuer, audience, signature, expiry, nonce, subject는 각각 무엇을 확인하는가?
- UserInfo 응답을 내부 계정과 연결할 때 어떤 조건을 확인해야 하는가?
개요
OIDC는 OAuth2 위에 사용자 인증 결과를 전달하는 identity layer다. 핵심 산출물은 ID Token이며, 이 token은 Authorization Server가 사용자를 인증했다는 claims를 담는다.
ID Token은 Resource Server 호출용 access token이 아니다. ID Token은 client가 사용자 인증 결과를 확인하는 데 쓰고, access token은 Resource Server에 접근할 권한을 나타낸다.
UserInfo endpoint는 사용자의 profile claims를 가져오는 protected resource다. UserInfo 응답도 provider에서 온 속성일 뿐 내부 권한이 아니며, ID Token의 subject와 일치하는지 확인해야 계정 혼동을 줄일 수 있다.
이 문서의 핵심은 “provider가 사용자 정보를 줬다”가 아니라 “그 정보가 이 client를 위해 이 issuer가 발급한 검증된 인증 결과인가”다.
공격 시나리오
- 공격자는 다른 client에 발급된 ID Token을 가져와 audience 검증이 약한 애플리케이션에 제시한다.
- issuer 검증이 약하면 비슷한 provider나 테스트 issuer의 token을 받아들이게 만든다.
- nonce 검증이 없으면 이전 인증 응답의 ID Token을 재사용한다.
- email만 보고 내부 계정을 찾게 하면 같은 이메일 claim을 가진 다른 provider 계정을 연결하려 한다.
- UserInfo subject가 ID Token subject와 다른데도 profile 정보를 신뢰하게 만든다.
- expired token이나 서명 검증 실패 token을 캐시 때문에 통과시키려 한다.
취약한 요청/응답 예시
OAuth2User user = oauth2UserService.loadUser(userRequest);
String email = user.getAttribute("email");
Member member = memberRepository.findByEmail(email)
.orElseGet(() -> memberRepository.save(Member.social(email)));HTTP/1.1 302 Found
Location: /me
Set-Cookie: SESSION=created문제는 provider, issuer, subject, email_verified, ID Token 검증 결과 없이 email만 내부 계정 키로 사용한다는 점이다.
주제별 핵심 판단
- ID Token은
iss,sub,aud,exp,iat,nonce, signature를 검증해야 한다. iss는 token을 발급한 issuer가 기대한 provider인지 확인한다.aud는 token이 우리 client를 대상으로 발급되었는지 확인한다.nonce는 인증 요청과 ID Token을 묶어 replay를 줄인다.sub는 provider 내에서 사용자를 식별하는 안정 claim이다.- UserInfo의
sub는 ID Token의sub와 일치해야 한다. - email과 profile은 속성일 뿐 내부 권한 또는 내부 계정 소유 증명이 아니다.
개선된 요청/응답 예시
public record ExternalIdentity(
String provider,
String issuer,
String subject,
String email,
boolean emailVerified
) {
public String stableKey() {
return provider + ":" + subject;
}
}ExternalIdentity identity = externalIdentityFrom(oidcUser);
Member member = socialAccountService.loginOrRequireLink(identity);내부 계정 연결은 provider + subject를 기본 키로 삼고 이메일은 보조 속성으로 저장한다. 기존 이메일 계정과 자동 병합하지 않는다.
방어 설계
- OIDC provider 설정은 issuer URI 기준으로 등록한다.
- ID Token signature와 key rotation을 framework가 검증하는지 확인한다.
- issuer, audience, expiry, issued at, nonce 검증 실패를 로그인 실패로 처리한다.
- UserInfo를 호출한다면 ID Token subject와 UserInfo subject 일치를 확인한다.
- provider별 subject를 내부 외부 계정 테이블에 unique key로 저장한다.
- email은 verified 여부와 함께 저장하고 내부 식별자 단독 기준으로 쓰지 않는다.
- provider profile 변경은 내부 권한 변경으로 자동 반영하지 않는다.
- claim mapping은 provider별 차이를 명시적으로 다룬다.
Spring/HTTP 예시
@Service
public class SocialOidcUserService extends OidcUserService {
@Override
public OidcUser loadUser(OidcUserRequest userRequest) {
OidcUser oidcUser = super.loadUser(userRequest);
OidcIdToken idToken = oidcUser.getIdToken();
ExternalIdentity identity = new ExternalIdentity(
userRequest.getClientRegistration().getRegistrationId(),
idToken.getIssuer().toString(),
idToken.getSubject(),
oidcUser.getEmail(),
Boolean.TRUE.equals(oidcUser.getEmailVerified())
);
socialAccountAudit.recordLoginAttempt(identity);
return new DomainOidcUser(oidcUser, identity);
}
}@Bean
SecurityFilterChain security(HttpSecurity http, SocialOidcUserService oidcUserService) throws Exception {
return http
.oauth2Login(oauth -> oauth
.userInfoEndpoint(userInfo -> userInfo
.oidcUserService(oidcUserService)))
.build();
}Spring Security는 OIDC client 흐름의 표준 검증을 제공하지만, 내부 계정 연결 정책과 claim mapping은 애플리케이션이 명확히 정의해야 한다.
운영 로그와 감사 지점
level=INFO event=oidc_login_succeeded result=allowed provider=google issuer=https://accounts.google.com subjectHash=812f emailHash=0df1 emailVerified=true memberId=1004 traceId=2bb49c71level=WARN event=oidc_claim_validation_failed result=blocked provider=google reason=audience_mismatch issuer=https://accounts.google.com subjectHash=812f traceId=2bb49c71- ID Token 원문은 로그에 남기지 않는다.
- subject와 email은 원문보다 hash 또는 masked value를 남긴다.
- claim 검증 실패 reason을 issuer, audience, expiry, nonce, signature로 구분한다.
- UserInfo subject 불일치는 단순 profile 오류가 아니라 보안 이벤트로 다룬다.
- provider claim mapping 변경은 배포 이벤트와 연결해 추적한다.
실전 판단 기준
- ID Token을 Resource Server API 호출용 bearer token으로 보내면 목적이 틀렸다.
- access token만 받아 UserInfo를 호출하고 email만 믿는 로그인은 provider별 위험을 따져야 한다.
- email_verified가 false이거나 없는 provider는 기존 이메일 계정과 자동 연결하면 안 된다.
- issuer와 audience 검증이 꺼져 있으면 token substitution에 취약하다.
- nonce 검증이 없으면 오래된 인증 응답 재사용을 탐지하기 어렵다.
- provider subject를 저장하지 않으면 나중에 계정 연결 문제를 복원하기 어렵다.
테스트 포인트
- 다른 client_id 대상으로 발급된 ID Token이 거부되는지 확인한다.
- 다른 issuer의 ID Token이 거부되는지 확인한다.
- expired token과 잘못된 signature token이 거부되는지 확인한다.
- nonce 불일치 또는 누락이 거부되는지 확인한다.
- UserInfo subject가 ID Token subject와 다를 때 거부되는지 확인한다.
- 같은 이메일을 가진 다른 provider 계정이 기존 내부 계정에 자동 연결되지 않는지 확인한다.
위험 신호!
- 내부 사용자 식별 키가 email 하나뿐이다.
- provider subject를 저장하지 않는다.
- ID Token 검증 실패가 일반 로그인 실패와 구분되지 않는다.
- UserInfo 응답을 내부 권한이나 role로 바로 mapping한다.
- email_verified를 보지 않고 기존 이메일 계정과 자동 연결한다.
- ID Token 원문이 debug log에 남는다.
확인 질문
확인 질문
- ID Token은 무엇을 증명하는가?
- 특정 issuer가 특정 client를 대상으로 사용자를 인증했다는 claims를 담은 token이다.
- UserInfo 응답을 받을 때 중요한 subject 검증은 무엇인가?
- UserInfo의
sub가 ID Token의sub와 일치해야 한다.- 내부 계정 연결의 안정 키는 무엇이어야 하는가?
- provider와 provider subject의 조합이다. 이메일은 보조 속성으로 다룬다.