diff --git a/keyword/chapter009/appendix/passkey.md b/keyword/chapter009/appendix/passkey.md new file mode 100644 index 0000000..c2a3ed2 --- /dev/null +++ b/keyword/chapter009/appendix/passkey.md @@ -0,0 +1,237 @@ +# FIDO란? + +FIDO(Fast IDentity Online)는 **비밀번호에 의존하지 않는 인증(passwordless authentication)**을 목표로 하는 국제 인증 표준이다. 기존 비밀번호 방식이 가진 근본적인 한계를 **공개키 암호화(비대칭키)** 기반 인증으로 대체하는 것이 핵심이다. + +## 왜 FIDO가 등장했는가 + +기존 비밀번호 인증은 다음과 같은 구조적 문제를 가진다. + +- **서버에 비밀번호(해시)가 저장된다** → 서버가 털리면 자격 증명이 통째로 유출된다. +- **사용자가 비밀번호를 직접 입력한다** → 피싱 사이트에 그대로 넘어갈 수 있다. +- **같은 비밀번호 재사용** → 한 사이트 유출이 다른 사이트 침해로 번진다. + +FIDO는 "**서버가 비밀번호를 알지 못하게 한다**"는 발상으로 이 문제들을 한 번에 해결한다. 사용자의 인증 정보(개인키)는 사용자 기기 안에서만 존재하고, 서버는 검증에 필요한 **공개키만** 저장한다. + +## FIDO의 핵심 원리 + +FIDO 인증의 본질은 **공개키 암호화 + Challenge-Response**다. + +1. 등록(Registration) 시 사용자 기기가 **키 쌍(개인키/공개키)**을 생성한다. +2. **개인키는 기기 안에 안전하게 보관**되고 절대 밖으로 나가지 않는다. +3. **공개키만 서버로 전송**되어 서버에 저장된다. +4. 로그인(Authentication) 시 서버는 매번 **랜덤한 challenge**를 보낸다. +5. 기기는 개인키로 challenge에 **서명**해서 응답하고, 서버는 저장된 공개키로 서명을 검증한다. + +비밀번호처럼 "공유된 비밀"을 주고받는 게 아니라, **개인키로 서명한 결과만 오가기 때문에** 네트워크를 도청해도, 서버가 털려도 사용자 인증 정보를 위조할 수 없다. + +## FIDO 표준의 발전 + +| 표준 | 설명 | +| --------- | ---------------------------------- | +| FIDO UAF | 비밀번호 없이 생체인증 등으로 인증 (Passwordless) | +| FIDO U2F | 기존 비밀번호 + 물리 보안키를 더하는 2차 인증 | +| **FIDO2** | UAF/U2F를 통합·발전시킨 최신 표준. 웹 표준으로 확장 | + +### FIDO2 = WebAuthn + CTAP + +현재 우리가 "패스키"라고 부르는 것의 기반이 되는 표준이 **FIDO2**이며, 두 가지 규격으로 구성된다. + +- **WebAuthn (Web Authentication API)**: 브라우저와 서버(웹 애플리케이션) 사이의 표준 API. W3C 웹 표준이다. 자바스크립트에서 `navigator.credentials.create()` / `navigator.credentials.get()`으로 호출한다. +- **CTAP (Client to Authenticator Protocol)**: 브라우저(클라이언트)와 인증 장치(Authenticator) 사이의 통신 규격. 예를 들어 PC 브라우저가 USB 보안키나 스마트폰과 통신할 때 사용한다. + +```text +[웹 서버] ←──── WebAuthn ────→ [브라우저/OS] ←──── CTAP ────→ [Authenticator] + (지문센서/보안키/스마트폰) +``` + +## 패스키(Passkey)와의 관계 + +**패스키(Passkey)**는 FIDO2/WebAuthn 기술을 일반 사용자가 쉽게 쓸 수 있도록 만든 사용자 친화적 구현이다. 기술적으로는 FIDO 자격증명(개인키)이지만, 다음과 같은 특징으로 대중화되었다. + +- **동기화 가능(Synced Passkey)**: 개인키를 iCloud 키체인, Google 비밀번호 관리자 등 클라우드에 암호화해 동기화 → 기기를 바꿔도 사용 가능. +- **기기 종속(Device-bound Passkey)**: 개인키가 특정 기기(보안키 등)를 절대 벗어나지 않음 → 보안성이 더 높지만 분실 시 복구가 까다로움. + +## 인증 수단(Authenticator)의 종류 + +- **Platform Authenticator (내장형)**: 기기에 내장된 인증 장치. 지문(Touch ID), 얼굴(Face ID), Windows Hello 등. +- **Roaming Authenticator (외장형)**: 분리 가능한 인증 장치. YubiKey 같은 USB/NFC 보안키, 또는 스마트폰을 PC의 인증 수단으로 사용하는 경우. + +## FIDO의 장점 + +- **피싱 저항성**: 자격증명이 도메인(Origin)에 묶여 있어 가짜 사이트에서는 동작하지 않는다. 사용자가 속아서 가짜 사이트에 접속해도 개인키 서명이 발생하지 않는다. +- **서버 유출에 강함**: 서버에는 공개키만 있으므로 DB가 털려도 인증을 위조할 수 없다. +- **재사용·도청 무력화**: 매번 다른 challenge에 서명하므로 응답을 가로채도 재사용 불가(Replay 방지). +- **사용자 편의성**: 비밀번호 암기·입력이 사라지고 생체인증으로 빠르게 로그인. + +## 한계 및 고려사항 + +- **계정 복구(Recovery)**: 기기 분실 시 복구 흐름 설계가 까다롭다. 보통 동기화 패스키나 보조 인증 수단으로 보완한다. +- **기존 시스템 전환 비용**: 비밀번호 기반 시스템을 FIDO로 옮기려면 등록/인증 흐름과 DB 스키마(공개키 저장) 변경이 필요하다. +- **백엔드 구현**: 서버는 WebAuthn 규격에 맞춰 challenge 발급·관리, 공개키 등록, 서명 검증 로직을 구현해야 한다. (Java에서는 `webauthn4j` 등의 라이브러리 활용) + + +# Challenge-Response Authentication + +Challenge-Response(시도-응답) 인증은 **서버가 매번 임의의 질문(Challenge)을 보내고, 클라이언트가 그에 대한 올바른 응답(Response)을 만들어 보내야만 인증되는 방식**이다. 핵심은 **비밀 자체를 네트워크로 전송하지 않는다**는 점이다. + +## 왜 필요한가 + +비밀번호를 그대로 전송하는 방식은 두 가지 공격에 취약하다. + +- **도청(Eavesdropping)**: 네트워크를 가로채면 비밀번호가 그대로 노출된다. +- **재전송 공격(Replay Attack)**: 한 번 가로챈 인증 데이터를 그대로 다시 보내면 똑같이 인증이 통과된다. + +Challenge-Response는 **매번 달라지는 challenge**를 사용해 이 두 문제를 동시에 해결한다. 비밀(개인키나 비밀번호)은 응답을 "계산"하는 데만 쓰이고 직접 전송되지 않으며, challenge가 매번 다르므로 가로챈 응답을 재사용할 수 없다. + +## 기본 동작 흐름 + +```text +1. 클라이언트 → 서버 : 로그인 시도 (나 로그인할게) + ↓ +2. 서버 → 클라이언트 : Challenge 전송 (랜덤한 nonce 값) + ↓ +3. 클라이언트 : 자신의 비밀(개인키/비밀번호)로 challenge를 가공해 Response 생성 + ↓ +4. 클라이언트 → 서버 : Response 전송 + ↓ +5. 서버 : 자신이 보낸 challenge + 알고 있는 정보로 Response 검증 + ↓ +6. 검증 성공 → 인증 완료 / 실패 → 거부 +``` + +여기서 **Challenge는 보통 nonce(number used once)**, 즉 한 번만 쓰이는 랜덤 값이다. 매 인증마다 새로 생성되기 때문에 같은 응답이 두 번 유효할 수 없다. + +## 두 가지 방식 + +challenge에 대한 응답을 "어떻게 만드느냐"에 따라 크게 두 갈래로 나뉜다. + +### 1. 대칭키(공유 비밀) 기반 + +서버와 클라이언트가 **같은 비밀**을 공유하고 있다고 가정한다. 클라이언트는 challenge와 비밀을 함께 해시(HMAC 등)해서 응답을 만들고, 서버도 같은 계산을 해서 비교한다. + +- 예: CHAP(Challenge-Handshake Authentication Protocol), 일부 API 인증 +- **한계**: 서버도 동일한 비밀을 저장하고 있어야 하므로, **서버가 털리면 비밀이 유출**된다. + +### 2. 비대칭키(공개키) 기반 - FIDO/패스키가 채택 + +클라이언트는 **개인키**로 challenge에 **서명**하고, 서버는 저장해 둔 **공개키**로 서명을 검증한다. + +- 서버는 공개키만 가지고 있으면 되므로 **서버가 털려도 개인키를 알아낼 수 없다.** +- 개인키는 사용자 기기를 절대 벗어나지 않는다. +- 이것이 FIDO2/WebAuthn(패스키)이 사용하는 방식이다. + +```text +[등록 단계] +사용자 기기 : 키 쌍 생성 → 개인키 보관, 공개키만 서버로 전송 + +[인증 단계] +서버 → 클라이언트 : challenge 전송 +클라이언트 : 개인키로 challenge에 서명(sign) +클라이언트 → 서버 : 서명된 응답 전송 +서버 : 저장된 공개키로 서명 검증(verify) +``` + + +## 대칭키 vs 비대칭키 Challenge-Response + +| 구분 | 대칭키 기반 | 비대칭키 기반 (FIDO) | +|------|-----------|-------------------| +| 서버 저장 정보 | 공유 비밀 (원문/해시) | 공개키 | +| 서버 유출 시 | 비밀 유출 → 위조 가능 | 공개키만 유출 → 위조 불가 | +| 응답 생성 | 비밀로 해시(HMAC) | 개인키로 서명 | +| 응답 검증 | 같은 비밀로 재계산 비교 | 공개키로 서명 검증 | +| 대표 사례 | CHAP, API HMAC 서명 | FIDO2 / WebAuthn / 패스키 | + +## 재전송 공격(Replay)을 막는 이유 + +Challenge-Response의 핵심 보안 가치는 **Replay 방지**다. + +- 매 요청마다 challenge(nonce)가 달라진다. +- 공격자가 네트워크에서 응답을 통째로 가로채도, 그 응답은 **그 challenge에만** 유효하다. +- 다음 인증에서는 새로운 challenge가 오기 때문에 가로챈 응답은 무용지물이 된다. + +이를 보장하기 위해 서버는 보통 다음을 관리한다. + +- challenge에 **짧은 유효시간(TTL)**을 둔다. +- 한 번 사용된 challenge는 **재사용 불가**로 폐기한다(예: Redis에 저장 후 사용 시 삭제). + +## 다른 인증과의 비교 관점 + +토큰/세션 방식과 비교하면 역할이 다르다. + +- **세션/토큰**: 인증에 *성공한 이후* 상태를 어떻게 유지·전달할지(인가)에 대한 방식. +- **Challenge-Response**: 애초에 인증 그 자체를 *어떻게 안전하게 수행할지*에 대한 방식. + +즉 Challenge-Response로 안전하게 인증한 뒤, 그 결과를 세션이나 토큰으로 유지하는 식으로 함께 쓰일 수 있다. + +# 왜 비대칭키 암호화 방식이 패스키에 적절한지 + +패스키(Passkey)는 FIDO2/WebAuthn 기반의 인증 수단으로, 그 핵심에는 **비대칭키 암호화(공개키 암호화)**가 있다. 왜 대칭키가 아니라 비대칭키를 쓰는지는 패스키가 해결하려는 문제(비밀번호의 한계)를 보면 명확해진다. + +## 대칭키 vs 비대칭키 + +| 구분 | 대칭키 | 비대칭키 | +| -------- | ---------------------- | ----------------- | +| 키 구성 | 하나의 키(비밀)를 양쪽이 공유 | 개인키 + 공개키 한 쌍 | +| 암호화/검증 | 같은 키로 암호화·복호화 | 개인키로 서명 → 공개키로 검증 | +| 비밀 노출 위험 | 양쪽 모두 비밀 보관 → 노출 지점 2곳 | 개인키는 한쪽(사용자)만 보관 | +| 대표 알고리즘 | AES, HMAC | RSA, ECDSA, EdDSA | + +대칭키는 양쪽이 **같은 비밀**을 들고 있어야 하지만, 비대칭키는 **개인키는 사용자만, 공개키는 누구나** 가질 수 있다. 이 비대칭성이 패스키의 모든 장점을 만든다. + +## 1. 서버가 비밀을 저장하지 않는다 + +비밀번호 방식의 가장 큰 약점은 **서버가 인증 비밀(비밀번호 해시)을 저장한다**는 점이다. 서버 DB가 털리면 자격 증명이 통째로 유출된다. + +비대칭키 방식에서는 **서버에 공개키만 저장**된다. + +- 공개키는 이름 그대로 **공개되어도 무방한** 값이다. +- 공개키만으로는 서명을 위조할 수 없다(개인키를 역산할 수 없으므로). +- 따라서 **서버 DB가 통째로 유출되어도 공격자는 사용자로 위장할 수 없다.** + +만약 대칭키를 썼다면 서버도 같은 비밀을 가져야 하므로, 서버 유출 = 비밀 유출이 되어 비밀번호 방식과 다를 게 없어진다. 비대칭키이기 때문에 "서버는 검증만 가능하고 위조는 불가능"한 구조가 성립한다. + +## 2. 개인키가 사용자 기기를 절대 벗어나지 않는다 + +패스키의 개인키는 사용자 기기의 안전한 영역(Secure Enclave, TPM 등)에서 생성·보관되며 **네트워크로 전송되지 않는다.** + +- 등록 시에도 **공개키만** 서버로 보낸다. +- 인증 시에도 개인키 자체가 아니라 **개인키로 서명한 결과(Response)**만 전송된다. +- 따라서 네트워크를 도청해도, 중간자(MITM)가 끼어도 개인키를 얻을 수 없다. + +대칭키라면 어느 시점엔 비밀을 공유·전달해야 하는 순간이 생기지만, 비대칭키는 **전송이 필요한 것이 "공개되어도 되는 값(공개키)"과 "위조 불가능한 서명"뿐**이라 전송 과정 자체가 안전하다. + +## 3. 피싱(Phishing)에 강하다 + +패스키의 자격증명은 **특정 도메인(Origin)에 묶여서** 생성된다(WebAuthn의 `rpId`). + +- `bank.com`에서 만든 패스키는 `bank.com`에서만 동작한다. +- 사용자가 속아서 `bank-fake.com`에 접속해도 브라우저가 Origin이 다름을 인지하여 **개인키 서명 자체가 일어나지 않는다.** + +이것은 "사용자가 비밀을 직접 입력하지 않는다"는 비대칭키 구조 덕분이다. 입력할 비밀이 없으니 피싱 사이트에 넘겨줄 비밀도 없다. + +## 4. Challenge-Response와 결합해 재전송을 막는다 + +비대칭키는 **서명(Signature)**이라는 강력한 도구를 제공한다. 서버가 매번 다른 challenge를 보내고 사용자가 개인키로 서명하면: + +- 응답은 그 challenge에만 유효하다 → **Replay 공격 무력화**. +- 서버는 공개키로 서명을 검증만 하면 된다 → 서버는 비밀을 몰라도 검증 가능. + +```text +서버 → 사용자 : challenge (랜덤 nonce) +사용자 기기 : 개인키로 challenge 서명 (개인키는 기기 밖으로 안 나감) +사용자 → 서버 : 서명된 응답 +서버 : 저장된 공개키로 서명 검증 → 통과 시 인증 +``` + +## 정리: 비대칭키가 패스키에 적절한 이유 + +비밀번호(대칭적 비밀 공유) 방식이 가진 **3대 약점**을 비대칭키가 정확히 메운다. + +| 비밀번호의 문제 | 비대칭키(패스키)의 해결 | +|----------------|----------------------| +| 서버에 비밀 저장 → 유출 위험 | 서버엔 공개키만 저장 → 유출돼도 위조 불가 | +| 비밀을 네트워크로 전송 | 개인키는 전송 안 함, 서명만 전송 | +| 사용자가 비밀을 입력 → 피싱 | 입력할 비밀이 없음 + Origin에 묶임 → 피싱 무력 | + +즉, **"검증은 누구나(서버) 할 수 있지만 위조는 개인키 소유자만 할 수 있다"**는 비대칭키의 본질이, 패스키가 추구하는 *서버가 비밀을 모르는 안전한 인증*을 가능하게 한다. 대칭키로는 이 구조를 만들 수 없기 때문에 패스키는 반드시 비대칭키 기반이어야 한다. \ No newline at end of file diff --git a/keyword/chapter009/keyword.md b/keyword/chapter009/keyword.md new file mode 100644 index 0000000..addfeef --- /dev/null +++ b/keyword/chapter009/keyword.md @@ -0,0 +1,340 @@ +# 세션(Session)과 토큰(Token) + +세션과 토큰은 사용자를 인증한 이후 생성된 **인증 정보**를 어떤 방식으로 저장하고 전달할 것인지에 관한 방법이며, 이를 통해 자원 접근에 대한 **인가(Authorization)**를 수행할 수 있도록 한다. + +## 세션 (Session) + +세션 기반 로그인은 사용자의 인증 정보가 **서버 측 세션 저장소**에 저장되는 방식이다. (Stateful) + +1️⃣ 사용자가 로그인을 하면, 서버는 해당 인증 정보를 세션 저장소에 저장한 후 세션 저장소의 식별자인 **Session ID**를 클라이언트 측에 전달한다. + +2️⃣ 클라이언트 측은 받은 Session ID를 웹 스토리지 혹은 쿠키에 저장하고 인가가 필요한 요청 시 함께 전달하게 된다. + +### 세션 저장소 선택 +서버가 인증 정보를 직접 들고 있어야 하기 때문에 어떤 저장소를 쓰느냐가 시스템 전반의 성능과 안정성을 좌우한다. + +- **In-Memory**: 빠르지만 서버 재시작 시 데이터 휘발, 다중 서버 환경에서 공유 불가 +- **DB**: 영속성은 보장되나 매 요청마다 DB 조회 발생 → 성능 저하 +- **Redis (가장 보편적)**: 인메모리 기반의 빠른 속도 + 다중 서버 공유 가능 + TTL 설정으로 자동 만료 + +### 세션 고정 공격(Session Fixation) +세션은 서버가 발급한 ID 하나로 사용자를 식별하기 때문에, 공격자가 미리 알고 있는 세션 ID를 피해자가 사용하도록 유도하면 그대로 계정을 가로챌 수 있다. + +이를 막기 위해 **로그인 성공 시 반드시 세션 ID를 재발급**해야 한다. Spring Security에서는 기본적으로 `sessionFixation().migrateSession()` 정책이 적용되어 로그인 시점에 새 세션을 발급해 준다. + + +## 토큰 (Token) + +토큰 기반 로그인은 인증 정보를 서버가 추적하지 않고, **토큰 자체**만으로 권한을 판별하는 방식이다. (Stateless) +사용자가 로그인을 하면, 서버는 인증 정보를 담은 Token을 발행한 후 클라이언트 측에 전달한다. 클라이언트 측은 전달받은 Token을 웹 스토리지 혹은 쿠키에 저장한다. + +대표적인 토큰인 **JWT**의 경우 디지털 서명이 존재해 토큰의 내용이 위변조 되었는지 서버 측에서 확인할 수 있다. + +### JWT 구조 +`Header.Payload.Signature` (Base64URL 인코딩, `.`으로 구분) + +- **Header**: 토큰 타입(JWT), 서명 알고리즘(HS256, RS256 등) +- **Payload**: Claims (사용자 정보, 발급/만료 시각 등) — **암호화가 아닌 인코딩이므로 민감 정보 X** +- **Signature**: Header + Payload를 서버의 Secret Key로 서명한 값 → 위변조 검증용 + +### JWT 서명 알고리즘 + +**HS256 (대칭키)**: 발급/검증에 같은 Secret Key를 사용. 빠르지만 키가 한 곳이라도 노출되면 위조가 가능해진다. 단일 서버나 모놀리식 환경에 적합하다. + +**RS256 (비대칭키)**: 인증 서버는 Private Key로 서명하고, 리소스 서버는 Public Key로 검증한다. Private Key를 인증 서버에만 두면 되므로 **MSA 환경에서 토큰 위조 위험을 줄일 수 있다.** + +### 토큰은 클라이언트 어디에 저장해야 안전한가 +서버가 토큰을 보관하지 않는 만큼, **클라이언트 측 저장 위치**가 보안의 가장 중요한 포인트가 된다. + +- **localStorage**: JS로 접근 가능 → **XSS 공격에 취약** +- **sessionStorage**: 탭 닫으면 사라짐, 마찬가지로 XSS 취약 +- **Cookie (HttpOnly + Secure + SameSite)**: JS 접근 차단으로 XSS는 막을 수 있지만, **CSRF 공격에 취약**해질 수 있음 +- 실무에서는 보통 **Access Token은 메모리, Refresh Token은 HttpOnly Cookie**에 저장하는 패턴이 권장된다. + +### JWT의 무효화 문제 (로그아웃·강제 로그아웃) +Stateless의 가장 큰 한계점이다. 발급된 토큰은 서버가 추적하지 않기 때문에 만료 전에 강제로 무효화하기 어렵다. 실무에서 자주 쓰이는 해결 방법은 다음과 같다. + +- **블랙리스트(Redis)**: 로그아웃된 토큰을 Redis에 저장하고 매 요청마다 확인 → Stateless의 장점을 일부 포기 +- **짧은 만료시간 + Refresh Token**: 가장 일반적인 절충안이다. Access Token의 유효기간을 짧게 두어 무효화의 의미 자체를 약화시킨다 (탈취되더라도 곧 만료되므로). +- **토큰 버전 관리**: 사용자 테이블에 `token_version` 컬럼을 두고, 비밀번호 변경 등 보안 이벤트 시 값을 증가시켜 이전 토큰을 무효화시킨다. + + +## 세션 vs 토큰 + +| 구분 | 세션 | 토큰 (JWT) | +| ----- | ------------------- | -------------------- | +| 상태 관리 | Stateful | Stateless | +| 저장 위치 | 서버 (세션 저장소) | 클라이언트 | +| 크기 | 작음 (Session ID만 전달) | 큼 (사용자 정보, 메타데이터 포함) | +| 보안 | 탈취 시 서버에서 무효화 가능 | 만료 전까지 무효화 어려움 | +| 확장성 | 세션 공유 작업 필요 | 수평 확장에 유리 | +| 서버 부담 | 저장소 관리 필요 | 검증만 수행 | + +### 1. 사이즈 +- 세션의 경우 Session ID만 실어 보내면 되므로 트래픽을 적게 사용한다. +- 하지만 JWT는 사용자 인증 정보와 토큰의 발급 시각, 만료 시각, 토큰 ID 등 담겨있는 정보가 Session ID에 비해 크기 때문에 세션 방식보다 훨씬 더 많은 네트워크 트래픽을 사용한다. + +### 2. 안정성과 보안 +- 세션의 경우 모든 인증 정보를 서버 측에서 관리하기 때문에 보안 측면에서 조금 더 유리하다. +- 세션 ID가 해커에게 탈취당한 경우, 서버 측에서 해당 세션을 무효 처리하면 된다. +- 하지만 토큰의 경우 서버가 토큰을 추적하지 않기 때문에 클라이언트가 모든 인증 정보를 가지고 있다. +- 따라서 토큰이 한번 해커에게 탈취되면 해당 토큰이 만료되기 전까지는 피해를 입을 수 있다. + +### 3. 확장성 +- 현재 거의 모든 웹 애플리케이션이 토큰의 '확장성' 때문에 토큰 기반 인증 방식을 사용한다. +- 일반적으로 웹 애플리케이션의 서버는 수평적으로 확장한다. 이때, 여러 대의 서버가 요청을 처리하게 되는데 별도의 작업을 해주지 않는다면 세션 기반 인증 방식은 **세션 불일치 문제**를 겪게 된다. +- 하지만 토큰 기반 인증 방식의 경우 직접 인증 방식을 저장하지 않고 클라이언트가 저장하기 때문에 세션 불일치 문제로부터 자유롭다. +- 세션도 Redis 같은 중앙 저장소로 공유하면 다중 서버 환경에서 사용 가능하지만, Redis 자체가 SPOF(Single Point of Failure)가 될 수 있고 네트워크 I/O 비용이 발생한다. + +### 4. 서버의 부담 +- 세션은 서버가 직접 세션 데이터를 저장해야 하는 반면 토큰은 서버가 인증 데이터를 가지고 있지 않기 때문에 서버에서 관리해야 하는 부담이 확실히 적다. + + +# 엑세스 토큰(Access Token)과 리프레시 토큰(Refresh Token)이란 + +토큰 기반 인증에서 사용자의 인증 정보를 표현하고 권한을 검증하기 위해 사용되는 두 종류의 토큰. 보안성과 사용자 경험을 모두 만족시키기 위해 **역할을 분리**해 사용한다. +## Access Token + +**자원(Resource)에 접근할 수 있는 권한**을 증명하는 토큰이다. 클라이언트가 API를 호출할 때마다 HTTP 헤더에 실어 보낸다. + + ``` + Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... + ``` + +- 서버는 이 토큰을 검증해서 "누가, 무엇을 할 수 있는지" 판단한다. + +### 특징 +- **짧은 유효 기간** (보통 15분 ~ 1시간) + - 통신 빈도가 높아 탈취 가능성이 크기 때문 + - 탈취되더라도 피해 시간을 최소화 +- 대부분 **JWT 형식**으로 발급 → 서버가 별도 저장소 조회 없이 검증 가능 (Stateless) +- 사용자 식별 정보, 권한(Role), 만료 시각 등이 Payload에 담김 + +### JWT Access Token 예시 Payload +```json +{ + "sub": "user123", // 사용자 ID + "role": "USER", + "iat": 1700000000, // 발급 시각 + "exp": 1700003600, // 만료 시각 + "jti": "uuid-1234" // 토큰 고유 ID (블랙리스트 처리용) +} +``` + +### Access Token Payload에 무엇을 담을 것인가 +Payload는 Base64URL 인코딩일 뿐 암호화가 아니라서 **누구나 디코딩해서 내용을 볼 수 있다**. 즉, 무엇을 담을지가 곧 보안과 성능을 좌우한다. + +- 비밀번호, 주민번호 같은 **민감 정보는 절대 담지 않는다.** +- 너무 많이 담으면 토큰 크기가 커져 매 요청마다 네트워크 비용이 증가한다. +- 일반적으로 사용자 ID(`sub`), 권한(`role`), 만료 시각(`exp`), 토큰 고유 ID(`jti`) 정도만 포함한다. + - `sub`, `exp`, `jti`는 JWT 표준(Registered Claim)에서 정해둔 짧은 이름이고, `role`은 우리가 직접 정의하는 Custom Claim이다. +- 사용자 정보가 자주 바뀐다면(예: 사용자명 변경) 토큰에 담지 말고 매번 DB에서 조회하는 편이 안전하다. 토큰에 박혀 있는 값은 만료 전까지 동기화되지 않기 때문. + + +## Refresh Token + +**새로운 Access Token을 발급받기 위한 용도**의 토큰이다. 자원 접근에 직접 사용되지 않고, **인증 서버의 토큰 갱신 엔드포인트**(`/auth/refresh` 등)에만 사용된다. + +### 특징 +- **긴 유효 기간** (보통 1주 ~ 2주, 길게는 한 달 이상) +- **통신 빈도가 낮음** → 탈취 가능성이 상대적으로 낮음 +- 클라이언트에 안전하게 저장 (HttpOnly Cookie, 모바일 Secure Storage) +- 서버 측에도 저장하는 경우가 많음 (Redis/DB) → 무효화 가능 + +### 왜 필요한가? +- Access Token만 사용한다면 **유효 기간 내 탈취 위험**에서 벗어날 수 없다. +- 그렇다고 Access Token의 유효 기간을 짧게 하면 사용자는 자주 로그인해야 해서 UX가 나쁘다. + → **AT 짧게 + RT 길게**로 두 가지를 모두 잡는다. + - AT 만료 → RT로 새 AT 발급 → 사용자는 다시 로그인할 필요 없음 + - AT 탈취되어도 짧은 시간 내에 자동 만료 + +### RT를 서버에 저장하면 세션과 뭐가 다른가? +RT를 Redis/DB에 저장하는 순간 "사실상 세션과 비슷해지는 것 아닌가?"라는 생각이 들 수 있다. 어느 정도는 맞는 말이지만, 결정적인 차이가 있다. + +- 세션은 **매 API 요청마다** 서버 저장소를 조회해야 한다. +- 반면 토큰 방식은 RT만 서버에 저장하고, **AT 검증만큼은 Stateless로 유지**된다. AT 검증은 서명만 확인하면 끝이므로 DB 조회가 없다. +- 즉, 매 요청의 비용을 줄이면서 무효화 가능성은 RT에 한정해 확보하는 **현실적인 절충안**이다. 트래픽이 많은 서비스일수록 이 차이가 크게 벌어진다. + + +## 두 토큰의 비교 + +| 구분 | Access Token | Refresh Token | +| ------------- | ---------------- | --------------------- | +| 용도 | 자원 접근 인가 | Access Token 갱신 | +| 유효 기간 | 짧음 (15분~1시간) | 김 (1주~2주) | +| 사용 빈도 | 매 API 요청마다 | AT 만료 시에만 | +| 사용 대상 엔드포인트 | 모든 보호된 API | `/auth/refresh` 등 제한적 | +| 서버 저장 여부 | 보통 X (Stateless) | 보통 O (Redis/DB) | +| 저장 위치 (클라이언트) | 메모리 권장 | HttpOnly Cookie 권장 | + + +## 인증 흐름 + +``` +[Client] [Server] + | | + |---- 로그인 (id/pw) ----------->| + |<--- AT + RT 발급 -------------| + | | + |---- API 요청 (AT) ------------>| + |<--- 200 OK -------------------| + | | + | (시간 경과, AT 만료) | + | | + |---- API 요청 (만료된 AT) ----->| + |<--- 401 Unauthorized ---------| + | | + |---- /auth/refresh (RT) ------->| + |<--- 새로운 AT (+ 새 RT) ------| + | | + |---- API 재요청 (새 AT) ------->| + |<--- 200 OK -------------------| +``` + +1. 로그인 성공 시 서버는 **AT + RT**를 발급한다. +2. 클라이언트는 두 토큰을 안전하게 저장한다. +3. 클라이언트는 API 요청 시 헤더에 **AT**를 실어 보낸다. +4. AT가 만료되어 401 응답을 받으면, 클라이언트는 **RT**로 갱신을 요청한다. +5. RT까지 만료되었다면 → **재로그인** 필요. + + +## Refresh Token Rotation (RTR) + +한 번 사용된 RT는 즉시 폐기하고, 새 RT를 함께 발급하는 방식이다. + +- OAuth 2.0 Security BCP에서 권장. +- 탈취당한 RT의 수명을 사실상 "다음 갱신 요청까지"로 단축. + +### Reuse Detection (재사용 감지) +- 이미 사용된 RT가 다시 사용되면 → **탈취 의심** → 해당 사용자의 모든 RT 무효화 +- 정상 사용자도 강제 로그아웃되지만, 보안을 우선시함 + +``` +정상 사용자: RT1 → (사용) → RT2 → (사용) → RT3 ... +공격자: 탈취한 RT1 재사용 시도 → 서버: "이미 쓴 토큰인데?" +→ RT2, RT3 전부 무효화 → 강제 로그아웃 +``` + + +## OAuth란? + +- **OAuth (Open Authorization)**: 제3자 애플리케이션이 사용자의 비밀번호를 알지 못한 채 **사용자의 자원에 접근할 수 있도록 권한을 위임**하는 표준 프로토콜. +- 예: "카카오로 로그인하기"를 누르면, 우리 서비스는 카카오 비밀번호를 알 필요 없이 카카오로부터 사용자 정보를 받아올 수 있다. + +### 주요 4가지 역할 (Role) + +| Role | 설명 | 예시 | +| -------------------- | ----------------- | ---------- | +| Resource Owner | 자원의 실제 소유자 (사용자) | 본인 | +| Client | 자원에 접근하려는 애플리케이션 | 우리 서비스 | +| Resource Server | 자원을 가지고 있는 서버 | 카카오 API 서버 | +| Authorization Server | 인증/인가 토큰을 발급하는 서버 | 카카오 인증 서버 | + + +## OAuth 1.0의 한계 + +2007년 발표된 최초 표준(RFC 5849). 매 요청마다 **서명(Signature)을 생성해 첨부**하는 방식이라 안전했지만, 그 자체로 큰 단점이 되었다. + +- **복잡한 서명 로직**: 요청마다 HMAC-SHA1로 서명을 만들어 헤더에 첨부해야 함 → 클라이언트 구현이 매우 까다로움 +- **모바일/SPA 친화적이지 않음**: 웹 기반 흐름에 최적화돼 있어 다양한 클라이언트 환경에 적용하기 어려움 +- **단일 인증 흐름**: 상황(SPA, 모바일, M2M 등)별 대응이 불가능 +- **Refresh Token 개념 없음**: 토큰 만료 시 매번 재인증 필요 + +→ HTTPS가 사실상 표준이 된 시대가 되고 "메시지 단위 서명"의 복잡함을 감수할 이유가 사라졌고, 결국 OAuth 2.0이 표준 자리를 차지하게 되었다. + + +## OAuth 2.0의 특징 + +2012년 발표된 새로운 표준(RFC 6749). OAuth 1.0과 **하위 호환되지 않는** 완전히 새로운 프로토콜이다. + +- **Bearer Token 기반**: 서명 대신 토큰 자체를 권한 증명으로 사용 (`Authorization: Bearer {token}`) +- **HTTPS 필수**: 토큰을 평문으로 전달하므로 전송 구간 암호화로 보안 책임을 이관 +- **Grant Type**: 상황에 맞게 인증 흐름을 골라 쓰는 구조 +- **Refresh Token 도입**: 짧은 AT + 긴 RT로 보안과 UX를 동시에 +- **다양한 클라이언트 지원**: 웹/SPA/모바일/IoT/서버 간 통신까지 포괄 + + +## Grant Types (인증 방식) + +OAuth 2.0의 가장 큰 특징은 **하나의 흐름을 강제하지 않는다**는 점. 클라이언트 환경에 따라 선택해 쓴다. + +| Grant Type | 사용 사례 | +| ----------------------------- | ----------------------------------- | +| **Authorization Code** | 서버사이드 웹 앱 (가장 일반적, 가장 안전) | +| **Authorization Code + PKCE** | SPA, 모바일 앱 (client_secret 보관 불가 환경) | +| **Client Credentials** | 서버 간 통신 (M2M) | +| **Refresh Token** | Access Token 갱신용 | + +### Grant Type +- **Authorization Code**: 우리 서비스(서버) ↔ 인증 서버 ↔ 사용자 브라우저 3자가 협력. 가장 안전. +- **Client Credentials**: 사용자가 끼지 않는 서버-서버 통신용. 예) 결제 게이트웨이 API. +- **Refresh Token**: AT가 만료되면 RT로 새 AT를 받아오는 흐름. +- **PKCE**: Authorization Code의 보안 강화 확장. 모바일/SPA처럼 `client_secret`을 안전하게 보관할 수 없는 환경에서 사용. OAuth 2.1에서는 **모든 클라이언트에 의무화** 추세. + + +## Authorization Code Flow + +소셜 로그인(카카오, 구글, 네이버 등)이 기본적으로 채택한 방식. **우리 서비스 백엔드**가 직접 토큰을 발급받기 때문에 가장 안전하다. + +### 흐름 + +``` +1. [User] → [Client(우리 서비스)]: "OO로 로그인" 클릭 +2. [Client] → [Auth Server]: 인증 요청 (redirect_uri, client_id, scope, state) +3. [Auth Server] → [User]: 로그인 페이지 표시 +4. [User] → [Auth Server]: 로그인 + 권한 동의 +5. [Auth Server] → [Client]: Authorization Code 발급 (redirect로 전달) +6. [Client] → [Auth Server]: Code + client_secret 전달 +7. [Auth Server] → [Client]: Access Token (+ Refresh Token) 발급 +8. [Client] → [Resource Server]: Access Token으로 사용자 정보/API 호출 +``` + +### 핵심: 왜 Code를 한 번 더 교환하는가? +Authorization Code는 **사용자 브라우저의 URL을 통해** 클라이언트에게 전달된다 (`redirect_uri?code=xxx`). 즉 노출 가능성이 있는 경로다. + +- Code는 **수명이 매우 짧고(보통 10분 이내) 단 1회만 사용 가능** +- Code만으로는 토큰을 받을 수 없고, 반드시 **서버 사이드에 보관된 `client_secret`** 과 함께 교환해야만 Access Token 발급 +- `client_secret`은 **브라우저에 절대 노출되지 않음** → 공격자가 Code를 가로채도 토큰 교환은 불가 + +이중 단계를 거치는 이유가 바로 이 보안 설계다. + +### 주요 파라미터 + +| 파라미터 | 역할 | +| --------------- | --------------------------------------- | +| `client_id` | 우리 서비스를 식별하는 공개 ID (Auth Server에 사전 등록) | +| `client_secret` | 우리 서비스의 비밀키 (백엔드 환경변수 등에 보관) | +| `redirect_uri` | Code를 받을 콜백 URL (사전 등록된 값과 일치해야 함) | +| `scope` | 요청할 권한 범위 (예: `profile`, `email`) | +| `state` | CSRF 방어용 랜덤 값 (요청과 콜백의 state 일치 검증) | +| `code` | Auth Server가 발급하는 1회용 단기 인증 코드 | + + +## PKCE (Proof Key for Code Exchange) + +Authorization Code 흐름의 **보안 강화 확장**. SPA/모바일처럼 `client_secret`을 안전하게 보관할 수 없는 환경을 위해 고안되었으며, OAuth 2.1에서는 **모든 클라이언트에 의무화** 추세이다. + +Auth Server가 제공한 Code가 중간에 탈취될 수 있는 가능성을 고려해 고안된 방식이다. + +### 동작 원리 +1. 클라이언트가 매번 랜덤한 비밀값 **`code_verifier`** 를 생성한다. +2. 이걸 해시한 값(**`code_challenge = SHA256(code_verifier)`**)을 인증 요청에 함께 보낸다. +3. Auth Server는 발급한 Code와 `code_challenge`를 함께 저장해둔다. +4. 토큰 교환 시 클라이언트가 원본 `code_verifier`를 제출한다. +5. Auth Server는 받은 `code_verifier`를 해시해 처음 받은 `code_challenge`와 일치하는지 검증 → 일치하면 토큰 발급. + +→ 공격자가 Code를 가로채도 **클라이언트 메모리에만 있는 `code_verifier`** 를 모르면 토큰을 받을 수 없다. + +### Provider별 PKCE 지원 현황 +- **구글, 깃허브, MS, Apple**: PKCE 완전 지원 +- **카카오, 네이버**: 공식 문서에 PKCE 언급이 없거나 검증을 명확히 하지 않음 → 보내도 무시될 가능성 + +즉, PKCE는 클라이언트 쪽에서 켜는 것만으로 끝나는 게 아니라 **Provider가 검증해줘야 실효성이 생긴다.** + + +## 결론 + +현재 실무에서 OAuth라고 하면 사실상 OAuth 2.0이며, 그중에서도 **Authorization Code 방식이 가장 표준적으로 사용**된다. OAuth 1.0과의 차이를 따지는 것보다 OAuth 2.0의 **어떤 Grant Type을 어떤 상황에 쓸지**를 이해하는 것이 훨씬 중요하다. + +PKCE는 보안 강화 측면에서 권장되는 확장이지만, **클라이언트가 Confidential인지 Public인지, Provider가 PKCE를 검증하는지에 따라 실제 적용 여부가 달라진다.** 국내 소셜 로그인(카카오·네이버)을 붙이는 일반적인 Spring 백엔드 프로젝트라면 PKCE 없이 Authorization Code만으로 동작하는 경우가 대부분이다. + diff --git a/mission/chapter09/assets/jwt.png b/mission/chapter09/assets/jwt.png new file mode 100644 index 0000000..9cf54fb Binary files /dev/null and b/mission/chapter09/assets/jwt.png differ diff --git a/mission/chapter09/assets/jwt2.png b/mission/chapter09/assets/jwt2.png new file mode 100644 index 0000000..8e3e123 Binary files /dev/null and b/mission/chapter09/assets/jwt2.png differ diff --git a/mission/chapter09/assets/kakao.png b/mission/chapter09/assets/kakao.png new file mode 100644 index 0000000..57825dc Binary files /dev/null and b/mission/chapter09/assets/kakao.png differ diff --git a/mission/chapter09/assets/kakao2.png b/mission/chapter09/assets/kakao2.png new file mode 100644 index 0000000..4ad466b Binary files /dev/null and b/mission/chapter09/assets/kakao2.png differ diff --git a/mission/chapter09/mission.md b/mission/chapter09/mission.md new file mode 100644 index 0000000..bd047a1 --- /dev/null +++ b/mission/chapter09/mission.md @@ -0,0 +1,7 @@ +### 카카오 소셜 로그인 +![alt text]() +![alt text]() + +### JWT 로그인 구현 +![alt text]() +![alt text]()