OIDC : OAuth 2.0 기반 인증(Authentication) 표준 프로토콜

기본 OAuth는 인가만 처리할 뿐, 인증 처리에 최적화 되어있지 않습니다. OIDC는 OAuth로 인증을 처리할 수 있게 만들어주는 표준 프로토콜입니다.

지난 글에서 기본적인 OAuth를 다뤘습니다. OAuth는 내 애플리케이션이 직접 로그인을 처리하지 않고도 구글, 카카오 같은 IdP의 데이터를 가져올 수 있게 해 주는 방법이었죠. 소셜 로그인을 예시로 들어 설명했었습니다.

그런데 엄밀히 따지면 OAuth만으로는 로그인을 처리할 수 없습니다. OAuth Flow의 최종 결과물인 Access Token은 Resource Server에서 데이터를 가져갈 권한을 증명하는 값이지, 사용자가 내 앱에 로그인했다는 것을 증명하는 값이 아니기 때문입니다.

OIDC는 이 문제를 해결하기 위해 OAuth 위에 인증을 얹은 표준 프로토콜입니다. 이 글에서는 OIDC에 대해 알아보겠습니다.

인증(Authentication), 인가(Authorization)

  • 인증 : "너 누구야?"
  • 인가 : "너 이거 해도 돼?"

OAuth는 인가를 위한 표준입니다. 그렇다고 Access Token으로 사용자가 누구인지 전혀 알 수 없는 건 아닙니다. 이 토큰으로 IdP의 프로필 API를 호출하면 사용자 정보를 받아올 수 있고, 실제로 예전에는 이렇게 로그인을 구현하기도 했습니다.

IdP에 따라 Access Token 안에 이메일 같은 사용자 정보가 들어 있기도 합니다. 하지만 그 값은 Resource Server가 읽으라고 넣은 것이고, 표준(RFC 9068)도 앱이 Access Token의 내용을 열어 보지 말라고 정해 두었습니다.

문제는 이것만으로는 "이 사용자가 내 앱에 로그인했다"는 증명이 되지 않는다는 점입니다. 내 앱은 이 토큰이 어떤 앱을 위해 발급됐는지 확인할 방법이 없기 때문입니다. 앱 두 개로 예를 들어 보겠습니다.

  • 내 앱 A: 모바일 앱입니다. 앱이 구글에서 받은 Access Token을 서버로 보내면, 서버가 그 토큰으로 프로필 API를 호출해 로그인시킵니다.
  • 운세 앱 B: 공격자가 만든 앱입니다. B에도 구글 로그인이 붙어 있습니다.
  1. 철수가 운세 앱 B에서 구글로 로그인합니다. B는 철수의 Access Token을 손에 넣습니다.
  2. 공격자는 이 토큰을 A 서버의 로그인 API에 그대로 보냅니다.
  3. A 서버가 그 토큰으로 프로필 API를 호출하면, 구글은 "이 사람은 철수입니다"라고 정상 응답합니다. 구글이 발급한 진짜 토큰이니까요.
  4. 공격자는 A에 철수로 로그인합니다.

A는 사용자가 철수라는 건 정확히 알아냈습니다. 하지만 그 토큰이 B를 위해 발급된 것이라는 건 알 수 없었습니다.

지난 글의 인가 코드 방식처럼 A의 서버가 직접 인가 코드를 토큰으로 바꾸는 구조라면, 공격자가 토큰을 끼워 넣기 어렵습니다. 이 공격은 외부에서 받은 Access Token을 그대로 믿는 구조에서 생깁니다.

정리하면, 내 앱이 로그인을 안전하게 처리하려면 IdP로부터 최소한 이 세 가지를 확인받아야 합니다.

  1. 로그인한 사용자가 누구인지
  2. 그 결과가 내 앱을 위해 발급된 것인지
  3. 그 결과를 정말 그 IdP가 발급했는지

OAuth에는 이걸 확인하는 표준 방법이 없습니다. OIDC는 바로 이 자리를 채웁니다.

OIDC란?

OIDC 스펙은 OIDC를 이렇게 정의합니다.

"OpenID Connect 1.0 is a simple identity layer on top of the OAuth 2.0 protocol." OAuth 2.0 위에 얹은 단순한 신원(identity) 계층이라는 뜻입니다.

"OAuth 위에 얹었다"는 말 그대로, OIDC는 OAuth의 인가 코드 흐름을 그대로 씁니다. 대신 흐름의 마지막에 IdP가 Access Token과 함께 ID Token이라는 토큰을 하나 더 발급합니다.

ID Token은 "IdP가 이 사용자를 인증했고, 이 결과는 이 앱을 위한 것이다"라는 내용을 담은 토큰입니다. 앞에서 정리한 세 가지와 대응시켜 보면 이렇습니다.

확인해야 할 것ID Token에서 확인하는 방법
사용자가 누구인지sub 값 (사용자 식별자)
내 앱을 위해 발급됐는지aud 값에 내 앱의 client_id가 들어 있는지
정말 그 IdP가 발급했는지iss 값과 서명

각 값은 뒤에서 자세히 보겠습니다. 여기서는 Access Token과 ID Token이 서로 다른 목적의 토큰이라는 점만 짚고 넘어가겠습니다.

Access TokenID Token
무엇을 증명하나데이터를 가져갈 권한사용자가 인증했다는 사실
누가 읽나Resource Server내 앱
형식정해져 있지 않음(IdP마다 다름)JWT로 정해져 있음

ID Token은 내 앱이 직접 읽으라고 만든 토큰이고, 형식도 OIDC가 정해 두었습니다. 그래서 구글이든 카카오든 같은 방식으로 읽고 검증할 수 있습니다.

OIDC에서 쓰는 이름

OIDC 문서나 라이브러리 설정을 보면 지난 글과 다른 이름이 나옵니다. 역할이 새로 생긴 건 아니고, 같은 역할을 OIDC 관점에서 부르는 이름입니다.

지난 글(OAuth)OIDC이 글에서
Authorization ServerOpenID Provider (OP)IdP
ClientRelying Party (RP)내 앱(클라이언트 서버)

스펙은 OP를 "사용자를 인증할 수 있는 OAuth 2.0 Authorization Server"로, RP를 "OP에게 사용자 인증과 사용자 정보를 요청하는 OAuth 2.0 Client"로 정의합니다.

OIDC가 OAuth에 더하는 것

OIDC가 OAuth 흐름에 더하는 것은 다음과 같습니다.

  • scope=openid: 이 요청이 OIDC 요청이라는 표시
  • nonce: ID Token이 이번 로그인 요청에 대한 것인지 확인하는 값
  • ID Token: 로그인 결과를 담은 토큰
  • UserInfo 엔드포인트: 사용자 정보를 표준 형식으로 돌려주는 엔드포인트
  • Discovery 문서: IdP의 엔드포인트 주소와 설정을 한곳에 모아 둔 문서

이제 로그인 흐름을 따라가면서 각각이 어디서 어떻게 쓰이는지 보겠습니다.

OIDC 로그인 흐름

sequenceDiagram
    participant B as 브라우저
    participant CS as 클라이언트 서버
    participant AS as IdP

    B->>CS: 1. GET /login
    rect rgba(255, 190, 0, 0.18)
    Note over CS: state, code_verifier, [추가] nonce 생성 후 세션에 저장<br/>code_challenge = SHA-256(code_verifier)
    CS-->>B: 2. 302 Redirect<br/>Location: IdP /authorize?client_id&redirect_uri<br/>&state&code_challenge&[추가] scope=openid&[추가] nonce<br/>Set-Cookie: 세션 ID
    B->>AS: 3. GET /authorize?... (Location 주소 그대로)
    end
    AS-->>B: 4. 로그인·동의 후 302 Redirect<br/>Location: redirect_uri?code&state
    B->>CS: 5. GET /callback?code&state<br/>Cookie: 세션 ID
    Note over CS: 세션의 state와 비교
    CS->>AS: 6. POST /token<br/>code, code_verifier, client_secret
    rect rgba(255, 190, 0, 0.18)
    AS-->>CS: 7. access_token + [추가] id_token
    Note over CS: [추가] id_token 검증 (세션의 nonce와 비교)<br/>→ iss + sub로 사용자 식별
    end

색칠된 구간이 OAuth와 달라지는 부분입니다.

1 ~ 3. 인가 요청 만들기: scope=openid와 nonce

사용자가 로그인 버튼을 누르면 브라우저가 클라이언트 서버의 /login으로 갑니다(1). 서버는 지난 글처럼 state와 code_verifier를 만들고, OIDC에서는 여기에 nonce를 하나 더 만듭니다. 세 값을 세션에 저장한 뒤, 이 값들을 쿼리 파라미터로 붙인 /authorize 주소를 만들어 브라우저를 그리로 리다이렉트시킵니다(2).

// GET /login
const state = crypto.randomBytes(16).toString('base64url');
const nonce = crypto.randomBytes(16).toString('base64url');
const codeVerifier = crypto.randomBytes(32).toString('base64url');
const codeChallenge = crypto.createHash('sha256').update(codeVerifier).digest('base64url');

// 콜백에서 꺼내 쓸 값은 세션에 저장
req.session.oauth = { state, nonce, codeVerifier };

const url = new URL('https://auth.example.com/authorize');
url.search = new URLSearchParams({
  response_type: 'code',
  client_id: 'my-app',
  redirect_uri: 'https://myapp.com/callback',
  scope: 'openid',
  state,
  nonce,
  code_challenge: codeChallenge,
  code_challenge_method: 'S256',
}).toString();

res.redirect(302, url.toString());

세션은 쿠키로 브라우저와 묶여 있습니다. 그래서 브라우저가 IdP를 거쳐 콜백으로 돌아올 때 같은 세션 쿠키를 들고 오고, 서버는 여기서 저장해 둔 값을 다시 꺼낼 수 있습니다.

브라우저는 리다이렉트된 주소로 그대로 이동합니다(3).

GET https://auth.example.com/authorize
  ?response_type=code
  &client_id=my-app
  &redirect_uri=https://myapp.com/callback
  &scope=openid
  &state=af0ifjsldkj
  &nonce=n-0S6_WzA2Mj
  &code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
  &code_challenge_method=S256

지난 글과 비교하면 두 값이 달라졌습니다.

scope=openid

지난 글에서 scope는 calendar.readonly처럼 가져올 데이터의 권한 범위였습니다. OIDC에서는 여기에 openid를 넣습니다. 스펙상 OIDC 요청에는 openid가 반드시 들어가야 하고, 없으면 IdP가 어떻게 동작할지 정해져 있지 않습니다. 즉 openid가 있어야 IdP가 이 요청을 OIDC 로그인 요청으로 처리하고 ID Token을 내려 줍니다.

이름이나 이메일 같은 사용자 정보도 필요하다면 scope=openid profile email처럼 함께 요청할 수 있습니다. profile은 이름, 프로필 사진 같은 기본 프로필 정보를, email은 이메일과 이메일 인증 여부를 요청합니다. 이 정보를 받는 곳은 뒤에서 볼 UserInfo 엔드포인트입니다.

nonce

nonce '이번 한 번만'이라는 뜻의 영어 단어입니다. "number used once"의 줄임말로 많이 알려져 있지만 실제 어원은 아닙니다.

nonce는 state와 똑같이 무작위로 만들어 세션에 저장하지만, 쓰임새가 다릅니다.

  • state는 IdP가 콜백 URL에 그대로 붙여 돌려줍니다. 콜백 요청이 내가 보낸 요청의 응답인지 확인하는 값입니다.
  • nonce는 IdP가 ID Token 안에 그대로 넣어 줍니다. ID Token이 이번 로그인 요청에 대해 발급된 것인지 확인하는 값입니다.

스펙은 nonce의 목적을 "클라이언트 세션과 ID Token을 연결하고, 재전송 공격(replay attack)을 막는 것"이라고 설명합니다. 실제로 어떤 공격을 막는지는 검증 파트에서 보겠습니다.

인가 코드 방식에서 nonce는 스펙상 선택 사항입니다. 다만 보냈다면 반드시 검증해야 하고, 많은 라이브러리가 기본으로 사용합니다.

4 ~ 5. 로그인·동의, 콜백

이 구간은 지난 글과 같습니다. IdP가 로그인·동의 화면을 보여 주고, 사용자가 동의하면 브라우저를 redirect_uri로 돌려보냅니다(4). 브라우저가 세션 쿠키와 함께 /callback으로 오면(5), 서버는 돌아온 state가 세션에 저장해 둔 값과 같은지 확인합니다.

6 ~ 7. 토큰 교환: ID Token 받기

서버가 인가 코드와 code_verifier를 들고 /token에 요청하는 것(6)도 지난 글과 같습니다. 달라지는 건 응답입니다. scope에 openid를 넣었기 때문에, 응답에 id_token이 함께 옵니다(7).

{
  "access_token": "eyJhbGciOi...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "id_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6ImtleS0xIn0.eyJpc3MiOi..."
}

이제 앱의 손에 ID Token이 들어왔습니다. 하지만 받았다고 바로 믿으면 안 됩니다. 먼저 안에 무엇이 들어 있는지 보고, 그다음 그 내용을 검증해야 합니다.

ID Token 들여다보기

ID Token은 JWT입니다. 앱이 내용을 직접 읽어야 하고, 동시에 그 내용이 위조되지 않았는지도 앱이 직접 확인해야 하기 때문에, 내용(Payload)과 서명(Signature)을 함께 담는 JWT 형식을 씁니다.

위의 id_token을 디코딩하면 이렇게 생겼습니다.

// Header
{
  "alg": "RS256",
  "kid": "key-1"
}

// Payload
{
  "iss": "https://auth.example.com",
  "sub": "248289761001",
  "aud": "my-app clientID",
  "exp": 1792226400,
  "iat": 1792222800,
  "nonce": "n-0S6_WzA2Mj"
}

Payload의 각 항목을 **클레임(Claim)**이라고 부릅니다. 스펙의 정의는 "어떤 대상에 대해 주장된(asserted) 정보 한 조각"입니다. "이 사용자는 248289761001번이다", "이 토큰은 my-app을 위해 발급됐다" 같은 주장 하나하나가 클레임입니다.

ID Token의 핵심 클레임은 다음과 같습니다. 각 클레임이 어떤 질문에 답하는지 함께 보면 이해가 쉽습니다.

클레임풀네임답하는 질문의미
ississuer누가 발급했나?이 토큰을 만든 IdP의 주소
subsubject누가 로그인했나?사용자 식별자. IdP 안에서 고유하고, 다른 사용자에게 재할당되지 않습니다.
audaudience누구를 위해 발급했나?이 토큰을 받을 앱. 내 앱의 client_id가 들어 있어야 합니다.
expexpiration언제까지 유효한가?만료 시각
iatissued at언제 발급했나?발급 시각
nonce-어느 로그인 요청에 대한 것인가?/authorize 요청 때 보낸 nonce

exp, iat은 Unix 시간(1970년 1월 1일부터 흐른 초)입니다.

Header에도 볼 값이 있습니다. alg는 서명에 쓴 알고리즘이고, kid(Key ID)는 IdP가 어떤 키로 서명했는지 알려 주는 값입니다. 둘 다 서명을 검증할 때 씁니다.

ID Token 검증

이제 다이어그램의 마지막 단계, ID Token 검증입니다. 검증은 앞에서 정리한 "확인해야 할 것"을 실제로 확인하는 단계입니다. 스펙이 정한 순서를 따라 하나씩 보겠습니다.

1. iss: 기대하는 IdP가 발급했다고 적혀 있나?

iss가 내가 로그인을 요청한 IdP의 주소와 정확히 같아야 합니다. 이 주소는 뒤에서 볼 Discovery 문서의 issuer 값입니다.

다만 iss는 Payload에 적힌 글자일 뿐이라 얼마든지 지어낼 수 있습니다. 그래서 3번의 서명 검증과 함께해야 의미가 있습니다.

2. aud: 내 앱을 위해 발급됐나?

aud에 내 앱의 client_id가 들어 있어야 합니다.

앞에서 본 운세 앱 B를 막는 게 바로 이 단계입니다. B가 받은 ID Token의 aud에는 B의 client_id가 들어 있습니다. 공격자가 이 ID Token을 A에 들고 와도, A는 aud를 보고 "나를 위해 발급된 토큰이 아니다"라며 거부할 수 있습니다. Access Token으로는 할 수 없었던 확인입니다.

3. 서명: 정말 그 IdP가 만들었나?

Payload는 누구나 디코딩할 수 있는 만큼, 누구나 그럴듯한 Payload를 만들 수도 있습니다. 그래서 서명으로 이 토큰을 정말 그 IdP가 만들었는지, 중간에 바뀌지 않았는지 확인합니다.

IdP는 개인키로 서명하고, 앱은 IdP의 공개키로 검증합니다. OIDC에서 알아야 할 건 그 공개키를 어디서 가져오느냐입니다.

IdP는 자신의 공개키 목록을 JWKS(JSON Web Key Set)라는 JSON 문서로 공개해 둡니다.

{
  "keys": [
    { "kid": "key-1", "kty": "RSA", "alg": "RS256", "n": "...", "e": "AQAB" },
    { "kid": "key-2", "kty": "RSA", "alg": "RS256", "n": "...", "e": "AQAB" }
  ]
}

키가 여러 개인 이유는 IdP가 보안을 위해 키를 주기적으로 교체하기 때문입니다. 교체 중에는 이전 키와 새 키가 함께 걸려 있습니다. 그래서 ID Token Header의 kid를 보고, JWKS에서 kid가 같은 키를 골라 서명을 검증합니다.

스펙상 인가 코드 방식처럼 /token에서 TLS로 직접 받은 ID Token은, 서명 검증 대신 TLS 서버 검증으로 발급자를 확인해도 됩니다. 그래도 대부분의 라이브러리는 서명을 검증하고, 그렇게 하는 게 안전합니다.

4. exp, iat: 아직 유효한가?

현재 시각이 exp보다 이전이어야 합니다. iat은 너무 오래전에 발급된 토큰을 걸러 내는 데 쓸 수 있고, 허용 범위는 앱이 정합니다.

5. nonce: 이번 로그인 요청에 대한 것인가?

ID Token이 어떤 경로로든 유출됐다고 해 보겠습니다. 공격자가 그 토큰을 다른 로그인 과정의 응답에 끼워 넣으면 어떻게 될까요? 서명은 진짜고, iss와 aud도 맞고, 아직 만료되지 않았다면 1~4번 검증을 모두 통과합니다. 이렇게 이미 발급된 토큰을 다시 써먹는 공격을 재전송 공격(replay attack)이라고 합니다.

nonce가 이걸 막습니다.

  1. 앱이 nonce를 만들어 세션에 저장하고, /authorize 요청에 함께 보냅니다.
  2. IdP는 그 nonce를 ID Token 안에 그대로 담아 줍니다.
  3. 앱은 ID Token의 nonce가 지금 세션에 저장된 값과 같은지 비교합니다.

다른 로그인 과정에서 발급된 ID Token에는 그 과정의 nonce가 들어 있습니다. 지금 세션의 nonce와 다르니 거부됩니다. 스펙이 nonce의 목적을 "클라이언트 세션과 ID Token을 연결하는 것"이라고 설명한 이유가 이것입니다.

지난 글의 state와 원리는 같고, 검사하는 대상이 다릅니다.

statenonce
담겨 오는 곳콜백 URL의 쿼리 파라미터ID Token 안
검사 대상이 콜백이 내가 보낸 요청의 응답인가?이 ID Token이 이번 로그인 요청을 위해 발급됐나?
막는 공격CSRF재전송(replay)

코드로 보기

JWT 라이브러리 jose로 검증하면 이렇습니다.

import { createRemoteJWKSet, jwtVerify } from 'jose';

// IdP의 JWKS 주소 (Discovery 문서의 jwks_uri)
const JWKS = createRemoteJWKSet(new URL('https://auth.example.com/jwks'));

// iss, aud, 서명(kid로 키 선택), exp 검증
const { payload } = await jwtVerify(idToken, JWKS, {
  issuer: 'https://auth.example.com',
  audience: 'my-app',
});

// nonce는 직접 비교
if (payload.nonce !== req.session.oauth.nonce) {
  throw new Error('nonce 불일치');
}
  • 라이브러리가 해 주는 것: JWKS 조회, kid로 키 선택, 서명 검증, exp 검사, 옵션으로 넘긴 iss·aud 검사
  • 직접 해야 하는 것: nonce 비교. 세션에 저장한 값은 라이브러리가 알 수 없기 때문입니다.

사용자 식별과 로그인 완료

검증을 통과했다면 이제 "이 사용자가 누구인지" 믿을 수 있습니다. 내 앱의 사용자와 연결할 때는 iss + sub 조합을 씁니다.

  • sub는 IdP 안에서만 고유합니다. 구글과 카카오 로그인을 함께 붙이면 두 IdP의 sub 값이 겹칠 수 있으니, 어느 IdP의 sub인지 iss로 구분합니다.
  • 이메일은 바뀔 수 있습니다. 스펙도 iss와 sub만이 사용자를 안정적으로 식별할 수 있는 값이고, 이메일은 고유 식별자로 쓰면 안 된다고 정해 두었습니다.
// (iss, sub)로 사용자를 찾고, 없으면 새로 만든다
const user = await findOrCreateUser({ iss: payload.iss, sub: payload.sub });

// 로그인 상태는 내 앱의 세션으로 유지
req.session.userId = user.id;

여기서 알아 둘 점이 있습니다. OIDC가 해 주는 일은 "이 사용자가 누구인지" 확인해 주는 것까지입니다. 그 뒤로 로그인 상태를 유지하는 건 내 앱의 세션(또는 내 앱이 발급하는 토큰)의 몫입니다. ID Token을 로그인 세션 대신 계속 들고 다니는 용도로 쓰지 않습니다.

UserInfo 엔드포인트: 사용자 정보가 더 필요할 때

ID Token으로 사용자가 누구인지는 알았지만, 서비스에 따라 이름이나 이메일 같은 정보가 더 필요할 수 있습니다. 이때 쓰는 것이 UserInfo 엔드포인트입니다.

UserInfo 엔드포인트는 Access Token을 보내면 사용자에 대한 클레임을 돌려주는 엔드포인트입니다. 앞에서 scope에 profile, email을 함께 요청했다면, 그 정보를 여기서 받습니다.

GET https://auth.example.com/userinfo
Authorization: Bearer eyJhbGciOi...
{
  "sub": "248289761001",
  "name": "김철수",
  "email": "chulsoo@example.com",
  "email_verified": true
}

IdP에 따라 이름, 이메일 같은 정보를 ID Token에 함께 넣어 주기도 합니다.

어디서 본 것 같다면 맞습니다. 도입부에서 본 "Access Token으로 프로필 API를 호출하는" 방식과 비슷합니다. 차이는 두 가지입니다.

  • 응답 형식이 OIDC 표준으로 정해져 있습니다.
  • 응답의 sub가 ID Token의 sub와 정확히 같은지 확인해야 합니다. 스펙은 그 이유로 토큰 바꿔치기 공격(token substitution)을 듭니다. 같지 않으면 UserInfo 응답을 쓰면 안 됩니다.

즉 로그인 여부는 ID Token으로 판단하고, UserInfo는 그 사용자의 추가 정보를 가져오는 용도로만 씁니다.

Discovery: 이 주소들은 어디서 알까?

지금까지 IdP의 주소가 여러 개 나왔습니다. /authorize, /token, JWKS, UserInfo, 그리고 iss와 비교할 issuer 값까지요. 이 값들을 IdP마다 문서를 찾아 하나씩 넣어야 할까요?

OIDC는 이 값들을 한 문서에 모아 공개하는 방법을 정해 두었습니다. OpenID Connect Discovery 스펙입니다. IdP의 issuer 주소 뒤에 /.well-known/openid-configuration을 붙이면 그 문서가 나옵니다.

GET https://auth.example.com/.well-known/openid-configuration
{
  "issuer": "https://auth.example.com",
  "authorization_endpoint": "https://auth.example.com/authorize",
  "token_endpoint": "https://auth.example.com/token",
  "userinfo_endpoint": "https://auth.example.com/userinfo",
  "jwks_uri": "https://auth.example.com/jwks",
  "scopes_supported": ["openid", "profile", "email"],
  "id_token_signing_alg_values_supported": ["RS256"]
}

앞의 검증 코드에 넣었던 JWKS 주소(jwks_uri)와 issuer 값이 모두 여기에 있습니다. ID Token의 iss는 이 문서의 issuer와 정확히 같아야 합니다.

그래서 많은 인증 라이브러리가 issuer 주소 하나만 받으면 이 문서를 읽어 나머지 설정을 알아서 채웁니다.

마치며

흐름 순서대로 다시 정리해 보겠습니다.

  • 인가 요청에 scope=openid와 nonce를 더해 보내면,
  • IdP가 토큰 응답에 ID Token을 함께 내려 주고,
  • 앱은 ID Token의 iss, aud, 서명, exp, nonce를 검증한 뒤,
  • iss + sub로 사용자를 식별해 로그인시킵니다.
  • 추가 정보가 필요하면 UserInfo 엔드포인트를, 주소와 설정은 Discovery 문서를 씁니다.

BetterAuth 같은 라이브러리를 쓰거나 AI가 만들어 준 로그인 코드를 볼 때는 아래 항목을 확인해 보세요.

  • scope에 openid가 들어 있는가?
  • nonce를 만들어 세션에 저장하고, ID Token의 nonce와 비교하는가?
  • ID Token의 iss, aud, 서명, exp를 검증하는가?
  • 사용자를 이메일이 아니라 iss + sub로 식별하는가?
  • UserInfo를 쓴다면, 응답의 sub를 ID Token의 sub와 비교하는가?

다음 글에서는 CIMD(Client ID Metadata Document)를 다뤄 보려고 합니다. 읽어주셔서 감사합니다.

댓글

아직 댓글이 없습니다.