세션 ID, 쿠키, API 키, OAuth 토큰, 사용자 ID, JWT는 모두 문자열처럼 보이지만 역할은 다르다. 어떤 값은 대상을 구분하는 식별자이고, 어떤 값은 요청 권한을 증명하는 자격 증명이며, 쿠키와 JWT는 값을 전달하거나 표현하는 메커니즘·형식이다.
기존 글은 이 값들을 응용 계층의 식별자로 한데 모았다. 다시 구분해 보니 보안 설계에서는 “무엇을 식별하는가”보다 “이 값이 유출되면 다른 사람이 권한을 행사할 수 있는가”를 먼저 물어야 했다.
먼저 한 표로 구분하면
| 항목 | 주된 역할 | 보통 무엇을 가리키나 | 값만 탈취해도 위험한가 |
|---|---|---|---|
| 사용자 ID | 식별자 | 서비스 내부 사용자 | 보통 이것만으로는 권한 없음 |
| 세션 ID | 서버 세션을 찾는 불투명한 핸들 | 로그인 세션 | 대개 매우 위험 |
| 쿠키 | 브라우저 저장·전달 메커니즘 | 세션 ID, 설정값 등 | 담긴 값에 따라 다름 |
| API 키 | 호출 주체 식별·서비스 접근 자격 증명 | 애플리케이션·프로젝트 | 권한이 연결돼 있으면 위험 |
| OAuth access token | 위임된 접근 권한 | scope·resource에 묶인 승인 | 매우 위험 |
| OAuth refresh token | access token 재발급 자격 증명 | 승인 grant와 client | access token보다 오래 악용될 수 있음 |
| JWT | claims를 담는 토큰 형식 | subject, issuer, audience 등 | 어떤 용도로 발급됐는지에 따라 다름 |
여기서 핵심은 형식과 용도를 분리하는 것이다. access token이 JWT일 수 있지만 반드시 JWT인 것은 아니다. 반대로 JWT라고 해서 자동으로 access token이 되는 것도 아니다.
사용자 ID는 왜 자격 증명이 아닌가
사용자 ID는 데이터베이스나 서비스 내부에서 사용자를 찾는 키다.
user_id = 48372
이 값은 로그, URL, API 응답에 나타날 수 있다. 그렇다고 48372를 안다는 사실만으로 그 사용자 권한을 얻어서는 안 된다. 서버는 별도의 인증 결과와 권한 검사를 통해 요청자가 해당 사용자 데이터에 접근할 수 있는지 확인해야 한다.
사용자 ID를 순차 숫자 대신 UUID로 바꾸면 추측은 어려워질 수 있지만, 권한 검사를 대체하지는 못한다. 추측하기 어려운 식별자와 권한을 증명하는 자격 증명은 다른 문제다.
세션 ID는 어떤 역할을 하나
전통적인 서버 세션에서는 로그인 상태와 권한 정보를 서버 쪽 저장소에 둔다. 브라우저는 그 레코드를 찾을 수 있는 무작위 세션 ID만 보낸다.
browser server
session_id=RANDOM_HANDLE ───▶ session store
├─ user_id
├─ roles
└─ expires_at
세션 ID 자체에 사용자 정보가 들어 있지 않아도, 서버가 그 값을 권한 있는 세션과 연결한다. 따라서 탈취한 사람이 같은 값을 제출해 세션을 재사용할 수 있다면 사실상 bearer credential처럼 취급해야 한다.
세션 ID에는 다음 원칙이 필요하다.
- 충분한 엔트로피를 가진 예측 불가능한 값으로 생성한다.
- 로그인과 권한 상승 뒤에는 새 값으로 회전한다.
- 로그아웃·만료 시 서버에서 무효화한다.
- 원문·분석 로그·오류 메시지에 그대로 남기지 않는다.
- URL 쿼리보다 보호 속성을 적용한 쿠키로 전달한다.
URL의 세션 ID는 브라우저 기록, 접근 로그, 복사한 링크, Referer 등을 통해 노출될 가능성이 있어 피하는 편이 안전하다.
쿠키는 세션 ID와 같은 것인가
쿠키는 브라우저가 서버의 Set-Cookie 응답을 저장했다가 조건에 맞는 요청의 Cookie 헤더로 되돌려 보내는 HTTP 상태 관리 메커니즘이다. 쿠키 안에 세션 ID를 넣을 수 있지만, 언어 설정이나 장바구니 식별자처럼 인증과 무관한 값도 넣을 수 있다.
Set-Cookie: __Host-session=RANDOM_HANDLE; Path=/; Secure; HttpOnly; SameSite=Lax
Secure: 안전한 연결에서만 쿠키를 전송하도록 제한한다.HttpOnly: 브라우저 스크립트 API가 쿠키 값을 읽지 못하게 한다.SameSite: cross-site 요청에서 쿠키가 전송되는 범위를 제한한다.Path=/: 쿠키의 경로 범위를 지정한다.__Host-접두사:Secure,Path=/,Domain미지정을 요구해 범위를 더 엄격하게 만든다.
이 속성 하나로 모든 공격이 사라지는 것은 아니다. 예를 들어 HttpOnly는 스크립트가 값을 직접 읽는 것을 막지만, XSS가 발생한 페이지에서 사용자의 세션으로 요청을 보내는 행위까지 막지는 못한다. CSRF 방어와 XSS 방어, 세션 회전, 서버 측 권한 검사를 함께 설계해야 한다.
RFC 6265를 대체하기 위해 작업 중인 rfc6265bis 초안은 현재 쿠키 동작과 SameSite, cookie prefix 등을 정리한다. 아직 RFC로 발행된 문서는 아니므로 구현 기준을 정할 때는 초안 상태와 브라우저 지원 범위를 함께 확인한다.
API 키는 사용자 로그인 토큰인가
API 키는 보통 어떤 애플리케이션이나 프로젝트가 API를 호출하는지 식별하고 사용량·쿼터·권한을 연결하는 데 쓴다. 서비스마다 의미가 다르므로 “API 키는 무조건 인증용”이라고 일반화할 수 없다.
권한이 연결된 API 키는 비밀로 취급한다.
X-API-Key: REDACTED
- 소스 코드와 공개 저장소에 넣지 않는다.
- 브라우저에 배포되는 코드에 장기 비밀 키를 심지 않는다.
- 쿼리 문자열보다 서비스가 지정한 헤더를 사용한다.
- 환경·용도별로 분리하고 최소 권한을 부여한다.
- 유출 가능성이 있으면 폐기하고 새 키로 회전한다.
- 서버 로그와 오류 출력에서 마스킹한다.
API 키가 호출 애플리케이션을 식별하더라도 최종 사용자가 누구인지, 그 사용자가 특정 데이터에 접근할 수 있는지는 별도로 확인해야 한다.
OAuth access token은 무엇을 증명하나
OAuth의 access token은 client가 resource server에 접근할 때 제시하는 권한 자격 증명이다. 일반적으로 사용자가 비밀번호를 제3자 앱에 주지 않고도 특정 범위의 접근을 위임하게 한다.
Authorization: Bearer ACCESS_TOKEN
Bearer token은 그 값을 가진 주체가 사용할 수 있으므로 전송과 저장에서 유출을 막아야 한다. 서버는 적어도 다음 항목을 검증해야 한다.
- 신뢰하는 issuer가 발급했는가?
- 이 resource server를 위한 audience인가?
- 현재 시각에 유효한가?
- 요청 작업에 필요한 scope·권한이 있는가?
- 취소·회전 정책에 따라 여전히 유효한가?
access token이 “사용자의 비밀번호를 인증한다”라고 설명하면 인증과 권한 위임이 섞인다. 사용자 인증은 authorization server의 별도 과정에서 일어날 수 있고, resource server가 받는 access token은 승인된 접근 범위를 판단하는 데 쓰인다.
refresh token은 access token과 어떻게 다른가
refresh token은 만료된 access token을 새로 받기 위해 authorization server에 제출하는 장기 자격 증명이다. 일반 API 요청을 위해 resource server에 보내는 값이 아니다.
client ── refresh token ──▶ authorization server
client ◀─ new access token ── authorization server
client ── access token ──▶ resource server
RFC 9700은 public client의 refresh token 재사용 공격을 탐지하기 위해 sender-constrained token이나 refresh token rotation을 사용하도록 요구한다. 모든 client에 refresh token을 발급할 필요도 없다. 발급 여부와 수명은 위험도와 사용 경험을 함께 보고 결정한다.
JWT는 암호화된 로그인 토큰인가
JWT는 JSON claims를 JWS 또는 JWE 구조로 표현하는 토큰 형식이다.
- JWS 기반 JWT: 서명 또는 MAC으로 무결성을 확인한다.
- JWE 기반 JWT: 내용을 암호화한다.
- Nested JWT: 서명과 암호화를 겹쳐 사용할 수 있다.
일반적인 서명 JWT의 payload는 암호문이 아니다. Base64url 디코딩만으로 claims를 읽을 수 있으므로 비밀번호, 주민번호, 비밀 키 같은 민감 정보를 넣으면 안 된다.
{
"iss": "https://issuer.example",
"sub": "user-48372",
"aud": "orders-api",
"exp": 1785700000,
"scope": "orders:read"
}
서명을 검증했다고 권한 검사가 끝나는 것도 아니다. 알고리즘과 키, iss, aud, exp, 필요한 claims를 애플리케이션 정책에 맞게 검증해야 한다.
무엇을 어디에 저장하고 보내야 하나
| 값 | 클라이언트 저장 | 전송 대상 | 로그 정책 |
|---|---|---|---|
| 사용자 ID | 필요 범위에서 가능 | 애플리케이션 API | 개인정보 정책에 따라 최소화 |
| 세션 ID | 보호 속성의 cookie가 일반적 | 세션을 소유한 origin | 원문 저장 금지 |
| API 키 | 서버 측 secret store 권장 | 지정된 API | 마스킹 |
| access token | client 유형별 보안 저장소 | 대상 resource server | 원문 저장 금지 |
| refresh token | access token보다 더 엄격한 저장 | authorization server | 원문 저장 금지 |
| JWT | 용도에 따라 다름 | 발급 계약의 대상 | payload도 민감할 수 있어 원문 저장 금지 |
브라우저 앱, 모바일 앱, 서버 사이드 앱은 공격 표면과 저장 수단이 다르다. 모든 환경에 하나의 저장 규칙을 복사하기보다 token의 가치, 실행 환경, XSS·CSRF 위험, 회전 가능성을 함께 판단한다.
설계할 때 던질 질문
- 이 값은 단순 식별자인가, 제시만으로 권한을 행사하는 자격 증명인가?
- 브라우저가 자동으로 전송하는가, 애플리케이션 코드가 명시적으로 붙이는가?
- 누가 발급하고 누가 검증하는가?
- 어떤 audience와 scope에 한정되는가?
- 유효 기간과 회전·폐기 방법이 있는가?
- 로그·분석 도구·오류 메시지에서 제거되는가?
- JWT라면 서명만 했는가, 암호화도 했는가?
- 식별자를 알게 된 것과 권한을 가진 것을 서버가 구분하는가?
쿠키와 캐시의 차이는 HTTP 쿠키와 웹 캐시 구분에서, GitHub 자격 증명을 안전하게 저장하는 방법은 GitHub 인증 오류 해결에서 이어서 볼 수 있다.
이 글은 인증 시스템을 운영해 해결한 사례가 아니라, 기존 학습 노트를 현재 표준과 보안 권고에 맞춰 다시 정리한 글이다.
참고 자료
'배움과 성장 > 네트워크·보안' 카테고리의 다른 글
| HTTP/1.1·HTTP/2·HTTP/3 차이: 연결·다중화·QUIC 비교 (0) | 2024.08.13 |
|---|---|
| REST·GraphQL·gRPC·WebSocket 차이: API 통신 방식 고르는 기준 (0) | 2024.08.13 |
| NAT와 NAPT 차이: IP 변환·포트 변환·포트 포워딩 구분 (0) | 2024.08.12 |
| IP·ICMP·ARP·DHCP 차이: 패킷 전송에서 각 프로토콜의 역할 (0) | 2024.08.12 |
| 데이터그램과 가상 회선 차이: IP 포워딩·MPLS 라벨 경로 (0) | 2023.04.04 |
댓글