인증·인가 - 100-hours-a-week/KTB4-5th-wiki GitHub Wiki

배경 (Background)

프로젝트 목표

  • 별도의 비밀번호 생성·관리 없이 가입하고 로그인할 수 있도록 하여 사용자의 인증 부담을 줄인다.
  • 특정 서버의 로그인 상태에 의존하지 않는 일반 API 인증 구조를 마련하여 유연한 수평 확장을 지원한다.
  • 소셜 인증부터 가입 완료까지 사용자의 인증 결과가 위조되거나 부당하게 재사용되지 않도록 한다.
  • 인증 정보의 노출과 계정 도용에 따른 피해를 줄인다.
  • 권한 없는 재고 접근·변경과 참여자 내보내기를 방지하여 공동으로 관리하는 정보와 정상적인 공유 이용을 보호한다.

문제 정의

  • 별도 비밀번호 관리 부담: 사용자는 새로운 서비스를 이용할 때마다 별도의 비밀번호를 생성하고 기억·관리해야 하는 부담이 있다.
  • 서버별 로그인 상태에 따른 확장 제약: 로그인 정보를 개별 서버의 메모리에 저장하면 이후 요청이 해당 서버의 상태에 의존하게 된다. 서버를 추가하거나 제거할 때 로그인 상태 유지와 요청 분산을 위한 별도 처리가 필요해, 유연한 수평 확장에 제약이 생긴다.
  • 소셜 인증과 최종 가입 사이의 검증 공백: 소셜 인증 후 추가 정보를 입력하는 단계를 거치므로, 최종 가입 요청이 앞서 인증된 사용자에게 연결된 요청인지 확인해야 한다. 이 연결을 검증하지 않거나 만료·사용된 가입 인증 정보를 허용하면, 타인의 소셜 계정에 연결된 가입이나 유효하지 않은 가입 요청이 처리될 수 있다.
  • 인증 정보 탈취에 따른 계정 도용: 인증 정보가 노출되어 탈취되면 공격자가 해당 사용자로 가장하여 냉장고 데이터와 기능에 접근할 수 있다. 탈취된 인증 정보가 오래 유효하거나 허용 범위가 넓을수록 피해가 커질 수 있다.
  • 권한 없는 조작에 따른 공유 서비스 이용 방해: 냉장고 공유 서비스는 참여자들이 함께 관리하는 재고 정보를 바탕으로 이용된다. 외부 사용자가 재고를 임의로 추가·수정·삭제하면 실제 보관 상태와 기록이 달라져 식재료 파악과 구매 판단에 혼란이 생긴다. 또한 권한 없는 사용자가 참여자를 내보내면 정상적인 사용자의 공유 냉장고 이용이 부당하게 차단될 수 있다.

가설

  • 기존 소셜 계정으로 가입·로그인할 수 있도록 하면, 사용자가 별도의 비밀번호를 생성·기억해야 하는 부담을 줄일 수 있을 것이다.
  • 일반 API 요청을 특정 서버의 로그인 세션에 의존하지 않고 검증하면, 인증을 위한 요청 고정이나 세션 공유 없이 API 서버를 확장할 수 있을 것이다.
  • 백엔드에서 검증한 소셜 인증 결과를 최종 가입 요청과 연결하고 가입 인증 정보의 만료·재사용을 통제하면, 위조되거나 유효하지 않은 요청에 의한 가입을 방지할 수 있을 것이다.
  • 인증 정보의 노출 경로를 줄이고 유효 기간과 사용 범위를 제한하면, 탈취 가능성과 탈취 시 피해를 줄일 수 있을 것이다.
  • 요청 대상 냉장고의 참여 여부와 역할에 따라 기능 실행을 제한하면, 권한 없는 사용자의 재고 조작과 부당한 참여자 내보내기를 방지할 수 있을 것이다.

목표가 아닌 것 (Non-goals) (Optional)

  • 추가 본인 확인을 통한 중복 가입 제한: 이번 범위에서는 소셜 계정 기반 가입·로그인을 지원하며, 동일인이 여러 서비스 계정을 생성하는 것을 제한하기 위한 휴대전화 인증 등의 추가 본인 확인 절차는 포함하지 않습니다.
  • 장기 미접속 계정의 휴면 관리: 로그인 기록을 기준으로 장기 미접속 계정을 휴면 상태로 전환하거나, 휴면 계정을 복구하는 기능은 이번 범위에 포함하지 않습니다.
  • 기기별 로그인 관리: 사용자가 로그인된 기기 목록을 조회하거나 특정 기기의 로그인을 원격으로 해제하는 기능은 이번 범위에 포함하지 않습니다.
  • 관리자 전용 기능과 역할 부여·변경 기능: 이번 범위에서는 기본 사용자에 대한 기능만 관리하며 유저 제재 등의 관리자 기능을 추가하지 않습니다.

설계 및 기술 자료 (Architecture and Technical Documentation)

설계 범위와 접근 규칙

신규 사용자의 가입 인증

소셜 인증에 성공했으나 서비스에 가입되지 않은 사용자의 소셜 식별 정보를 임시 저장하고, 해당 정보에 연결된 가입용 토큰을 발급한다.

인증 도메인은 가입용 토큰의 유효성과 사용 가능 여부를 검증하고, 검증된 소셜 식별 정보를 회원 도메인에 제공한다. 회원 도메인은 클라이언트가 직접 제출한 소셜 식별 정보를 가입 자격으로 신뢰하지 않는다.

회원 생성 성공 후에는 같은 가입 자격을 재사용할 수 없도록 한다. 회원 생성 실패 시에는 토큰 유효기간 내 재시도를 허용한다. 토큰의 생성·보관·소비 정책은 ‘인증 상태와 토큰 관리 정책’, 실패 복구의 책임 경계는 ‘동시 요청과 실패·재시도 처리’에서 정의한다.

가입 정보 검증, 약관 동의 및 회원 생성은 회원 도메인의 책임으로 한다. 가입 완료 후 인증 토큰 발급은 서버 내부에서 확인된 회원 생성 결과와 현재 회원 상태를 기준으로 수행한다.

회원 도메인과의 연동

회원 정보 관리 및 회원 생성 절차는 회원 도메인에서 정의한다.

회원 도메인은 클라이언트가 제출한 소셜 식별 정보를 신뢰하지 않고, 인증 도메인이 registrationToken을 검증하여 제공한 정보를 사용한다.

가입용 토큰의 검증·소비와 회원 생성의 연동은 동일 토큰으로 중복 가입이 발생하지 않도록 처리한다. 회원 생성 실패 시 토큰의 유효 기간 내 재시도를 허용하고, 회원 생성 성공 후에는 같은 가입 자격을 재사용할 수 없도록 한다. 구체적인 처리 순서와 실패 복구는 회원 도메인의 회원가입 처리 규칙에서 정의한다.

가입 완료 후 자동 로그인을 제공하기 위해 인증 도메인은 서버 내부에서 확인된 회원 생성 결과와 사용자 상태를 기준으로 Access Token과 Refresh Token을 발급한다. 클라이언트가 제출한 회원 ID만으로 인증 토큰을 발급하지 않는다.

로그인

  • 아이디·비밀번호 로그인은 v1에서만 제공하며, v2부터 OAuth 기반 소셜 로그인을 제공한다.
  • 인증에 성공한 ACTIVE 회원에게 Access Token과 Refresh Token을 발급하고, 쿠키로 전달한다.
  • 발급한 Refresh Token은 서버 저장소에 ACTIVE 상태로 등록한다.
  • 여러 기기에서의 로그인을 허용하며, 새 로그인 시 다른 기기의 Refresh Token은 유지한다.
  • 소셜 인증에 성공했으나 가입되지 않은 사용자는 가입용 토큰을 발급하여 회원가입 절차로 연결한다. Access Token과 Refresh Token은 가입 완료 후 발급한다.

로그아웃

  • 현재 브라우저의 Access Token·Refresh Token 쿠키를 삭제하고, 요청에 포함된 Refresh Token을 서버에서 무효화한다.
  • 다른 기기의 Refresh Token은 유지한다.
  • 이미 발급된 Access Token은 서버에서 별도로 폐기하지 않으며, 만료 시각까지 유효하다. 다만 탈퇴 사용자의 요청은 현재 사용자 상태 검증을 통해 차단한다.

인증 갱신

  • 유효한 Refresh Token과 현재 사용자 상태를 검증하여 ACTIVE 회원에게 새 Access Token과 Refresh Token을 발급한다.
  • 기존 Refresh Token의 USED 전환과 새 Refresh Token의 ACTIVE 등록을 원자적으로 처리한다.
  • 새 토큰은 쿠키로 전달하여 기존 값을 교체한다.
  • 이미 발급된 Access Token은 Refresh Token 교체로 무효화하지 않으며, 만료 시각까지 유효하다.

사용자 상태와 역할

상태

일반 서비스 API는 ACTIVE 사용자만 사용할 수 있다.

WITHDRAWN 사용자의 로그인, 인증 갱신 및 일반 서비스 API 접근은 차단한다.

  • ACTIVE: 활성 상태
  • WITHDRAWN: 탈퇴 상태

서비스 역할

  • USER: 일반 사용자
  • ADMIN: 향후 관리자 기능 확장을 위해 예약한 역할

서비스 역할은 users의 role 컬럼에 저장한다. 현재 설계 범위에는 관리자 전용 기능과 그에 대한 접근 제어를 포함하지 않는다. 신규 회원의 기본 역할은 USER이며, ADMIN 역할 부여 절차와 관리자 권한 정책은 해당 기능을 도입할 때 별도로 정의한다.

Access Token의 role 클레임에는 발급 시점의 DB 역할을 포함한다. 현재는 이 값을 이용한 관리자 전용 접근 제어를 수행하지 않는다. 향후 관리자 기능 도입 시 역할 변경의 반영 시점과 권한 판단 기준을 함께 정의한다.

냉장고 접근 권한은 서비스 역할과 별개로, 해당 냉장고의 참여 관계와 OWNER·MEMBER 역할을 기준으로 판단한다.

냉장고 공유에서의 역할

  • OWNER: 냉장고 소유자
  • MEMBER: 소유자를 제외한 냉장고 공유 참여자

냉장고 소유자 · 참여자의 권한

냉장고 관련 요청은 사용자 상태와 해당 냉장고의 멤버십·역할을 기준으로 접근 권한을 판단한다. 참여·초대·탈퇴·소유권 이전에 대한 추가 제약은 냉장고 도메인 설계의 규칙을 따른다.

기능 OWNER MEMBER
재고 조회 허용 허용
재고 등록·수정·삭제 허용 허용
초대코드 발급 허용 불가
MEMBER 내보내기 허용 불가

시스템 구성과 기술 선택 근거

가입용 토큰에 연결된 임시 가입 정보와 Refresh Token의 상태는 유효 기간이 있는 데이터이므로, TTL을 적용할 수 있는 저장소에서 관리한다. 단일 인스턴스에서는 Caffeine을 사용하고, 다중 인스턴스에서는 모든 서버가 동일한 상태를 확인할 수 있도록 Redis를 사용한다.

가입이 완료된 계정의 식별 정보, 비밀번호 해시, 소셜 계정 연결 정보, 사용자 상태와 역할은 DB에 저장한다.

Spring Security를 사용하여 인증·인가와 CSRF 방어 정책을 공통으로 적용하고, API별 중복 구현과 보안 정책의 분산을 줄인다.

Access Token은 서명된 JWT로 발급하며, 발급한 토큰을 서버에 별도로 저장하지 않는다. Refresh Token은 토큰별 유효·사용·폐기 상태를 저장하여 인증 갱신, 재사용 탐지 및 현재 브라우저의 로그아웃을 지원한다.

서비스 전체 역할(USER·ADMIN)은 users에 저장하고, 냉장고별 참여 여부와 역할(OWNER·MEMBER)은 사용자와 냉장고의 관계를 나타내는 REFRIGERATOR_MEMBERS에 저장한다.

사용자가 기존 소셜 계정으로 가입·로그인할 수 있도록 Kakao와 Google을 지원하여 가입 절차의 부담을 줄인다.


데이터베이스 스키마

ERD

image image

회원 — users

서비스 회원의 기본 정보, 상태 및 v1 일반 로그인 정보를 관리한다.

물리명 타입 Null 키 설명
user_id BIGINT N PK 회원 ID - 회원을 식별하는 내부 대리키
nickname VARCHAR(10) N — 닉네임 - 서비스에서 표시하는 2~10자 회원 이름. UNIQUE(nickname). 탈퇴 시 치환값도 중복 불가
profile_image_key VARCHAR(1024) N — 프로필 이미지 Object key - 미등록 시 기본 이미지 Object key 저장
status VARCHAR(20) N — 회원 상태 - ACTIVE: 활성, WITHDRAWN: 탈퇴. CHECK (status IN ('ACTIVE','WITHDRAWN'))
기본값/생성식: 'ACTIVE'
cooking_count INT N — 만든 요리 수 - 재고 차감을 완료한 누적 횟수. CHECK (cooking_count >= 0)
기본값/생성식: 0
role VARCHAR(20) N — 역할 - USER: 일반 사용자, ADMIN: 관리자. CHECK (role IN ('USER','ADMIN'))
기본값/생성식: USER
created_at DATETIME(6) N — 회원 생성 일시
기본값/생성식: CURRENT_TIMESTAMP(6)
updated_at DATETIME(6) N — 회원 정보 최종 수정 일시
기본값/생성식: CURRENT_TIMESTAMP(6)
deleted_at DATETIME(6) Y — 회원 탈퇴 일시. 활성 회원은 NULL
login_id VARCHAR(10) Y — v1 영문 숫자 2~10자. UNIQUE(login_id). password_hash와 모두 NULL 또는 모두 NOT NULL CHECK. OAuth-only는 NULL
password_hash VARCHAR(60) Y — bcrypt 해시. login_id와 동시 존재 CHECK. 탈퇴 시 둘 다 NULL. 일반 응답에서 제외
password_changed_at DATETIME(6) Y — 기존 local_accounts의 변경 시각 보존. 이 시각보다 먼저 발급된 토큰 무효화 판단. OAuth-only는 NULL

소셜 계정 — social_accounts

외부 소셜 계정과 서비스 회원의 연결을 관리한다.

물리명 타입 Null 키 설명
social_account_id BIGINT N PK 소셜 계정 연결 ID
user_id BIGINT N FK 연결된 서비스 회원 ID. UNIQUE(user_id, provider)
provider VARCHAR(20) N — OAuth 제공자 - KAKAO, GOOGLE. CHECK (provider IN ('KAKAO','GOOGLE'))
provider_user_id VARCHAR(255) N — OAuth 제공자 사용자 식별자. UNIQUE(provider, provider_user_id)
created_at DATETIME(6) N — OAuth 계정 연결 일시
기본값/생성식: CURRENT_TIMESTAMP(6)
updated_at DATETIME(6) N — OAuth 계정 정보 최종 수정 일시
기본값/생성식: CURRENT_TIMESTAMP(6)
provider_email VARCHAR(255) Y — OAuth 제공자가 전달한 이메일 스냅샷. 로그인 식별자는 아니며 UNIQUE 미적용. 제공자가 이메일을 반환한 경우 저장하고, 성공 로그인 시 최신 값으로 갱신

냉장고 참여자 — refrigerator_members

회원과 냉장고의 참여 관계, 냉장고별 역할 및 현재 선택한 냉장고를 관리한다.

물리명 타입 Null 키 설명
refrigerator_member_id BIGINT N PK 참여자 ID - 냉장고 참여를 식별하는 대리키
refrigerator_id BIGINT N FK 냉장고 ID - 참여 대상 냉장고
user_id BIGINT N FK 참여 회원 ID. UNIQUE(refrigerator_id, user_id)
is_active TINYINT(1) N — 현재 사용하는 냉장고 여부. 전환 시 개인 관계 보존. CHECK (is_active IN (0,1))
기본값/생성식: 0
active_marker BIGINT Y — is_active=1이면 user_id, 아니면 NULL. UNIQUE(active_marker)는 회원당 활성 냉장고 최대 1개 제한. 유형·가입 자격을 나타내지 않음
기본값/생성식: GENERATED ALWAYS AS (IF(is_active=1,user_id,NULL)) STORED
role VARCHAR(20) N — 역할 - OWNER: 소유자, MEMBER: 참여자. 소유권은 이 값으로 표현(냉장고에 소유자 FK 중복 저장 안 함). CHECK (role IN ('OWNER','MEMBER'))
기본값/생성식: MEMBER
created_at DATETIME(6) N — 참여 시각
기본값/생성식: CURRENT_TIMESTAMP(6)
updated_at DATETIME(6) N — 최종 수정 일시
기본값/생성식: CURRENT_TIMESTAMP(6)

테이블 관계

관계 설명
users 1 : 0..N social_accounts 현재는 가입에 사용한 소셜 계정으로 로그인하며, 기존 회원에 대한 추가 소셜 계정 연결 기능은 제공하지 않는다.
데이터 모델은 향후 여러 제공자의 계정 연결을 수용할 수 있도록 구성한다.
users 1 : N refrigerator_members 회원은 여러 냉장고 참여 관계를 가질 수 있다.
회원은 개인 냉장고를 반드시 한 대 소유하므로 하나 이상의 냉장고 참여 관계를 가진다.
냉장고 1 : N refrigerator_members 냉장고에는 여러 회원이 참여할 수 있다.

회원과 냉장고의 다대다 관계를 refrigerator_members로 표현하며, 냉장고별 인가는 해당 관계의 role을 기준으로 판단한다.


인증 상태와 토큰 관리 정책

JWT의 무결성 검증에는 대칭키 기반의 HS256(HMAC-SHA256)을 사용한다.

RS256·ES256과 같은 공개키 방식은 토큰 발급 권한과 검증 권한을 분리할 때 유리하다. 현재 서비스는 동일한 백엔드 애플리케이션이 토큰 발급과 검증을 모두 수행하며, 모든 인스턴스가 발급 권한을 갖기 때문에 공개키 방식의 권한 분리 이점이 크지 않다.

따라서 키 관리가 단순하고 일반적으로 서명·검증 연산 비용이 낮은 HS256을 선택한다.

인증 토큰은 쿠키로 발급·전달하고 HttpOnly 설정을 통해 탈취 위험을 줄이고 CSRF 토큰을 사용해 CSRF 공격을 방지한다.

Access Token, Refresh Token 및 가입용 토큰은 HttpOnly 쿠키로 전달하여 페이지의 JavaScript가 토큰 값을 직접 읽지 못하도록 한다. 운영 환경에서는 Secure 속성을 적용하여 HTTPS 연결에서만 쿠키를 전송한다.

쿠키는 브라우저가 요청에 자동으로 포함하므로, 상태를 변경하는 요청에는 CSRF 토큰 검증을 적용한다. 회원가입 완료, 일반 로그인, 토큰 갱신 및 로그아웃도 보호 대상에 포함한다. 일반 서비스 API는 GET 요청으로 상태를 변경하지 않는다.

OAuth 콜백은 일반 API의 CSRF 토큰 대신 OAuth 요청에 연결된 state 등을 검증하여 보호한다.

Access Token — 검증 및 클레임

  • 검증: 허용된 서명 알고리즘(HS256), 서명, 토큰 용도(type=access), 만료 시각 및 필수 클레임
  • 클레임: userId, type, role, iat, exp
  • 만료 조건: 만료 시각 도달(15분)
  • 저장 위치: 서버에 별도로 저장하지 않는다.
  • 로그아웃이나 Refresh Token 교체 시 기존 Access Token을 서버에서 별도로 폐기하지 않는다.
  • 일반 서비스 API의 접근 허용 여부는 토큰 검증 후 현재 사용자 상태와 필요한 권한을 추가로 확인하여 판단한다.

Refresh Token — 검증 및 폐기 조건

  • 검증: 허용된 서명 알고리즘(HS256), 서명, 토큰 용도(type=refresh), 만료 시각, 필수 클레임, 저장소의 토큰 상태 및 현재 사용자 상태
  • 클레임: userId, type, iat, exp, sid, jti
  • jti: 로그인 및 갱신으로 새 Refresh Token을 발급할 때마다 새로운 UUID v4를 생성한다.
  • 만료·무효화 조건:
    • 만료 시각 도달(2일)
    • 갱신에 사용된 기존 Refresh Token의 교체
    • 현재 브라우저에서 로그아웃한 경우 해당 Refresh Token 무효화
    • 만료 전 사용 완료된 Refresh Token의 재사용이 탐지된 경우 해당 사용자의 모든 Refresh Token 무효화
    • 회원 탈퇴 시 해당 사용자의 모든 Refresh Token 무효화
  • 새로운 기기에서 로그인하더라도 기존 기기의 Refresh Token은 유지한다.

Refresh Token 저장 구조 및 상태 관리

Refresh Token의 상태는 단일 인스턴스에서는 Caffeine, 다중 인스턴스에서는 Redis에 저장한다.

저장 정보는 토큰별 정보와 사용자별 토큰 목록으로 구분한다.

1. 토큰별 정보

각 Refresh Token을 고유 식별자인 jti로 구분한다. jti는 토큰을 발급할 때마다 새로운 UUID v4로 생성한다.

항목 설명
jti Refresh Token의 고유 식별자
userId 토큰이 발급된 사용자 ID
sid 로그인 세션 고유 식별자
status ACTIVE, USED, REVOKED
expiresAt 해당 토큰의 원래 만료 시각

상태의 의미는 다음과 같다.

  • ACTIVE: 인증 갱신에 사용할 수 있는 토큰
  • USED: 인증 갱신에 이미 사용되어 교체된 토큰
  • REVOKED: 로그아웃 등의 사유로 명시적으로 무효화된 토큰

Refresh Token 원문은 저장하지 않는다. 서버는 요청에 포함된 JWT의 서명·용도·만료 등을 먼저 검증하고, 토큰별 정보를 조회하여 사용자 ID 일치 여부와 상태를 확인한다.

논리적 저장 형태

각 Refresh Token 기록에 해당 토큰이 속한 로그인 세션의 sid를 포함한다.

jti → { userId, sid, status, expiresAt }

2. 로그인 세션 정보

로그인마다 새로운 sid를 생성한다. 같은 세션에서 Refresh Token을 갱신할 때는 sid를 유지하고 jti만 교체한다.

sid → { userId, status, currentJti, expiresAt }
  • status: 세션 상태. ACTIVE 또는 REVOKED
  • currentJti: 해당 세션에서 현재 갱신에 사용할 수 있는 Refresh Token의 식별자
  • expiresAt: 세션 기록의 보관 종료 시각

로그아웃 시 해당 세션을 REVOKED로 변경한다. 연결된 토큰 기록이 ACTIVE이더라도 세션이 폐기되었으면 갱신을 허용하지 않는다.

3. 사용자별 세션 목록

여러 로그인 세션을 사용자 단위로 관리하기 위해, 사용자 ID별로 sid 목록을 저장한다. 이 목록은 회원 탈퇴 시 전체 세션 폐기와 기록 정리에 사용한다.

userId → Set<sid>

예를 들어 사용자 10에게 두 로그인 세션이 있고, 첫 번째 세션에서 토큰을 한 번 갱신했다면 다음과 같이 관리한다.

토큰별 정보
uuid-A → { userId: 10, sid: session-1, status: USED, expiresAt: ... }
uuid-B → { userId: 10, sid: session-1, status: ACTIVE, expiresAt: ... }
uuid-C → { userId: 10, sid: session-2, status: ACTIVE, expiresAt: ... }

세션별 정보
session-1 → { userId: 10, status: ACTIVE, currentJti: uuid-B, expiresAt: ... }
session-2 → { userId: 10, status: ACTIVE, currentJti: uuid-C, expiresAt: ... }

사용자별 세션 목록
10 → { session-1, session-2 }

uuid-A와 uuid-B는 같은 세션에서 갱신한 토큰이다. uuid-C는 별도의 로그인 세션에서 발급된 토큰이다.

session-1에서 로그아웃하면 session-1을 REVOKED로 변경한다. 이때 uuid-B도 더 이상 갱신에 사용할 수 없으며, session-2와 uuid-C는 유지한다.

4. 발급·갱신·무효화 처리

상황 토큰별 정보 처리 로그인 세션 처리 사용자별 세션 목록 처리
로그인 새 토큰을 ACTIVE로 저장 새 sid를 ACTIVE로 생성하고 currentJti 설정 새 sid 추가
정상 갱신 기존 토큰을 USED로 변경하고 새 토큰을 ACTIVE로 저장 sid 유지, currentJti 교체 및 보관 종료 시각 갱신 sid 유지. 필요한 경우 목록 보관 기간 연장
현재 브라우저 로그아웃 해당 세션의 ACTIVE 토큰을 REVOKED로 변경. 기존 USED 기록 유지 해당 sid만 REVOKED로 변경 보관 종료 시각까지 sid 유지
만료 전 USED 토큰의 갱신 재사용 해당 사용자의 모든 ACTIVE 토큰을 REVOKED로 변경. 기존 USED 기록 유지 해당 사용자의 모든 세션을 REVOKED로 변경 보관 중인 sid 목록 유지
회원 탈퇴 해당 사용자의 모든 ACTIVE 토큰을 REVOKED로 변경 해당 사용자의 모든 세션을 REVOKED로 변경 보관 종료 시각까지 유지
토큰 기록 만료 해당 jti 기록 제거 자체 보관 종료 시각까지 유지 변경 없음
세션 기록 만료 토큰별 만료 규칙에 따라 정리 해당 sid 기록 제거 해당 sid 정리
새 기기에서 로그인하더라도 기존 기기의 ACTIVE 토큰은 유지한다.

5. 만료 및 목록 정리

토큰별 정보는 해당 JWT의 원래 만료 시각까지 보관한다. 만료 검증에 시간 오차 허용값을 적용한다면 보관 기간에도 이를 반영한다.

토큰 상태가 USED 또는 REVOKED로 변경되어도 원래 만료 시각을 연장하지 않는다. 만료된 JWT는 갱신을 거절하며, 해당 요청만으로 사용자 전체 토큰을 추가로 무효화하지 않는다.

세션 기록의 보관 종료 시각은 해당 세션에서 발급된 토큰들의 보관 종료 시각 중 가장 늦은 시각으로 관리한다. 정상 갱신으로 필요한 보관 기간이 늘어나면 세션 기록의 보관 기간도 연장한다. REVOKED로 변경된 세션도 보관 종료 시각까지 폐기 상태를 유지한다.

사용자별 세션 목록에는 sid를 저장한다. 목록은 포함된 세션들의 보관 종료 시각 중 가장 늦은 시각까지 유지하며, 새 로그인이나 정상 갱신으로 필요한 경우 목록의 보관 기간을 연장한다.

개별 토큰 기록이 만료되어도 사용자별 세션 목록에서 sid를 제거하지 않는다. 세션 기록이 만료된 경우, 목록 조회·갱신 시 또는 주기적인 정리 작업에서 기록이 없는 sid를 제거한다. 목록이 비면 목록도 삭제한다.

Refresh Token 유효 기간 선택 근거

일 단위의 재방문을 고려하여 Refresh Token의 유효 기간을 2일로 설정한다. 정상 갱신 시 새 Refresh Token의 만료 시각을 발급 시점으로부터 2일 후로 설정하므로, 갱신을 지속하는 사용자는 로그인을 유지할 수 있다. 알림 수신 자체는 토큰 유효 기간을 연장하지 않는다.

registrationToken — 검증

  • 검증: 쿠키 존재 여부, 연결된 임시 가입 정보의 존재 여부, 만료 여부 및 사용 가능 상태
  • 사용 범위: 회원가입 완료 요청에만 사용하며, 일반 서비스 API의 인증 수단으로 허용하지 않는다.
  • 만료 조건: 만료 시각 도달(10분), 회원가입에 사용하여 회원 등록 성공

인증 플로우

v1 — 일반 로그인

sequenceDiagram
    participant B as 브라우저
    participant S as 백엔드
    participant D as DB
    participant C as 캐시

    B->>S: 아이디·비밀번호로 로그인 요청
    S->>S: CSRF 및 입력값 검증
    S->>D: 아이디로 회원 조회
    D-->>S: 조회 결과

    alt 회원 없음
        S-->>B: 가입 절차 필요 응답
        Note over B: 같은 화면에서 회원 도메인의 가입 절차 진행
    else 기존 회원
        S->>S: 비밀번호 및 ACTIVE 상태 확인
        alt 로그인 가능
            S->>C: Refresh Token을 ACTIVE로 저장
            C-->>S: 저장 완료
            S-->>B: 로그인 성공 및 인증 쿠키 설정
        else 로그인 불가
            S-->>B: 로그인 실패
        end
    end
Loading

v2 이후 — OAuth 로그인 및 가입 인증

sequenceDiagram
    participant B as 브라우저
    participant S as 백엔드
    participant O as OAuth 제공자
    participant D as DB
    participant C as 캐시

    B->>S: 소셜 로그인 시작 요청
    S->>S: state 등 OAuth 요청 정보 생성·보관
    S-->>B: OAuth 제공자로 리다이렉트
    B->>O: 인증 요청
    Note over B,O: 사용자 로그인 및 동의
    O-->>B: 인가 코드와 state를 포함한 콜백으로 리다이렉트
    B->>S: OAuth 콜백 요청
    S->>S: OAuth 요청 연결 검증
    S->>O: 인가 코드로 제공자 토큰 요청
    O-->>S: 제공자 토큰
    S->>O: 사용자 정보 요청
    O-->>S: 제공자 사용자 식별 정보
    S->>D: 소셜 식별자로 연결된 회원 조회
    D-->>S: 조회 결과

    alt 기존 ACTIVE 회원
        S->>C: Refresh Token을 ACTIVE로 저장
        C-->>S: 저장 완료
        S-->>B: 인증 쿠키 설정 및 서비스 화면으로 리다이렉트
    else 기존 WITHDRAWN 회원
        S-->>B: 로그인 차단 안내 화면으로 리다이렉트
    else 신규 사용자
        S->>C: registrationToken에 연결된 소셜 인증 정보 저장
        C-->>S: 저장 완료
        S-->>B: 가입용 쿠키 설정 및 가입 화면으로 리다이렉트
        Note over B: 이후 회원가입은 회원 도메인에서 처리
    end
Loading

인증 갱신

flowchart TD
    request["클라이언트: Refresh Token 쿠키와 CSRF 토큰으로 갱신 요청"]
    request --> check{"CSRF·JWT·사용자 상태 검증 성공?"}
    check -->|아니오| reject["갱신 거절"]
    check -->|예| state{"저장된 Refresh Token 상태"}

    state -->|ACTIVE| rotate["원자적 처리: 기존 토큰 USED 전환 및 새 토큰 ACTIVE 등록"]
    rotate --> success{"교체 성공?"}
    success -->|예| cookies["새 Access Token·Refresh Token 쿠키 발급"]
    success -->|아니오| conflict["상태 재확인 후 재사용·장애 정책 적용"]

    state -->|USED| revoke["해당 사용자의 모든 Refresh Token 무효화"]
    revoke --> reject
    state -->|REVOKED 또는 기록 없음| reject
Loading

요청 인증·인가 처리 구조

SecurityFilterChain

Spring Security의 SecurityFilterChain을 사용하여 CSRF 보호, 요청 인증, URL별 접근 제어 및 보안 오류 처리를 공통으로 적용한다.

일반 서비스 API는 인증된 ACTIVE 회원만 접근할 수 있다.

일반 서비스 API에는 Access Token 기반 인증을 적용한다.

로그인, OAuth 인증 시작·콜백, CSRF 토큰 발급, 회원가입 완료는 일반 API용 JwtAuthenticationFilter의 검증 대상에서 제외한다. 해당 경로는 각 절차에 필요한 가입용 토큰, Refresh Token 또는 OAuth 요청 연결 정보를 별도로 검증한다.

JWT 필터의 적용 범위는 경로별 필터 제외 조건 또는 SecurityFilterChain 분리로 명시한다. PUBLIC 접근 설정만으로 JWT 필터의 실행이 생략된다고 가정하지 않는다.

쿠키 기반 상태 변경 요청에는 CSRF 보호 정책을 적용한다. Access Token 인증 제외는 CSRF 검증이나 각 API의 자격 검증을 생략한다는 의미가 아니다.

Access Token 인증

JwtAuthenticationFilter는 일반 서비스 API 요청의 Access Token 쿠키를 추출하고, 별도의 인증 컴포넌트를 호출하여 필터 처리 단계에서 인증을 완료한다.

인증 컴포넌트는 토큰의 서명 알고리즘, 서명, 발급자, 토큰 용도, 만료 시각 및 필수 클레임을 검증한다. 이어 회원 도메인의 조회 기능을 통해 회원의 존재 여부, 현재 상태 및 서비스 역할을 확인한다.

필터는 인증 성공 시 사용자 정보와 권한을 담은 Authentication을 SecurityContext에 설정한 뒤 다음 필터로 요청을 전달한다. 비밀번호 해시, 토큰 원문 및 회원 엔티티 전체는 인증 주체 정보에 포함하지 않는다.

Access Token 쿠키가 전송되지 않으면 인증 정보를 설정하지 않고 다음 필터로 요청을 전달하며, 후속 인가 단계에서 인증 필요 여부를 판단한다. 쿠키가 만료되어 전송되지 않은 경우도 동일하게 처리한다.

전달된 Access Token이 만료되었거나 유효하지 않으면 인증 컴포넌트는 Spring Security에 의존하지 않는 자체 인증 예외를 발생시킨다.

JwtAuthenticationFilter는 해당 예외를 잡아 SecurityContext를 비우고, AuthenticationException 계열의 예외로 변환하여 AuthenticationEntryPoint에 전달한다.

AuthenticationEntryPoint는 401 응답을 반환하며, 필터는 다음 필터로 요청을 전달하지 않고 처리를 종료한다.

인증 정보의 전달

Controller는 @AuthenticationPrincipal 등을 사용하여 인증 주체 정보를 전달받고, 업무 처리에 필요한 사용자 ID를 Service에 명시적으로 전달한다.

요청자 식별에는 인증 정보에서 추출한 사용자 ID를 사용한다. 요청 본문이나 경로에 포함된 대상 회원 ID를 요청자의 신원으로 사용하지 않는다.

Service는 SecurityContextHolder나 HTTP 요청 객체에 직접 의존하지 않고, 전달받은 요청자 ID와 업무 입력값을 사용한다.

SecurityContext의 수명

일반 API의 인증 정보는 현재 요청 처리에만 사용하며, 다음 요청의 인증을 위해 HTTP 세션에 저장하지 않는다. 이후 요청은 전달된 Access Token으로 다시 인증한다.

일반 API 인증은 STATELESS 정책으로 구성한다. OAuth 인증 과정의 임시 요청 정보와 Refresh Token·가입용 토큰의 저장은 별도 정책에 따른다.

sequenceDiagram
    participant client as 클라이언트
    participant filter as JwtAuthenticationFilter
    participant auth as 인증 컴포넌트
    participant member as 회원 도메인 조회
    participant context as SecurityContext
    participant entry as AuthenticationEntryPoint
    participant chain as 후속 필터 체인

    client->>filter: 일반 서비스 API 요청
    filter->>filter: Access Token 쿠키 추출

    alt Access Token 쿠키 없음
        filter->>chain: 인증 정보 없이 요청 전달
        chain->>chain: 인가 단계에서 인증 필요 여부 판단
    else Access Token 쿠키 있음
        filter->>auth: Access Token 인증 요청
        auth->>auth: 서명 알고리즘, 서명, 발급자, 용도, 만료, 필수 클레임 검증

        alt 토큰 만료 또는 유효하지 않음
            auth-->>filter: 자체 인증 예외 발생
            filter->>context: 인증 정보 초기화
            filter->>filter: AuthenticationException 계열로 변환
            filter->>entry: commence 호출
            entry-->>client: 401 응답
            filter->>filter: 요청 처리 종료
        else 토큰 검증 성공
            auth->>member: 회원 조회
            member-->>auth: 회원 정보 반환
            auth->>auth: 회원 존재 여부, 현재 상태, 서비스 역할 확인

            alt 회원 확인 결과 인증 불가
                auth-->>filter: 자체 인증 예외 발생
                filter->>context: 인증 정보 초기화
                filter->>filter: AuthenticationException 계열로 변환
                filter->>entry: commence 호출
                entry-->>client: 401 응답
                filter->>filter: 요청 처리 종료
            else 인증 성공
                auth-->>filter: 사용자 정보와 권한 반환
                filter->>filter: Authentication 생성
                filter->>context: Authentication 설정
                filter->>chain: 요청 전달
                chain->>chain: 인가 단계에서 접근 허용 여부 판단
            end
        end
    end
Loading

냉장고별 인가

냉장고 관련 Service는 전달받은 요청자 ID와 업무 대상 ID를 이용하여, 업무 처리 전에 ‘인가 정책과 판단 흐름’에 정의된 참여 관계·냉장고 선택 상태·역할·대상 소속 조건을 확인한다.

요청 본문이나 경로에 포함된 대상 회원 ID를 요청자의 신원으로 사용하지 않는다. 권한 확인과 데이터 변경 사이의 관계·역할 변경은 냉장고 도메인의 트랜잭션 및 동시성 정책에 따라 처리한다.

구체적인 기능별 권한표와 접근 조건은 ‘인가 정책과 판단 흐름’에서 단일 기준으로 관리한다.


인가 정책과 판단 흐름

일반 서비스 API

일반 서비스 API는 유효한 Access Token을 요구한다. 서버는 토큰 검증 후 DB에서 현재 사용자 상태를 확인하며, ACTIVE 사용자만 접근을 허용한다.

냉장고 관련 API

냉장고 관련 API는 요청 대상에 대한 사용자의 참여 관계와 역할을 기준으로 권한을 판단한다.

재고 조회·등록·수정·삭제, 초대코드 발급 및 참여자 내보내기는 요청자의 refrigerator_members 관계가 존재하고 is_active=1인 냉장고에 대해서만 허용한다.

기능 OWNER MEMBER
재고 조회·등록·수정·삭제 허용 허용
초대코드 발급 허용 불가
MEMBER 내보내기 허용 불가

재고 ID나 냉장고 ID로 요청하는 경우, 해당 대상이 권한을 확인한 냉장고에 속하는지도 검증한다. 참여자 내보내기는 같은 냉장고의 MEMBER만 대상으로 허용한다.

냉장고 목록 조회·전환·공유 참여·탈퇴·소유권 이전에는 각 기능의 별도 접근 조건을 적용한다. 구체적인 조건과 상태 변경 규칙은 냉장고 도메인 테크 스펙에서 정의한다.

인증 관련 API

일반 서비스 API와 달리 인증 관련 API에는 목적에 맞는 인증 수단을 적용한다.

  • 로그인 및 OAuth 인증 시작: Access Token 없이 요청할 수 있다.
  • OAuth 회원가입 완료: registrationToken으로 가입 자격을 검증한다.
  • 토큰 갱신: Refresh Token으로 검증하며, 유효한 Access Token을 요구하지 않는다.
  • 로그아웃: Access Token이 만료된 상태면 갱신 후 처리할 수 있도록 한다.

쿠키 기반 상태 변경 요청에는 별도의 CSRF 보호 정책을 적용한다. OAuth 콜백은 OAuth 요청 연결 검증을 적용한다.


API 명세

인증 API 명세

일반 로그인 API

API 명세

POST /api/auth/sessions

권한

모든 사용자(로그인/비로그인 상태 무관하게 요청할 수 있음)

구현 상세

요청 (Request Body)

항목 설명
loginId 로그인용 ID. 2~10자의 영문·숫자 조합. 앞뒤 공백을 제거한 후 검증한다. 내부 공백은 허용하지 않는다. 회원가입과 로그인에 동일한 규칙을 적용한다.
password 비밀번호. 최소 8자 이상이며, 영문과 숫자를 최소 1자 이상 포함한다.

응답

헤더

Location: {FRONTEND_URL}/
Set-Cookie: accessToken, refreshToken

본문

{
  "code": "AUTH-200-001",
  "message": "로그인 성공",
  "data": null
}

처리 로직

  1. loginId를 기준으로 회원 정보를 조회한다.
  2. 조회한 회원 정보를 이용해 PasswordEncoder.matches(입력 비밀번호, 저장 해시)로 검증한다.
    • 일치하면 accessToken과 refreshToken을 Set-Cookie를 통해 발급한다.
      • 발급한 refreshToken에 대한 정보를 인메모리 캐시 혹은 Redis에 저장한다.
    • 일치하지 않는 경우 401 응답 코드로 아이디·비밀번호가 틀렸음을 알린다.
    • 회원 정보가 존재하지 않는 경우 404로 응답하여 회원가입을 유도한다.
      • v1은 로그인과 회원가입을 동일 화면에서 제공한다. 입력한 ID에 해당하는 회원이 없으면 404를 반환하고, 클라이언트는 회원 도메인의 가입 절차로 이어간다. 기존 회원의 비밀번호가 일치하지 않으면 401을 반환한다. 이 흐름은 v1에 한정한다.
    • 입력 형식이 잘못된 경우는 400으로 응답한다.

OAuth 인증 시작

API 명세

GET /api/auth/oauth/{provider}

권한

모든 사용자(로그인/비로그인 상태 무관하게 요청할 수 있음)

구현 상세

요청

  • Path-variable: kakao, google

응답

헤더

Location: {OAuth 인증 경로}

처리 로직

  • 302 응답으로 OAuth 로그인 화면으로 이동시킨다.

OAuth 로그인 콜백 API

API 명세

GET /login/oauth2/code/{provider}

권한

모든 사용자(로그인/비로그인 상태 무관하게 요청할 수 있음)

구현 상세

요청

  • Path-variable: kakao, google
  • Query: code, state

응답

헤더

Location: {FRONTEND_URL}/
Set-Cookie: accessToken, refreshToken

처리 로직

  • 사용자가 OAuth 로그인 성공 시 등록된 백엔드 콜백 URL로 GET 요청
  • 백엔드는 요청 URL의 Query로 전달받은 code를 사용해 OAuth 토큰 발급 요청
  • 발급받은 OAuth 토큰을 사용해 사용자 정보 조회: provider_user_id, email
  • {provider, provider_user_id}의 조합으로 가입된 소셜 계정 조회
    • 존재하면 인증 토큰 발급
    • 없으면 회원가입에 필요한 데이터 저장 후 해당 데이터와 매핑되는 registrationToken을 쿠키로 발급
  • 로그인 취소나 실패한 경우 redirect URL의 error 쿼리로 실패 사유 전달

토큰 갱신 API

API 명세

POST /api/auth/token-renewals

권한

모든 사용자(로그인/비로그인 상태 무관하게 요청할 수 있음)

구현 상세

요청

  • refreshToken 쿠키 필수

응답

Set-Cookie: accessToken, refreshToken

처리 로직

  • 쿠키로 전달된 refreshToken을 추출 및 만료 검사
  • 만료되지 않았다면 jti를 이용해 저장소에서 refreshToken 조회
  • 존재 여부 검사 성공했다면 새로운 accessToken, refreshToken 생성, 기존 refreshToken 사용 처리를 원자적으로 처리
    • 존재하지 않는 경우, 만료된 경우, 유효하지 않은 경우 401 응답을 반환한다.
  • 새롭게 생성한 accessToken, refreshToken을 쿠키로 전달

로그아웃 API

API 명세

DELETE /api/auth/sessions

권한

인증 상태인 사용자(로그인한 사용자)

구현 상세

요청

  • accessToken 필수
  • refreshToken 쿠키 필수

응답

본문

{
  "code": "AUTH-200-005",
  "message": "로그아웃 성공",
  "data": null
}

처리 로직

  • 쿠키로 전달된 accessToken, refreshToken을 추출 및 만료 검사
  • 만료되지 않았다면 jti를 이용해 저장소에서 refreshToken 조회 및 제거
  • 요청에 포함된 Refresh Token을 무효화한다. sid를 통해 후속 토큰이 생기지 않도록 한다. 프론트엔드는 정상 흐름에서 갱신 완료 후 로그아웃을 요청한다.

동시 요청과 실패·재시도 처리

실행 환경과 갱신 요청 조정

본 설계는 단일 앱 실행 컨텍스트를 기준으로 한다. 여러 브라우저 탭 간 갱신 동기화는 이번 범위에 포함하지 않는다.

프론트엔드는 공통 API 호출 계층에서 하나의 갱신 요청만 실행하고 그 결과를 공유한다. 갱신 중 발생한 다른 갱신 대상 인증 오류는 별도의 갱신 요청을 만들지 않고 진행 중인 결과를 기다린다.

일반 API의 인증 실패와 재시도

일반 서비스 API가 Access Token 누락·만료 등 갱신 가능한 인증 오류를 반환하면 토큰 갱신을 요청한다. 사용자 상태에 따른 접근 차단이나 권한 부족은 자동 갱신 대상에 포함하지 않는다.

갱신에 성공하면 인증 실패로 거절된 원래 요청을 최대 한 번 재시도한다. 요청을 보낸 이후 이미 갱신이 완료된 경우에는 늦게 도착한 인증 오류에 대해 추가 갱신 없이 원래 요청을 최대 한 번 재시도한다.

갱신 API 자체와 재시도한 요청에는 자동 갱신을 반복 적용하지 않는다.

서버는 인증 실패로 거절하는 일반 API 요청에 대해 업무 처리를 수행하지 않는다. 네트워크 오류 등으로 업무 처리 여부가 불명확한 요청의 재시도는 각 도메인의 중복 처리 방지 정책을 따른다.

갱신 실패와 응답 유실

  • 갱신 API가 401을 반환하면 자동 갱신을 중단하고 재로그인을 안내한다.
  • CSRF 검증 실패인 403은 CSRF 오류로 처리하며 자동 갱신을 반복하지 않는다.
  • 5xx 또는 네트워크 오류는 일시적인 실패로 안내한다.
  • 갱신 결과가 불명확하면 자동 갱신을 중단한다. 이후 일반 API의 인증 오류에서도 기존 Refresh Token을 자동으로 다시 제출하지 않는다.
  • 인증 상태를 복구할 수 없는 경우 재로그인으로 복구한다.

로그아웃과 갱신의 순서

프론트엔드는 로그아웃 시작 시 새로운 갱신 요청을 차단한다. 진행 중인 갱신이 있으면 완료를 기다린다. 갱신이 성공하면 브라우저에 반영된 최신 Refresh Token으로 로그아웃을 요청한다.

서버는 로그아웃 요청에 포함된 Refresh Token을 무효화하고 인증 쿠키를 삭제한다. 다른 Refresh Token은 유지한다.

갱신 응답 유실로 최신 Refresh Token을 확보하지 못한 경우에도, 서버는 로그아웃 요청에 포함된 이전 Refresh Token의 서명·용도·만료와 저장된 토큰 정보의 일치 여부를 검증한 뒤 해당 sid의 로그인 세션을 REVOKED로 변경한다. 이때 갱신에 이미 사용된 USED 토큰도 해당 세션의 로그아웃에 사용할 수 있다.

세션이 폐기되면 같은 sid로 이미 발급된 후속 Refresh Token도 인증 갱신에 사용할 수 없다. 다른 로그인 세션은 유지한다.

인증 갱신의 세션 상태 확인·토큰 교체와 로그아웃의 세션 폐기는 서로 경쟁하더라도 폐기된 세션이 다시 활성화되지 않도록 원자적으로 처리한다. 갱신이 먼저 완료되면 로그아웃이 해당 세션과 후속 토큰의 사용을 차단하고, 로그아웃이 먼저 완료되면 이후 갱신을 거절한다.

서버의 세션 폐기가 완료된 경우에만 로그아웃 성공을 응답한다. 저장소 장애나 요청 미도달로 처리가 완료되지 않은 경우에는 서버 측 무효화를 보장하지 않는다.

이미 발급된 Access Token은 기존 정책에 따라 만료 시각까지 유효하다.

회원 탈퇴와의 연동

WITHDRAWN 사용자의 로그인, 인증 갱신 및 일반 서비스 API 접근을 차단한다. 회원 탈퇴 시 해당 사용자의 모든 ACTIVE Refresh Token을 무효화한다.

회원 상태 변경과 토큰 발급·갱신 사이의 동시 처리 및 토큰 저장소 무효화 실패의 복구 절차는 회원 도메인과의 연동 규칙으로 정의한다.


검증 시나리오

시나리오 사전 상태·요청 기대 결과
정상 인증 갱신 ACTIVE 사용자가 ACTIVE Refresh Token 제출 200 및 새 인증 쿠키 발급. 기존 토큰은 USED, 새 토큰은 ACTIVE이며 사용자별 목록에 모두 포함
Refresh Token 누락·무효 쿠키 누락, 잘못된 형식, 서명 오류 또는 만료 401. 새 토큰 발급 없음
폐기되었거나 기록이 없는 토큰 REVOKED 또는 저장 기록이 없는 토큰 제출 401. JWT만으로 ACTIVE 기록을 복원하지 않음
사용 완료된 토큰 재사용 만료 전 USED 토큰 제출 401. 해당 사용자의 모든 ACTIVE Refresh Token 무효화. 다른 사용자의 토큰은 유지
여러 일반 API의 동시 401 한 화면에서 여러 요청이 동시에 인증 실패 프론트엔드의 갱신 요청은 한 번만 실행. 성공 후 각 원래 요청을 최대 한 번 재시도
동일 토큰의 서버 동시 갱신 동일 ACTIVE 토큰으로 갱신 요청 두 개 전송 교체는 최대 한 번만 성공. 후속 요청은 재사용으로 처리되어 새 토큰을 포함한 사용자 전체 ACTIVE 토큰이 무효화
늦게 도착한 401 다른 요청의 갱신 완료 후 이전 요청의 401 수신 불필요한 추가 갱신 없이 원래 요청을 최대 한 번 재시도
갱신 성공 후 응답 유실 서버는 토큰 교체를 완료했으나 브라우저가 새 쿠키를 받지 못함 자동 갱신 재시도 없음. 기존 토큰을 다시 제출하면 재사용 정책 적용
저장소 장애 캐시 접근 또는 갱신 처리 실패 5xx. 상태 저장 성공이 확인되지 않은 새 토큰을 성공 응답으로 전달하지 않음
CSRF 검증 실패 유효한 Refresh Token과 누락·잘못된 CSRF 토큰 제출 403. Refresh Token 상태 변경 없음
로그아웃과 갱신의 동시 실행 같은 브라우저에서 두 요청이 겹침 로그아웃 대상의 유효한 후속 Refresh Token이 남지 않음. 다른 기기 토큰은 유지
탈퇴 사용자 접근 WITHDRAWN 사용자가 갱신 또는 일반 API 요청 갱신·업무 처리 거절
OAuth 가입용 토큰 검증 만료·사용 완료·존재하지 않는 registrationToken 제출 가입 자격 검증 실패. 회원 도메인에 유효한 가입 인증 결과를 제공하지 않음
⚠️ **GitHub.com Fallback** ⚠️