인증
세션, TOTP, 디바이스 검증, OAuth.
세션
회원가입은 지연(deferred) 방식입니다. POST /v0/auth/signup은 대기 중인 페이로드를 Redis에 저장하고
검증 토큰을 이메일로 발송하며, 계정 행은 POST /v0/auth/verify-email이 이를 소비할 때에만
생성됩니다. 이 방식은 미검증 가입 데이터를 데이터베이스에서 완전히 배제하고,
회원가입을 자연스럽게 계정 열거(enumeration)에 안전하게 만듭니다.
세션은 불투명한 256비트 bearer 토큰입니다. 토큰의 BLAKE3 해시만 저장되므로
(Redis의 session:{hash}), Redis 스냅샷이 유출되어도 재사용 가능한 자격 증명이 노출되지 않습니다.
세션은 다음을 갖습니다:
- 슬라이딩 TTL — 만료 시점 근처에서 갱신되며, 절대 최대 수명으로 상한이 제한됩니다;
- 관리용 id —
GET /v0/auth/sessions와DELETE /v0/auth/sessions/{management_id}에서 사용하는 별도의 식별자로, 세션 목록 조회/철회 시 bearer 토큰 해시가 절대 노출되지 않습니다.
브라우저는 토큰을 HttpOnly 쿠키로 전달하고(프로덕션에서는 __Host- 접두사 사용),
네이티브 앱은 /v0/app/auth/* 하위의 동일한 엔드포인트를 사용하며 토큰을 Authorization: Bearer로 전달합니다.
자격 증명 변경 시 세션은 적극적으로 무효화됩니다. 비밀번호 재설정과 이메일 변경은 모든 세션을 종료하고, 비밀번호 변경은 현재 세션만 유지합니다. 모든 자격 증명 변경은
소유자에게 이메일로 통지되며, auth_events 감사 행이 기록됩니다
(로그인 성공/실패, 비밀번호/이메일 변경, TOTP 활성화/비활성화, 새 디바이스 로그인).
TOTP 2FA
POST /v0/auth/totp/setup → enable이 RFC 6238 TOTP를 프로비저닝합니다. 시크릿은 TOTP_ENCRYPTION_KEY에서 파생된 키로 AES-256-GCM 암호화되어 저장되며, 검증된 코드는
유효 시간 동안 Redis에 클레임되어 동일한 코드가 절대 재사용될 수 없습니다. 10개의
일회용 백업 코드는 keyed hash로 저장되고 행 잠금(row lock) 하에서 소비되므로,
동일한 코드를 동시에 두 번 사용하는 시도는 둘 다 성공할 수 없습니다. TOTP가 활성화된 경우
로그인은 짧은 수명의 임시 토큰과 함께 202를 반환하며, POST /v0/auth/totp/verify가
이 토큰을 실제 세션으로 교환합니다.
새 디바이스 검증
자격 증명(및 TOTP) 검증을 모두 통과했더라도, 인식되지 않은 디바이스에서의 로그인은 보류됩니다. 서버는 일회용 챌린지를 이메일로 발송하고 세션 대신 202를 반환합니다. POST /v0/auth/device/verify가 이메일로 받은 토큰을 소비하고, 디바이스를 등록하며
(known_devices에 토큰 해시로 저장), 세션과 함께 수명이 긴 디바이스 쿠키를 발급합니다.
네이티브 앱은 X-Device-Token 헤더로 동일한 절차를 수행합니다.
OAuth (Google, GitHub, Google One Tap)
authorization-code 플로우는 PKCE와 함께, 일회용이고 해시로 저장되며 TTL이 적용되는 state를 사용합니다. 이 state는 호출자의 익명 쿠키에도 바인딩되어 로그인 CSRF를 방어합니다.
계정은 이메일 기준으로 절대 자동 연결되지 않습니다. 충돌 시 명시적인 오류가 반환되며,
기존 계정에 프로바이더를 연결하는 것은 세션 인증이 필요한 별도의 플로우입니다.
연결 해제는 마지막으로 남은 인증 수단의 제거를 거부합니다. Google ID 토큰은
발급자(issuer)/대상(audience)이 고정된 상태로 Google의 JWKS를 통해 검증되며, 강제 갱신에는
속도 제한이 적용됩니다. One Tap의 nonce는 일회용이며 익명 id에 바인딩됩니다.
새 OAuth 사용자는 POST /v0/auth/complete-signup을 통해 핸들을 선택합니다. 대기 중인
페이로드는 Redis 잠금과 멱등적인 완료 상태로 보호되므로, 응답이 유실되어도
안전하게 재시도할 수 있습니다.