Refresh Token 쿠키 인증 API 명세

Refresh Token을 DB에 저장하지 않고 stateless JWT로 유지하면서, 브라우저 저장 위치를 localStorage에서 HttpOnly 쿠키로 변경한 인증 계약입니다.

공통 규칙

  • Base path: /v1/auth
  • Access Token은 응답 본문으로 반환하고 이후 요청의 Authorization 헤더에 사용합니다.
  • Refresh Token은 응답 본문에 포함하지 않고 refresh_token HttpOnly 쿠키로만 전달합니다.
  • 프론트 요청은 쿠키 송수신을 위해 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_tokenRefresh Token 쿠키 이름
HttpOnlytrueJavaScript에서 쿠키 접근 차단
Secure운영 trueHTTPS에서만 쿠키 전송
SameSiteLax교차 사이트 요청의 쿠키 전송 제한
Path/v1/auth인증 API에만 쿠키 전송
Max-AgeJWT_REFRESH_EXPIRATIONJWT Refresh Token 만료 시간과 동일한 초 단위 값

로컬 HTTP 환경에서는 REFRESH_COOKIE_SECURE=false를 사용합니다.

1. Google OAuth 완료

Google callback에서 받은 일회용 ticket으로 로그인을 완료합니다.

POST /v1/auth/google/complete
Content-Type: application/json

Request

{
  "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발생 조건
DB0003400OAuth ticket 또는 요청 값이 올바르지 않음
AU0001401Refresh Token 쿠키가 없음
AU0005400Refresh Token이 만료됐거나 형식·서명·타입이 올바르지 않음
US0001404토큰의 회원을 찾을 수 없음
US0002400탈퇴한 회원

재발급 실패 응답에서는 쿠키가 자동으로 삭제되지 않습니다. 프론트는 재발급 실패 시 /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, 로컬 템플릿 falseSecure 쿠키 적용 여부
REFRESH_COOKIE_SAME_SITELaxSameSite 정책

프론트와 API가 서로 다른 사이트에 배포되어 쿠키가 전달되지 않는 경우에만 SameSite=NoneSecure=true 조합을 사용합니다. CORS 허용 origin에는 정확한 프론트 origin이 등록되어야 하며 wildcard origin은 사용할 수 없습니다.

Stateless 방식의 제약

  • 로그아웃은 브라우저 쿠키만 삭제하며 이미 발급된 JWT 자체를 서버에서 폐기하지 않습니다.
  • 쿠키에서 복사되거나 탈취된 기존 Refresh Token은 만료 전까지 다시 사용할 수 있습니다.
  • Refresh Token 재사용 감지, 기기별 세션 관리, 전체 기기 로그아웃은 지원하지 않습니다.
  • 회원이 탈퇴하면 Refresh Token이 남아 있어도 재발급을 거부합니다.