OAuth 2.0 위에 "이 사용자가 누구인가" 를 표준화해 얹은 인증 규격 (OpenID Connect).
왜 필요했나
OAuth 2.0은 인가 프로토콜이다. Access Token은 "이 앱이 자원에 접근해도 된다"는 증표일 뿐, 누구인지를 담고 있지 않다.
그런데 실무에서는 "구글로 로그인"이 필요했다. 각 회사가 제각각 방식으로 사용자 정보를 얻어 쓰다 보니
페이스북: GET /me
- 구글: — GET /userinfo
- 카카오: — GET /v2/user/me
- 제공자마다 다른 구현. 표준이 없다
게다가 Access Token만으로 로그인을 처리하면 보안 문제가 있다
OIDC가 이것을 표준화했다.
무엇을 더했나 — ID Token
OAuth 2.0 → Access Token (자원 접근용)
OIDC → Access Token + ID Token (누구인지 증명)
ID Token은 서명된 JWT이며 사용자 정보를 담는다.
{
"iss": "https://accounts.google.com", // 발급자
"aud": "내앱의-client-id", // 대상 ★
"sub": "1234567890", // 사용자 고유 ID ★
"email": "user@example.com",
"email_verified": true,
"nonce": "무작위값", // 재생 공격 방지 ★
"exp": 1700000000,
"iat": 1699996400
}
Access Token으로 로그인하면 안 되는 이유
OAuth 항목에서 언급한 confused deputy 문제를 구체적으로 보자.
- ① 공격자가 자기 앱 "악성앱"을 만들고 피해자에게 구글 로그인을 시킨다
- ② 악성앱은 피해자의 Access Token을 얻는다
- ③ 그 토큰을 들고 우리 서비스에 "구글 로그인" 시도
- ④ 우리 서비스가 토큰으로 /userinfo 를 호출 → "피해자입니다" 응답
- ⑤ 피해자 계정으로 로그인 성공 ✗
Access Token에는 "누구를 위해 발급됐는지"가 없어서 생기는 문제다.
ID Token에는 aud(대상) 가 있다. 우리 서비스는 aud가 자기 client_id와
일치하는지 검증하므로, 다른 앱용 토큰은 거부된다.
반드시 검증해야 할 것들
- ① 서명 — — 제공자의 공개키(JWKS 엔드포인트)로 검증
- ② iss — — 기대한 발급자인가
- ③ aud — — 내 client_id 와 일치하는가 ★ 가장 중요
- ④ exp — — 만료되지 않았는가
- ⑤ nonce — — 내가 보낸 값과 같은가 (재생 공격 방지)
라이브러리를 쓰되 이 검증들이 실제로 켜져 있는지 확인해야 한다.
표준 엔드포인트
- /.well-known/openid-configuration ← 설정 자동 발견
- /authorize — ← 인증 요청
- /token — ← 토큰 교환
- /userinfo — ← 추가 사용자 정보
- /jwks — ← 서명 검증용 공개키
/.well-known/openid-configuration 덕분에 제공자를 바꿔도
설정을 하드코딩할 필요가 없다. 이 표준화가 OIDC의 실질적 가치다.
scope
- openid — ← 필수. 이게 있어야 OIDC로 동작한다
- profile — ← 이름, 사진 등
- email — ← 이메일
openid scope가 없으면 그냥 OAuth 2.0이고 ID Token이 안 나온다.
사용자 식별은 sub로
❌ 이메일로 사용자 식별
- 이메일은 바뀔 수 있다. 다른 사람에게 재할당될 수도 있다
✅ (iss, sub) 조합으로 식별
- sub는 제공자 내에서 영구 불변
- iss까지 봐야 하는 이유: 다른 제공자의 sub와 겹칠 수 있다
우리 DB에는 provider + provider_user_id 를 유니크 키로 두고,
이메일은 부가 정보로 저장한다.
면접 답변 골격
"OAuth 2.0은 인가, OIDC는 그 위의 인증 규격입니다.
소셜 로그인을 붙일 때 Access Token으로 사용자를 판단하면 aud 검증이
불가능해 다른 앱의 토큰으로 로그인당할 수 있어, ID Token을 검증해서
씁니다. 식별자는 이메일이 아니라 (iss, sub) 를 씁니다."