aster®

[로그인인증] 시리즈 안내 (00)

마무리 · 2026-09

초록으로 채운 큰 열쇠 하나가 가로로 놓여 있고, 그 뒤에 같은 모양의 열쇠 두 개가 남색 윤곽선만으로 겹쳐 있다. 열쇠들 아래로 가는 선이 하나 지나간다.

로그인 기능은 거의 모든 서비스에 들어가지만, 막상 처음부터 직접 만들어 본 개발자는 드뭅니다. 보통은 프레임워크나 인증 서비스가 대신 해 주고, 우리는 "로그인하면 토큰이 온다" 정도만 압니다. 그러다 어느 날 이런 질문을 받습니다.

이 책은 이 질문들에 직접 만들어 보고 직접 공격해 보는 방식으로 답합니다. 실제로 토큰을 발급하고, 회전시키고, 탈취된 토큰을 탐지하고, 공격 페이지를 만들어 CSRF 를 성공시킨 다음 방어를 한 겹씩 입혀 공격이 실패하는 것을 눈으로 확인합니다.

무엇을 만드는가

auth-lab — 프론트엔드와 백엔드가 분리된 로그인 앱입니다.

부분기술주소
백엔드 APINestJS 12, PostgreSQL 17, pghttp://localhost:4000
API 문서Swagger UI (@nestjs/swagger)http://localhost:4000/docs
프론트엔드React 19 + Vite 8, openapi-fetchhttp://localhost:5173
공격 실습 페이지정적 HTMLhttp://localhost:5174, http://127.0.0.1:5174

완성된 앱은 다음을 합니다.

누구를 위한 책인가

쿠키와 CORS 는 몰라도 됩니다. 1장에서 처음부터 다룹니다.

이 책이 다루지 않는 것


차례

1부. 원리

장제목이 장을 마치면
1인증은 무엇을 기억하는 일인가세션과 토큰 방식의 차이, 쿠키의 모든 속성, 출처와 사이트를 구분해 설명할 수 있다
2토큰을 설계하다JWT 를 손으로 해부하고, 액세스·리프레시를 나누는 이유와 이 책의 설계 표를 이해한다

2부. 백엔드 — 토큰을 발급한다

장제목이 장을 마치면
3실습 환경을 세우다DB·NestJS·Swagger 가 뜨고, 환경변수를 검증하는 설정 계층이 생긴다
4가입 — 비밀번호를 저장하는 법argon2id 로 해싱하고, 에러 응답 형식과 Swagger 문서화 규칙을 정한다
5로그인과 액세스 토큰JWT 를 발급·검증하고, 가드로 API 를 보호하고, 로그인 시도를 제한한다
6리프레시 토큰을 발급하다불투명 토큰을 해시로 저장하고, 쿠키 속성을 하나하나 정해 내려보낸다
7회전과 재사용 탐지트랜잭션으로 토큰을 회전하고, 탈취를 탐지하고, 세션을 관리한다

3부. CSRF — 공격하고 막는다

장제목이 장을 마치면
8CSRF 공격을 직접 해 보기공격 페이지를 만들어 다른 사람의 닉네임을 바꾸는 데 성공한다
9방어를 한 겹씩SameSite·Origin 검사·서명된 CSRF 토큰을 하나씩 켜며 공격이 실패하는 것을 확인한다

4부. 프론트엔드 — Swagger 만 보고 붙인다

장제목이 장을 마치면
10Swagger 로 협업하기계약 파일을 내보내고, 그 파일에서 FE 타입과 클라이언트를 만든다
11로그인 화면과 세션 상태액세스 토큰을 메모리에 두고, 새로고침 뒤에도 로그인을 되살린다
12끊기지 않는 세션401 자동 재시도, single-flight, 탭 간 잠금, 로그아웃 동기화를 만든다

5부. 마무리

장제목이 장을 마치면
13시나리오로 증명하기22개 시나리오를 e2e 테스트와 curl 스크립트로 돌려 설계를 증명한다
14운영으로 가져가기도메인 구성, 비밀 관리, 키 교체, 모니터링 체크리스트를 채운다

부록


한 장의 그림으로 보는 전체 흐름

 브라우저 (localhost:5173)                         API (localhost:4000)                  PostgreSQL
 ─────────────────────────                        ─────────────────────                 ──────────
 ① GET /auth/csrf ─────────────────────────────▶  CSRF 토큰 생성
   ◀──── { csrfToken } + Set-Cookie: csrf_token
   (csrfToken 은 메모리에)

 ② POST /auth/login ───────────────────────────▶  CsrfGuard: Origin 확인, 헤더=쿠키 확인
    X-CSRF-Token: …                                비밀번호 argon2 검증
    { email, password }                            리프레시 토큰 생성 ─────────────────▶ INSERT (해시만)
   ◀──── { accessToken, user }                     액세스 토큰(JWT) 서명
         + Set-Cookie: refresh_token (HttpOnly)
   (accessToken 은 메모리에)

 ③ GET /users/me ──────────────────────────────▶  JwtAuthGuard: 서명·만료·iss·aud 확인
    Authorization: Bearer <accessToken>
   ◀──── { id, email, nickname }

    … 10분 뒤 액세스 토큰 만료 …

 ④ GET /users/me ─────────────────────────────▶   401 ACCESS_TOKEN_INVALID
 ⑤ POST /auth/refresh ────────────────────────▶   CsrfGuard
    Cookie: refresh_token (브라우저가 자동으로)      family 잠금, SELECT … FOR UPDATE ──▶ 옛 토큰 잠금
                                                   새 토큰 INSERT, 옛 토큰 rotated ───▶ 회전
   ◀──── { accessToken } + Set-Cookie: 새 refresh_token
 ⑥ GET /users/me (재시도) ────────────────────▶   200

이 그림의 번호 하나하나가 책의 한 장씩입니다. ①은 9장, ②는 5·6장, ③은 5장, ④~⑥은 7장과 12장에서 만듭니다.


실습 환경

준비물

도구버전확인 명령
Node.js22.22.3 이상 또는 24.15 이상 (Nest CLI 요구사항. 앱 실행만은 20.19+/22.12+ 로도 된다)node -v
pnpm11 이상 (이 책은 11.20)pnpm -v
Docker Desktop최신docker compose version
Chrome최신8·9장 실습은 Chrome 기준으로 설명합니다
curl, jq아무 버전curl --version, jq --version
Python 3아무 버전공격 페이지를 띄우는 데만 씁니다

이 책이 쓴 패키지 버전

2026년 9월에 npm 레지스트리에서 확인한 최신 버전입니다. 버전이 올라가면 API 가 바뀔 수 있으니, 책과 다르게 동작하면 먼저 버전을 확인하고 해당 패키지의 공식 문서를 보세요.

패키지버전쓰는 곳
@nestjs/core · @nestjs/common · @nestjs/platform-express12.1API
@nestjs/swagger12.0API 문서
@nestjs/jwt12.0JWT 서명·검증
@nestjs/config12.0.env 읽기
@nestjs/throttler6.7로그인 시도 제한
pg8.23PostgreSQL 드라이버
argon20.45비밀번호 해싱
cookie-parser1.4요청 쿠키 파싱
helmet8.3보안 헤더
class-validator · class-transformer0.15 · 0.5DTO 검증
typescript6.xNestJS 12 는 TypeScript 6 을 요구합니다. 스캐폴드가 설치하는 버전을 그대로 씁니다
react · react-dom19.3FE
vite8.3FE 개발 서버
react-router8.4FE 라우팅 (선언형 BrowserRouter·Routes 사용)
openapi-typescript7.13Swagger → TS 타입
openapi-fetch0.17타입 안전 API 클라이언트
vitest4.1e2e 테스트 러너 (Nest 스캐폴드 기본값)

ORM 을 쓰지 않는 이유. 이 책은 SQL 을 pg 로 직접 씁니다. 리프레시 토큰 회전의 핵심은 트랜잭션과 행 잠금(SELECT … FOR UPDATE)인데, ORM 을 쓰면 이 부분이 추상화 뒤로 숨어 버립니다. 인증 로직의 모든 줄이 눈에 보이는 것이 이 책의 목표라 SQL 을 그대로 드러냈습니다. 실무에서 ORM 을 쓴다면 7장의 트랜잭션만 그 ORM 의 방식으로 옮기면 됩니다.

폴더 구조

~/Workspace/auth-lab/
├── curriculum/        ← 지금 읽고 있는 책
├── docker-compose.yml
├── contract/          ← BE 가 내보낸 openapi.json (10장)
├── api/               ← NestJS
├── web/               ← React
└── evil/              ← 공격 페이지 (8장)

각 장의 구성

모든 장은 같은 순서를 따릅니다.

  1. 이 장을 마치면 — 장이 끝났을 때 할 수 있어야 하는 일
  2. 들어가며 — 왜 이 장이 필요한가, 앞 장과 어떻게 이어지는가
  3. 본문 — 이론 → 코드 → 실행 → 결과 확인을 한 묶음으로 반복합니다
  4. 확인하기 — 명령과 기대 결과. 여기가 통과해야 다음 장으로 넘어갑니다
  5. 자주 하는 실수 — 실제로 막히는 곳과 증상
  6. 정리
  7. 연습문제 — 해답은 문제 바로 아래 접힌 칸에 있습니다. 먼저 풀고 여세요

표기

먼저 알아 둘 용어

이 책에서 반복해서 쓰는 말입니다. 1장과 2장에서 자세히 다룹니다.

용어뜻
출처(origin)스킴 + 호스트 + 포트. http://localhost:5173 과 http://localhost:4000 은 다른 출처
사이트(site)스킴 + 등록 가능한 도메인. 포트를 보지 않는다. 위 둘은 같은 사이트
액세스 토큰API 를 부를 때 Authorization 헤더에 싣는 짧은 수명의 JWT
리프레시 토큰새 액세스 토큰을 받을 때만 쓰는 긴 수명의 난수. 쿠키로만 오간다
family로그인 한 번에서 시작해 회전으로 이어진 리프레시 토큰들의 묶음. 이 책에서는 곧 세션
회전(rotation)리프레시할 때마다 리프레시 토큰을 새것으로 바꾸고 옛것을 폐기하는 것
재사용 탐지폐기된 리프레시 토큰이 다시 오면 탈취로 보고 family 전체를 끊는 것
grace동시 요청 때문에 옛 토큰이 늦게 도착하는 것을 봐주는 짧은 시간(10초)
single-flight동시에 여러 곳에서 refresh 를 요청해도 실제 요청은 하나만 보내고 결과를 나눠 갖는 것