ID 토큰과 액세스 토큰 명쾌한 역할 구분

풀스택 개발자로서 수많은 프로젝트에서 소셜 로그인이나 API 보안을 구현하다 보면, OAuth 2.0OpenID Connect (OIDC)라는 두 프로토콜을 반드시 마주하게 됩니다. 많은 개발자들이 이 둘을 혼용하거나, 그 차이점을 어렴풋이만 알고 넘어가는 경우가 많습니다. 특히 이 두 프로토콜이 반환하는 '토큰(Token)'의 종류와 역할을 명확히 구분하지 못해 보안 허점을 만들거나 잘못된 아키텍처를 설계하기도 합니다. 바로 액세스 토큰(Access Token)ID 토큰(ID Token) 이야기입니다.

이 두 토큰은 생김새가 비슷해 보일지 몰라도, 태생부터 목적, 그리고 사용되는 주체까지 모든 것이 다릅니다. 마치 자동차 키와 운전면허증처럼 말이죠. 자동차 키(액세스 토큰)는 자동차의 특정 기능(리소스)을 사용할 '권한'을 주지만, 운전자가 누구인지 '신원'을 증명해주지는 않습니다. 반면 운전면허증(ID 토큰)은 그 사람이 운전할 자격이 있는 특정인임을 '증명'합니다. 이 글에서는 풀스택 개발자의 관점에서 두 토큰의 근본적인 차이점을 깊이 있게 파헤치고, 실제 시나리오에서 어떻게 사용해야 하는지 명확한 가이드를 제시하고자 합니다. 이 둘의 차이를 이해하는 것이 곧 인가(Authorization)인증(Authentication)의 차이를 이해하는 핵심 열쇠입니다.

모든 것의 시작: 인가(Authorization)와 인증(Authentication)

액세스 토큰과 ID 토큰을 논하기 전에, 우리는 반드시 '인가'와 '인증'이라는 두 거인의 어깨 위에 올라서야 합니다. 이 두 용어는 보안의 가장 기본적인 개념이지만, 종종 혼용되어 혼란을 야기합니다. 이 둘의 개념을 명확히 하는 것만으로도 토큰의 역할이 절반은 이해됩니다.

인증 (Authentication): "당신은 누구십니까?" (Who are you?)

인증은 사용자의 신원을 확인하는 과정입니다. 사용자가 자신이 주장하는 그 사람이 맞는지 증명하는 절차를 말합니다. 가장 흔한 예는 아이디와 비밀번호를 입력하여 로그인하는 것입니다. 지문 인식, 얼굴 인식, OTP(One-Time Password) 등도 모두 인증의 한 형태입니다. 인증의 결과물은 "이 사용자는 'user123'이 맞다"라는 신원 확인 그 자체입니다.

인가 (Authorization): "당신은 무엇을 할 수 있습니까?" (What can you do?)

인가는 인증된 사용자가 특정 리소스나 기능에 접근할 수 있는 권한이 있는지 확인하는 과정입니다. 즉, 신원이 확인된 후에 일어나는 일입니다. 예를 들어, 'user123'이라는 사용자가 로그인(인증)에 성공했더라도, 관리자 페이지에 접근하거나 다른 사용자의 글을 삭제할 권한(인가)은 없을 수 있습니다. 인가의 결과물은 "user123은 글 읽기, 쓰기 권한은 있지만, 삭제 권한은 없다"와 같은 구체적인 권한 목록입니다.

이 개념을 프로토콜에 대입해 보겠습니다.

  • OAuth 2.0인가(Authorization)를 위한 프레임워크입니다. 제3자 애플리케이션(Client)이 사용자를 대신하여 특정 리소스 서버(Resource Server)의 자원에 접근할 수 있는 권한을 위임받는 절차를 정의합니다. OAuth 2.0의 핵심 결과물은 바로 이 '권한'을 증명하는 액세스 토큰입니다. OAuth 2.0 자체는 사용자가 누구인지에 대한 표준화된 정보를 제공하지 않습니다. 오직 '무엇을 할 수 있는지'에만 집중합니다.
  • OpenID Connect (OIDC)OAuth 2.0 위에 구축된 인증(Authentication) 계층입니다. OAuth 2.0의 인가 흐름을 그대로 사용하면서, 그 과정에서 사용자의 신원 정보를 담은 ID 토큰을 추가로 발급합니다. 즉, OIDC는 "이 사용자가 구글 계정으로 성공적으로 로그인했으므로, 이 사람의 이메일은 'test@gmail.com'이고 이름은 '홍길동'이다"와 같은 신원 정보를 클라이언트에게 안전하게 전달하는 방법을 표준화한 것입니다.

결론적으로, OIDC는 OAuth 2.0의 확장판이며, OAuth 2.0만 사용하면 '인가'만 가능하지만, OIDC를 사용하면 '인가'와 '인증'을 모두 처리할 수 있게 됩니다. 이 구조적 차이 때문에 액세스 토큰과 ID 토큰이라는 두 가지 다른 목적의 토큰이 탄생하게 된 것입니다.

액세스 토큰 (Access Token): 리소스 접근을 위한 '만능 열쇠'

액세스 토큰은 OAuth 2.0 프로토콜의 심장과도 같습니다. 이 토큰의 유일한 목적은 보호된 리소스(API)에 접근할 때 제시하는 '자격 증명'입니다. 클라이언트 애플리케이션은 이 토큰을 획득하여 리소스 서버에 API를 요청할 때마다 HTTP `Authorization` 헤더에 담아 보냅니다.

액세스 토큰의 핵심은 '누가'를 알려주는 것이 아니라, '무엇을 할 수 있는지'를 증명하는 것입니다. 리소스 서버는 이 토큰을 보고 요청자가 어떤 권한(scope)을 가지고 있는지 확인하고, 그에 따라 요청을 처리하거나 거부합니다.

액세스 토큰의 형식: Opaque vs JWT

OAuth 2.0 명세(RFC 6749)는 액세스 토큰의 구체적인 형식을 강제하지 않습니다. 리소스 서버가 이해할 수만 있다면 어떤 형태든 상관없습니다. 크게 두 가지 형태로 나눌 수 있습니다.

1. Opaque Tokens (불투명 토큰)

문자 그대로 토큰 자체가 아무런 의미를 가지지 않는 랜덤한 문자열입니다. 마치 데이터베이스의 기본 키(Primary Key)와 같습니다.

v1.a9f4a2b1f3c5e7d9a0b3c5e...

클라이언트나 리소스 서버는 이 문자열만 보고는 아무런 정보를 얻을 수 없습니다. 리소스 서버가 이 토큰을 받으면, 토큰을 발급한 인가 서버(Authorization Server)에 "이 토큰 유효한가? 유효하다면 어떤 권한을 가지고 있나?"라고 직접 물어봐야 합니다. 이 과정을 토큰 검증(Token Introspection)이라고 합니다. (RFC 7662)

  • 장점:
    • 보안성: 토큰 자체가 정보를 담고 있지 않으므로, 만약 탈취되더라도 공격자가 얻을 수 있는 정보가 제한적입니다.
    • 중앙 관리: 인가 서버에서 토큰의 상태를 실시간으로 제어할 수 있습니다. 예를 들어, 특정 사용자의 액세스 토큰을 즉시 강제로 만료시키는 것이 가능합니다.
  • 단점:
    • 성능 부하: 리소스 서버는 API 요청을 받을 때마다 인가 서버와 네트워크 통신을 해야 하므로, 응답 시간이 길어지고 인가 서버에 부하가 집중됩니다.

2. JWT (JSON Web Token) Access Tokens

최근 마이크로서비스 아키텍처에서 널리 사용되는 방식입니다. 액세스 토큰 자체가 필요한 모든 정보를 담고 있는 JSON 객체를 Base64Url로 인코딩한 형태입니다. JWT는 헤더(Header), 페이로드(Payload), 서명(Signature) 세 부분으로 구성되며, 각 부분은 `.`으로 구분됩니다.

eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2F1dGguZXhhbXBsZS5jb20vIiwic3ViIjoiMTIzNDU2Nzg5MCIsImF1ZCI6Imh0dHBzOi8vYXBpLmV4YW1wbGUuY29tLyIsInNjb3BlIjoicmVhZDpwaG90b3Mgd3JpdGU6cGhvdG9zIiwiZXhwIjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

리소스 서버는 이 토큰을 받으면, 인가 서버에 물어볼 필요 없이 토큰에 포함된 서명을 인가 서버의 공개키로 검증하여 유효성을 자체적으로 확인할 수 있습니다. 이를 자체 검증(Self-Contained Validation)이라고 합니다.

  • 장점:
    • 성능 및 확장성: 리소스 서버가 인가 서버에 의존하지 않고 독립적으로 토큰을 검증할 수 있어 빠르고, 마이크로서비스 환경에 적합합니다.
  • 단점:
    • 보안 고려사항: 페이로드가 단순히 인코딩된 것이라 누구나 디코딩하여 내용을 볼 수 있으므로, 민감한 정보를 담아서는 안 됩니다.
    • 토큰 폐기의 어려움: 한 번 발급된 토큰은 만료 시간(exp)이 되기 전까지는 유효합니다. 만약 토큰이 탈취되어도 즉시 무효화하기가 어렵습니다. (이를 해결하기 위해 Blacklist 관리 등의 추가적인 방법이 필요합니다.)

액세스 토큰(JWT)의 내용 (Claims)

JWT 형식의 액세스 토큰 페이로드에는 다음과 같은 정보(클레임)가 포함됩니다. 이는 토큰의 '권한'과 관련된 정보에 집중합니다.

  • iss (Issuer): 토큰 발급자 (예: `https://auth.example.com/`)
  • sub (Subject): 토큰이 발급된 주체 (사용자 ID 또는 클라이언트 ID)
  • aud (Audience): 토큰의 대상 수신자. 즉, 이 토큰을 사용해야 할 리소스 서버를 지정합니다. (예: `https://api.example.com/`)
  • exp (Expiration Time): 토큰 만료 시간. 이 시간 이후에는 토큰이 유효하지 않습니다.
  • iat (Issued At): 토큰 발급 시간.
  • jti (JWT ID): JWT의 고유 식별자. Replay 공격 방지에 사용될 수 있습니다.
  • scope (Scope): 이 토큰으로 접근할 수 있는 권한의 범위. 이것이 액세스 토큰의 핵심입니다. (예: `read:profile write:posts delete:posts`)

중요한 점은, 클라이언트 애플리케이션은 액세스 토큰의 내용을 해석하려 해서는 안 된다는 것입니다. 클라이언트에게 액세스 토큰은 그저 리소스 서버로 전달해야 할 '티켓'일 뿐입니다. 토큰의 내용을 열어보고 사용자 정보를 얻으려는 시도는 OAuth 2.0의 원칙에 위배됩니다. 그 역할은 바로 다음에 설명할 ID 토큰의 몫입니다.

ID 토큰 (ID Token): 사용자가 누구인지 증명하는 '디지털 신분증'

ID 토큰은 OpenID Connect (OIDC) 프로토콜의 핵심 결과물입니다. 액세스 토큰이 '무엇을 할 수 있는지'에 대한 것이라면, ID 토큰은 순수하게 '사용자가 누구이며, 어떻게 인증했는지'에 대한 정보를 담고 있습니다. 이 토큰의 주 소비자는 리소스 서버가 아닌, 사용자의 인증을 요청한 클라이언트 애플리케이션입니다.

클라이언트 애플리케이션은 ID 토큰을 통해 사용자의 신원을 확인하고, 이를 바탕으로 애플리케이션 내에서 로그인 세션을 생성하거나 사용자 맞춤형 화면을 보여줄 수 있습니다. ID 토큰은 API 호출을 위한 것이 아닙니다.

ID 토큰의 형식: 언제나 JWT

액세스 토큰과 달리, OIDC 명세는 ID 토큰이 반드시 JWT 형식이어야 한다고 규정합니다. 또한, 위변조를 방지하기 위해 반드시 JWS(JSON Web Signature)를 통해 전자서명이 되어 있어야 합니다. 클라이언트는 이 서명을 검증하여 토큰이 신뢰할 수 있는 인가 서버(OpenID Provider, OP)로부터 발급되었음을 확신할 수 있습니다.

ID 토큰의 내용 (Standard Claims)

ID 토큰의 페이로드에는 OIDC 표준에 정의된 클레임들이 포함됩니다. 이 클레임들은 사용자의 '신원'과 '인증' 행위에 대한 정보를 담고 있습니다.

  • iss (Issuer): 토큰을 발급한 OP의 식별자. 반드시 HTTPS 스킴을 사용해야 합니다.
  • sub (Subject): OP 내에서 해당 사용자를 고유하게 식별하는 값. 사용자의 ID입니다.
  • aud (Audience): 토큰을 발급받은 클라이언트의 ID (`client_id`). ID 토큰은 이 `aud` 클레임에 명시된 클라이언트만을 위한 것입니다.
  • exp (Expiration Time): 토큰의 만료 시간.
  • iat (Issued At): 토큰이 발급된 시간.

위 클레임들은 모든 ID 토큰에 필수로 포함되어야 하는 정보입니다. 여기에 더해, 인증 과정에 대한 중요한 정보들이 포함됩니다.

  • auth_time (Authentication Time): 사용자가 마지막으로 인증을 수행한 시간. 세션 관리나 재인증 요구 시점을 판단하는 데 사용될 수 있습니다.
  • nonce (Nonce): 클라이언트가 인증 요청 시 보냈던 임의의 문자열. 응답으로 받은 ID 토큰에 이 값이 그대로 포함되어 있는지 확인하여 Replay 공격을 방지합니다. 매우 중요한 보안 장치입니다.
  • amr (Authentication Methods References): 사용자가 어떤 방식으로 인증했는지를 나타내는 식별자 배열. (예: `["pwd", "mfa"]`는 비밀번호와 다중 요소 인증을 모두 사용했음을 의미)
  • azp (Authorized party): 토큰이 위임된 당사자를 나타냅니다. `aud` 클레임이 여러 개인 경우, 이 토큰이 특별히 어떤 클라이언트를 위해 발급되었는지 명확히 하기 위해 사용될 수 있습니다.

또한, 클라이언트가 인증 요청 시 `scope` 파라미터에 `profile`, `email`, `address`, `phone` 등을 포함하면, 사용자의 동의 하에 다음과 같은 개인정보 클레임들이 ID 토큰에 포함될 수 있습니다.

  • name: 사용자의 전체 이름
  • given_name: 이름
  • family_name: 성
  • email: 사용자의 이메일 주소
  • email_verified: 이메일 주소가 검증되었는지 여부
  • picture: 사용자 프로필 사진 URL
  • ... 등 OIDC 표준에 정의된 다양한 프로필 정보

클라이언트는 이 정보들을 사용하여 별도의 API 호출 없이도 사용자 정보를 얻고 UI에 표시할 수 있습니다. 예를 들어, "홍길동님, 환영합니다!"와 같은 메시지를 보여주거나, 프로필 이미지를 표시하는 것이 가능해집니다.

한눈에 비교하는 액세스 토큰 vs. ID 토큰

지금까지 설명한 내용을 표로 정리하면 두 토큰의 차이점을 더욱 명확하게 이해할 수 있습니다.

항목 액세스 토큰 (Access Token) ID 토큰 (ID Token)
주요 목적 인가 (Authorization): 보호된 리소스(API)에 대한 접근 권한 부여 인증 (Authentication): 사용자의 신원 정보 및 인증 사실 증명
관련 프로토콜 OAuth 2.0 OpenID Connect (OIDC)
주요 소비자 리소스 서버 (API 서버) 클라이언트 애플리케이션
형식 정해진 형식 없음 (Opaque 또는 JWT 등) 반드시 JWT 형식 (JWS로 서명 필수)
핵심 정보 (클레임) 권한 범위 (scope), 대상 리소스 (aud), 만료 시간 (exp) 등 사용자 식별자 (sub), 발급자 (iss), 대상 클라이언트 (aud), 인증 정보 (auth_time, nonce) 등
검증 방법 - Opaque: 인가 서버에 Introspection 요청
- JWT: 서명 자체 검증
클라이언트가 OP의 공개키를 이용해 JWT 서명을 직접 검증
전송 위치 클라이언트가 리소스 서버로 API 요청 시 HTTP Authorization: Bearer 헤더에 담아 전송 인가 서버(OP)가 클라이언트로 전송. 다른 곳으로 전송해서는 안 됨
주요 사용 사례 '구글 캘린더 API'를 호출하여 일정 정보 가져오기 '구글 계정으로 로그인' 후, 웹사이트에 "홍길동님 환영합니다" 표시하기
비유 영화 티켓, 자동차 키, API Key 주민등록증, 운전면허증, 여권

실제 시나리오: 소셜 로그인 흐름 속 두 토큰의 여정

이론적인 설명을 넘어, 실제 '구글 계정으로 로그인' 시나리오에서 두 토큰이 어떻게 사용되는지 Authorization Code Flow를 따라가며 살펴보겠습니다. 이 흐름을 이해하면 모든 조각이 맞춰질 것입니다.

  1. 1단계: 사용자가 '구글로 로그인' 버튼 클릭
    클라이언트 애플리케이션(예: `our-service.com`)은 사용자를 구글의 인가 서버(OP)로 리디렉션시킵니다. 이때 여러 쿼리 파라미터를 함께 보냅니다. GET https://accounts.google.com/o/oauth2/v2/auth? response_type=code& client_id=YOUR_CLIENT_ID.apps.googleusercontent.com& scope=openid%20profile%20email& redirect_uri=https://our-service.com/auth/callback& nonce=aBcDeFgHiJkLmNoP
    • response_type=code: Authorization Code를 받겠다는 의미입니다.
    • client_id: 구글에 미리 등록한 우리 서비스의 식별자입니다.
    • scope=openid profile email: 이 부분이 핵심입니다. openid는 OIDC 프로토콜을 사용하겠다는 선언이며, ID 토큰을 요청하는 역할을 합니다. profileemail은 ID 토큰에 사용자의 이름, 프로필 사진, 이메일 정보를 포함해달라는 요청입니다. 만약 scopehttps://www.googleapis.com/auth/calendar.readonly 와 같은 API 권한을 추가하면, 액세스 토큰도 함께 발급됩니다.
    • redirect_uri: 구글이 인증 후 사용자를 돌려보낼 우리 서비스의 주소입니다.
    • nonce: Replay 공격을 막기 위한 클라이언트 측에서 생성한 랜덤 문자열입니다.
  2. 2단계: 사용자 인증 및 동의
    사용자는 구글 로그인 페이지에서 아이디/비밀번호를 입력하여 인증(Authentication)하고, "our-service.com이 당신의 이름, 이메일 주소, 프로필 정보에 접근하는 것을 허용하시겠습니까?" 와 같은 동의 화면에서 인가(Authorization)를 수행합니다.
  3. 3단계: Authorization Code 발급
    인증과 동의가 완료되면, 구글은 사용자를 redirect_uri로 돌려보내면서 쿼리 파라미터에 일회성 Authorization Code를 담아줍니다. HTTP/1.1 302 Found Location: https://our-service.com/auth/callback?code=4/0AY0e-g78...
  4. 4단계: 토큰 교환
    이제 클라이언트 애플리케이션의 백엔드 서버가 나설 차례입니다. 브라우저가 전달해준 Authorization Code를 가지고, 구글의 토큰 엔드포인트에 POST 요청을 보냅니다. 이 요청은 사용자의 브라우저를 통하지 않고 서버 간에 직접 이루어지므로 보안에 안전합니다. POST /oauth2/v4/token HTTP/1.1 Host: www.googleapis.com Content-Type: application/x-www-form-urlencoded code=4/0AY0e-g78...& client_id=YOUR_CLIENT_ID.apps.googleusercontent.com& client_secret=YOUR_CLIENT_SECRET& redirect_uri=https://our-service.com/auth/callback& grant_type=authorization_code
    • client_secret: 클라이언트의 비밀키입니다. 서버 간 통신이므로 안전하게 사용할 수 있습니다.
    • grant_type=authorization_code: Authorization Code를 토큰으로 교환하겠다는 의미입니다.
  5. 5단계: 토큰 수신 및 사용
    구글의 토큰 엔드포인트는 모든 정보가 유효하면, 응답으로 JSON 객체를 반환합니다. 드디어 우리가 기다리던 두 주인공이 등장합니다. { "access_token": "ya29.a0AfH6...-A", "expires_in": 3599, "scope": "openid https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email", "token_type": "Bearer", "id_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6Ij..." } 이제 클라이언트 백엔드 서버는 이 두 토큰을 가지고 각자의 역할을 수행합니다.
    • ID 토큰 처리 (인증):
      1. 먼저 `id_token` (JWT)을 파싱하기 전에, 서명을 반드시 검증합니다. 구글이 제공하는 공개키를 가져와서 서명이 유효한지 확인합니다.
      2. 서명 검증 후, `iss`가 `https://accounts.google.com`인지, `aud`가 우리 서비스의 `client_id`와 일치하는지 확인합니다.
      3. `exp`를 통해 만료되지 않았는지 확인하고, 1단계에서 보냈던 `nonce` 값이 토큰 내의 `nonce` 클레임과 일치하는지 확인합니다.
      4. 모든 검증을 통과하면, 클라이언트는 이 사용자가 진짜 구글 사용자임을 신뢰할 수 있습니다.
      5. ID 토큰의 페이로드에서 `sub`, `email`, `name` 등의 정보를 꺼내 우리 서비스의 데이터베이스에 사용자를 생성하거나 조회합니다.
      6. 마지막으로, 우리 서비스의 세션 쿠키를 생성하여 사용자의 브라우저에 내려보내 로그인을 완료시킵니다.
    • 액세스 토큰 처리 (인가):
      1. `access_token`은 나중에 구글 API를 호출할 일이 있을 때를 대비하여, 사용자 세션이나 데이터베이스에 안전하게 저장합니다. (암호화하여 저장하는 것이 좋습니다)
      2. 예를 들어, 사용자가 '내 구글 프로필 정보 가져오기' 버튼을 클릭하면, 서버는 저장해둔 `access_token`을 사용하여 구글의 `userinfo` 엔드포인트에 API 요청을 보냅니다. GET /oauth2/v3/userinfo HTTP/1.1 Host: www.googleapis.com Authorization: Bearer ya29.a0AfH6...-A

이처럼, ID 토큰은 클라이언트 내부에서 사용자의 신원을 확인하고 세션을 만드는 데 사용된 후 그 역할이 끝나지만, 액세스 토큰은 외부 API를 호출할 때마다 지속적으로 사용됩니다.

개발자가 흔히 저지르는 실수와 보안 모범 사례

두 토큰의 역할을 명확히 이해하지 못하면 심각한 보안 문제나 잘못된 설계를 초래할 수 있습니다. 다음은 현업에서 자주 발생하는 실수와 이를 방지하기 위한 모범 사례입니다.

실수 1: 액세스 토큰으로 사용자를 인증하려는 시도

문제점: JWT 형식의 액세스 토큰을 디코딩해서 `sub` 클레임을 보고 사용자를 식별하고 로그인 처리하는 경우가 있습니다. 이는 매우 위험하고 잘못된 방식입니다.

  • 잘못된 Audience: 액세스 토큰의 `aud` 클레임은 리소스 서버(API)를 가리킵니다. 클라이언트 애플리케이션은 이 토큰의 정식 수신자가 아닙니다.
  • 인증 정보 부재: 액세스 토큰에는 사용자가 '언제', '어떻게' 인증했는지에 대한 `auth_time`, `amr` 같은 표준 정보가 없습니다.
  • 위조 가능성: ID 토큰과 달리 액세스 토큰은 서명되지 않거나, 다른 용도의 서명 키를 사용할 수 있습니다. 클라이언트가 이를 검증할 표준 방법이 없습니다.

해결책: 사용자의 신원 확인 및 로그인 처리는 반드시 ID 토큰으로만 수행해야 합니다. ID 토큰은 오직 클라이언트를 위해 발급되었고, 신원 확인에 필요한 모든 정보를 표준화된 방식으로 제공하며, 검증 방법 또한 명확합니다.

실수 2: ID 토큰을 API 호출에 사용하는 경우

문제점: 반대로, ID 토큰을 `Authorization: Bearer` 헤더에 담아 리소스 서버(API)에 보내는 실수도 있습니다.

  • 잘못된 Audience: 리소스 서버는 ID 토큰을 받으면 `aud` 클레임이 자신의 식별자가 아닌 클라이언트의 ID인 것을 확인하고 요청을 거부해야 합니다. (이것이 올바른 구현입니다)
  • 불필요한 정보 노출: ID 토큰에는 사용자의 이메일, 이름 등 API가 필요로 하지 않는 민감한 개인정보가 포함될 수 있습니다. 이는 최소 권한 원칙에 위배됩니다.
  • 목적의 오용: ID 토큰은 신분증이지, 출입증이 아닙니다.

해결책: 보호된 리소스(API)에 접근할 때는 반드시 액세스 토큰을 사용해야 합니다. 액세스 토큰은 해당 API를 호출할 수 있는 명시적인 '권한'을 나타냅니다.

실수 3: ID 토큰 검증 절차 생략

문제점: 가장 치명적인 실수 중 하나입니다. 인가 서버(OP)가 보낸 ID 토큰을 검증 없이 그냥 디코딩해서 사용하는 것입니다. 공격자는 악의적으로 조작된 JWT를 클라이언트에 전송하여 다른 사용자로 위장할 수 있습니다.

해결책: 클라이언트는 ID 토큰을 수신하면 절대로, 절대로 검증을 생략해서는 안 됩니다.

  1. 서명(Signature) 검증: OP의 JWKS(JSON Web Key Set) 엔드포인트에서 공개키를 받아 서명을 검증합니다.
  2. 발급자(iss) 검증: 토큰을 발급한 주체가 내가 신뢰하는 OP가 맞는지 확인합니다.
  3. 대상(aud) 검증: 이 토큰이 나(내 클라이언트 ID)에게 발급된 것이 맞는지 확인합니다.
  4. 만료 시간(exp) 검증: 토큰이 만료되지 않았는지 현재 시간과 비교합니다.
  5. Nonce 검증: 인증 요청 시 보냈던 `nonce` 값과 ID 토큰 내의 `nonce` 값이 일치하는지 확인하여 Replay 공격을 방어합니다.

대부분의 언어와 프레임워크에는 신뢰할 수 있는 OIDC/JWT 라이브러리가 존재하므로, 직접 구현하기보다는 검증된 라이브러리를 사용하는 것이 안전합니다.

결론: 각자의 자리에 맞는 토큰을 사용하자

액세스 토큰과 ID 토큰의 관계는 명확합니다. 이 둘은 서로를 대체할 수 없는, 각자의 명확한 역할과 책임이 있는 독립적인 존재입니다.

  • ID 토큰클라이언트를 위한 인증(Authentication)의 결과물입니다. "당신은 누구입니까?"라는 질문에 대한 답입니다. 클라이언트는 이를 통해 사용자의 신원을 확인하고 로그인 세션을 시작합니다.
  • 액세스 토큰리소스 서버(API)를 위한 인가(Authorization)의 증표입니다. "무엇을 할 수 있습니까?"라는 질문에 대한 답입니다. 클라이언트는 이를 사용하여 보호된 리소스에 대한 접근 권한을 행사합니다.

풀스택 개발자로서 이 두 토큰의 근본적인 차이점을 깊이 이해하고 올바르게 사용하는 것은, 단순히 기능을 구현하는 것을 넘어 안전하고 확장성 있는 시스템을 설계하는 첫걸음입니다. 다음에 OAuth 2.0이나 OIDC 기반의 인증/인가 시스템을 구축할 때, 이 두 토큰을 손에 쥐고 "너는 인증용, 너는 인가용"이라고 명확히 구분할 수 있다면, 당신은 이미 보안 전문가의 길에 한 걸음 더 다가선 것입니다.

Post a Comment