Refresh Token 쿠키 인증 API 명세
Refresh Token을 DB에 저장하지 않고 stateless JWT로 유지하면서, 브라우저 저장 위치를 localStorage에서 HttpOnly 쿠키로 변경한 인증 계약입니다.
공통 규칙
- Base path:
/v1/auth - Access Token은 응답 본문으로 반환하고 이후 요청의
Authorization헤더에 사용합니다. - Refresh Token은 응답 본문에 포함하지 않고
refresh_tokenHttpOnly 쿠키로만 전달합니다. - 프론트 요청은 쿠키 송수신을 위해
credentials: 'include'를 사용해야 합니다. - Refresh Token은 DB나 Redis에 저장하지 않습니다.
Authorization: Bearer {accessToken}Refresh Token 쿠키
Set-Cookie: refresh_token={refreshToken}; Path=/v1/auth; Max-Age={JWT_REFRESH_EXPIRATION}; Secure; HttpOnly; SameSite=Lax| 속성 | 기본값 | 설명 |
|---|---|---|
| 이름 | refresh_token | Refresh Token 쿠키 이름 |
HttpOnly | true | JavaScript에서 쿠키 접근 차단 |
Secure | 운영 true | HTTPS에서만 쿠키 전송 |
SameSite | Lax | 교차 사이트 요청의 쿠키 전송 제한 |
Path | /v1/auth | 인증 API에만 쿠키 전송 |
Max-Age | JWT_REFRESH_EXPIRATION | JWT Refresh Token 만료 시간과 동일한 초 단위 값 |
로컬 HTTP 환경에서는 REFRESH_COOKIE_SECURE=false를 사용합니다.
1. Google OAuth 완료
Google callback에서 받은 일회용 ticket으로 로그인을 완료합니다.
POST /v1/auth/google/complete
Content-Type: application/jsonRequest
{
"ticket": "ticket-01"
}Response 200 OK
응답 헤더로 Refresh Token 쿠키를 발급합니다.
Set-Cookie: refresh_token=eyJ...; Path=/v1/auth; Max-Age=604800; Secure; HttpOnly; SameSite=Lax응답 본문에는 Refresh Token이 없습니다.
{
"isNewMember": false,
"accessToken": "access-token",
"memberId": "member-1",
"displayName": "홍길동",
"email": "hong@example.com"
}2. 토큰 재발급
쿠키의 Refresh Token을 검증한 뒤 Access Token과 Refresh Token을 모두 새로 발급합니다.
POST /v1/auth/token/refresh
Cookie: refresh_token={refreshToken}- 요청 body는 없습니다.
Authorization헤더는 필요하지 않습니다.- 브라우저에서는 쿠키를 직접 읽거나
Cookie헤더를 직접 만들지 않습니다.
fetch('/v1/auth/token/refresh', {
method: 'POST',
credentials: 'include',
})Response 200 OK
기존 쿠키를 새로운 Refresh Token 쿠키로 교체합니다.
Set-Cookie: refresh_token=new-refresh-token; Path=/v1/auth; Max-Age=604800; Secure; HttpOnly; SameSite=Lax{
"isNewMember": false,
"accessToken": "new-access-token",
"memberId": "member-1",
"displayName": "홍길동",
"email": "hong@example.com"
}3. 로그아웃
브라우저가 보관 중인 Refresh Token 쿠키를 만료시킵니다.
POST /v1/auth/logout- 요청 body가 없습니다.
Authorization헤더가 없어도 호출할 수 있습니다.credentials: 'include'를 사용해야 합니다.
Response 200 OK
Set-Cookie: refresh_token=; Path=/v1/auth; Max-Age=0; Secure; HttpOnly; SameSite=Lax{
"success": true
}로그아웃 후 프론트는 메모리 또는 기존 localStorage에 남은 Access Token을 직접 제거해야 합니다.
오류 응답
{
"code": "AU0001",
"message": "Authentication token is missing.",
"data": null
}| 코드 | HTTP | 발생 조건 |
|---|---|---|
DB0003 | 400 | OAuth ticket 또는 요청 값이 올바르지 않음 |
AU0001 | 401 | Refresh Token 쿠키가 없음 |
AU0005 | 400 | Refresh Token이 만료됐거나 형식·서명·타입이 올바르지 않음 |
US0001 | 404 | 토큰의 회원을 찾을 수 없음 |
US0002 | 400 | 탈퇴한 회원 |
재발급 실패 응답에서는 쿠키가 자동으로 삭제되지 않습니다. 프론트는 재발급 실패 시 /v1/auth/logout을 호출해 HttpOnly 쿠키를 만료시킨 뒤 로그인 화면으로 이동합니다.
프론트 재발급 흐름
보호 API 요청
├─ 성공 → 응답 처리
└─ 401
└─ POST /v1/auth/token/refresh
├─ 성공 → 새 Access Token 저장 → 원래 요청 1회 재시도
└─ 실패 → POST /v1/auth/logout → Access Token 제거 → /login 이동- 동시에 여러 API가
401을 반환해도 refresh 요청은 하나만 실행해야 합니다. - refresh API 자체의 실패는 다시 refresh하지 않습니다.
- 원래 API 요청은 최대 한 번만 재시도합니다.
- 로그인 이동 전 현재 경로를 저장하고, 로그인 성공 후 해당 경로로 복귀합니다.
- 기존
localStorage의 Refresh Token 저장과 조회 코드는 제거합니다.
환경 변수
| 변수 | 기본값 | 설명 |
|---|---|---|
JWT_REFRESH_EXPIRATION | 필수 | Refresh JWT 및 쿠키의 만료 시간(초) |
REFRESH_COOKIE_SECURE | 운영 true, 로컬 템플릿 false | Secure 쿠키 적용 여부 |
REFRESH_COOKIE_SAME_SITE | Lax | SameSite 정책 |
프론트와 API가 서로 다른 사이트에 배포되어 쿠키가 전달되지 않는 경우에만 SameSite=None과 Secure=true 조합을 사용합니다. CORS 허용 origin에는 정확한 프론트 origin이 등록되어야 하며 wildcard origin은 사용할 수 없습니다.
Stateless 방식의 제약
- 로그아웃은 브라우저 쿠키만 삭제하며 이미 발급된 JWT 자체를 서버에서 폐기하지 않습니다.
- 쿠키에서 복사되거나 탈취된 기존 Refresh Token은 만료 전까지 다시 사용할 수 있습니다.
- Refresh Token 재사용 감지, 기기별 세션 관리, 전체 기기 로그아웃은 지원하지 않습니다.
- 회원이 탈퇴하면 Refresh Token이 남아 있어도 재발급을 거부합니다.