---
template: policy-doc@1
doc_id: 01-authn
title: 인증 — 표면 2종의 세션 수립 · 만료 · 갱신 · 실패 코드 · 계정 상태 차단
config: ../_meta/template-config.yaml
last_verified: 4d798f55
policies:
  - id: SESSION_ESTABLISHMENT
    status: active-prod
    evidence: [S-guard, S-contract]
    limitations:
      - 인증 표면이 둘이라는 것까지가 이 문서의 사정거리다. 표면마다 열린 로그인 입구의 전수는 여기서 열거하지 않는다
      - 토큰 안에 무엇이 실리는가는 인가 층 소관이며 이 문서는 그것을 판정하지 않는다
    anchors:
      - insuniverse-server/apps/api/src/helpers/cookie.helper.ts:3
      - insuniverse-server/apps/api/src/helpers/cookie.helper.ts:38
      - insuniverse-server/apps/api/src/modules/auth/user/jwt.guard.ts:38
      - insuniverse-server/apps/api/src/modules/auth/user/auth.controller.ts:63
      - insuniverse-server/apps/api/src/modules/auth/admin/jwt.guard.ts:26
      - insuniverse-server/apps/api/src/modules/auth/admin/auth.controller.ts:28
      - insuniverse-server/apps/api/src/modules/auth/admin/auth.controller.ts:55
      - insuniverse-server/apps/api/src/modules/auth/admin/auth.controller.ts:75
      - insuniverse-server/apps/api/src/modules/auth/admin/auth.controller.ts:96
      - insuniverse-site/front/src/shared/api/index.ts:68
      - insuniverse-admin/src/shared/api/index.ts:12
  - id: SESSION_LIFETIME
    status: active-prod
    evidence: [S-constant, S-component]
    limitations:
      - 이 정책이 여는 것은 저장소에 박힌 두 자리 계열뿐이다. 토큰 자체의 만료는 이 문서 부록이 따로 다룬다
      - 두 자리 계열이 같은 값인 것이 의도인지 우연인지는 코드가 말하지 않는다. 서로를 참조하는 자리가 없다는 사실까지가 관측이다
    anchors:
      - insuniverse-server/apps/api/src/helpers/cookie.helper.ts:25
      - insuniverse-server/apps/api/src/modules/auth/admin/auth.controller.ts:31
      - insuniverse-server/apps/api/src/modules/auth/admin/auth.controller.ts:58
      - insuniverse-server/apps/api/src/modules/auth/admin/auth.controller.ts:78
      - insuniverse-server/apps/api/src/modules/auth/admin/auth.controller.ts:99
      - insuniverse-site/front/src/entities/user/model/store.ts:18
      - insuniverse-site/front/src/entities/user/model/store.ts:30
  - id: SESSION_RENEWAL
    status: active-prod
    evidence: [S-contract, S-guard]
    limitations:
      - 갱신이 실제로 몇 번까지 이어지는가는 토큰 만료값에 달려 있고 그 값은 저장소에서 판정되지 않는다
      - 저장된 갱신 토큰이 언제 지워지는가는 이 문서가 여는 자리까지만 관측했다. 그 밖의 삭제 경로가 있는지는 미검증이다
    anchors:
      - insuniverse-server/apps/api/src/modules/auth/user/auth.service.ts:315
      - insuniverse-server/apps/api/src/modules/auth/user/auth.service.ts:321
      - insuniverse-server/apps/api/src/modules/auth/user/auth.service.ts:613
      - insuniverse-server/apps/api/src/modules/auth/user/auth.controller.ts:143
      - insuniverse-server/apps/api/src/modules/auth/user/auth.service.ts:658
      - insuniverse-server/apps/api/src/modules/auth/admin/auth.service.ts:49
      - insuniverse-server/apps/api/src/modules/auth/admin/auth.service.ts:84
      - insuniverse-server/apps/api/src/modules/auth/admin/auth.service.ts:123
      - insuniverse-server/apps/api/src/modules/auth/admin/auth.service.ts:180
      - insuniverse-server/drizzle/schema.ts:1500
  - id: AUTH_FAILURE
    status: active-prod
    evidence: [S-guard, S-constant]
    limitations:
      - 두 코드값이 서로 다른 뜻인가는 기계가 판정하지 못한다. 이 문서는 값이 갈린다는 사실까지만 쓴다
      - 인증 층 밖에서 같은 응답 코드를 쓰는 자리가 더 있는지는 이 문서가 세지 않는다
    anchors:
      - insuniverse-server/apps/api/src/constants/api.constant.ts:16
      - insuniverse-server/apps/api/src/constants/admin.constant.ts:58
      - insuniverse-server/apps/api/src/modules/auth/user/jwt.guard.ts:5
      - insuniverse-server/apps/api/src/modules/auth/admin/jwt.guard.ts:1
      - insuniverse-server/apps/api/src/modules/auth/user/jwt.guard.ts:72
      - insuniverse-server/apps/api/src/modules/auth/admin/jwt.guard.ts:47
  - id: ACCOUNT_STATE_REVOCATION
    status: active-prod
    evidence: [S-guard, S-schema]
    limitations:
      - 관리자 콘솔 표면에는 같은 재검증이 없다. 그 표면의 계정 상태 판정은 이 문서가 열지 않는다
      - 두 상태 칸이 언제 켜지는가는 운영 데이터이며 저장소에서 판정되지 않는다
    anchors:
      - insuniverse-server/apps/api/src/modules/auth/user/jwt.guard.ts:89
      - insuniverse-server/apps/api/src/modules/auth/user/jwt.guard.ts:93
      - insuniverse-server/apps/api/src/modules/auth/user/auth.service.ts:289
      - insuniverse-server/apps/api/src/modules/auth/user/auth.service.ts:295
      - insuniverse-server/drizzle/schema.ts:403
      - insuniverse-server/drizzle/schema.ts:1496
  - id: TOKEN_CLIENT_COPY
    status: active-prod
    evidence: [S-component, A-comment]
    limitations:
      - 저장 사본이 실제로 어느 기기에서 쓰이는가는 런타임 관측이 필요하고 이 문서는 하지 않았다
      - 관리자 콘솔 표면에는 같은 사본이 없다. 그 표면이 쿠키만 쓰는 것까지가 관측이다
      - 저장 사본의 복호화 키는 화면 코드가 환경변수에서 읽고 하드코딩 폴백 값을 갖는다. 그 보관이 어느 수준의 보호인지는 이 문서가 판정하지 않는다
    anchors:
      - insuniverse-server/apps/api/src/modules/auth/user/auth.controller.ts:77
      - insuniverse-site/front/src/entities/user/model/store.ts:41
      - insuniverse-site/front/src/entities/user/model/store.ts:63
      - insuniverse-site/front/src/shared/api/index.ts:34
      - insuniverse-site/front/src/shared/api/index.ts:70
  - id: TOKEN_EXPIRY_CONFIG
    status: conditional-unknown
    evidence: [S-constant, A-comment]
    unknown_reason: 세 만료값이 전부 배포 환경변수에서 주입되고 저장소에는 주입 지점만 있다. 운영에 실린 값이 무엇인지 이 저장소에서 판정되지 않는다
    limitations:
      - 값이 비어 있을 때 서명이 어떻게 동작하는지는 이 문서가 관측하지 않았다
    anchors:
      - insuniverse-server/apps/api/src/config/configuration.ts:87
      - insuniverse-server/apps/api/src/config/configuration.ts:89
      - insuniverse-server/apps/api/src/config/configuration.ts:90
  - id: NONPROD_AUTH_BYPASS
    status: conditional-unknown
    evidence: [S-guard, A-comment]
    unknown_reason: 두 우회의 활성 여부가 같은 배포 환경변수의 값에 걸려 있고 그 값이 저장소에서 판정되지 않는다. 두 우회가 그 변수에 대해 서로 다른 값을 조건으로 삼는다
    limitations:
      - 우회가 지금 켜져 있는지 꺼져 있는지를 이 문서는 판정하지 않는다. 조건과 자리까지가 사정거리다
    anchors:
      - insuniverse-server/apps/api/src/modules/auth/user/jwt.guard.ts:44
      - insuniverse-server/apps/api/src/modules/auth/user/jwt.guard.ts:65
      - insuniverse-server/apps/api/src/modules/auth/admin/jwt.guard.ts:42
      - insuniverse-server/apps/api/src/helpers/request.helper.ts:20
      - insuniverse-server/apps/api/src/helpers/cookie.helper.ts:12
---

# 인증 — 표면 2종의 세션 수립 · 만료 · 갱신 · 실패 코드 · 계정 상태 차단

이 문서는 **로그인한 뒤 무엇으로 인증이 유지되고 언제 끊기며 어떻게 이어지는가**를 정한다. 인증 표면이 둘이고 두 표면이 만료·갱신·실패 코드에서 각각 다르다는 것이 이 문서의 핵심이며, 앱 환경에서만 달라지는 것들은 별도 절에 모았다.

**여기는 "누구인지 확인하는 층" 만 쓴다.** 확인에 실패하면 차단이고 그 응답은 401 이다. 확인된 주체가 무엇을 할 수 있는가 — 계정 종류·쓰기 범위·기업 노출 등급·메뉴 노출 — 는 다른 층이고 그 응답은 403 이며, `02-authz-roles.md` 부터가 그 소관이다. 층↔문서 대응의 정본은 `../_meta/template-config.yaml` 의 `auth_layers` 다.

응답에 실리는 코드값의 체계는 여기 없다 — `../01-foundation/03-error-codes.md` 소관이며 이 문서는 그 규칙이 인증 축에서 나타난 형태 하나를 자세히 쓴다.

## 정책 (기획)

### SESSION_ESTABLISHMENT

**인증 표면이 둘이고 두 표면은 서로 다른 인증 쿠키를 읽는다.** 이용자 표면과 관리자 콘솔 표면은 로그인 성공 시 각각 자기 쿠키를 심고, 이후 요청에서 자기 쿠키만 읽는다. 한 표면에서 받은 세션이 다른 표면에서 통하지 않는다.

**두 표면은 후보를 받는 방식도 다르다.** 이용자 표면은 인증 쿠키가 여러 개 실려 와도 전부 후보로 받고 그중 검증에 성공하는 것을 쓰며, 요청 헤더로 실려 온 토큰도 같은 후보 목록에 넣는다. 옛 로그인의 만료 쿠키가 지워지지 않은 채 새 쿠키와 함께 전송되던 상황이 그 설계의 이유다. 관리자 콘솔 표면은 자기 쿠키 하나만 읽고 후보라는 개념이 없다.

**관리자 콘솔 표면의 로그인 입구는 하나가 아니다.** 운영 관리자·영업대리점·기업 최고관리자·기업 조직관리자가 각각 다른 입구로 들어오며, 네 입구가 모두 같은 쿠키 하나에 세션을 싣는다. 그래서 "관리자 콘솔에 로그인했다" 는 진술만으로는 어느 입구로 들어왔는지 정해지지 않는다.

### SESSION_LIFETIME

**만료는 한 곳에 있지 않다.** 저장소에 박혀 있는 만료는 두 계열이고, 그 둘은 서로를 참조하지 않는다.

| 계열 | 무엇의 수명인가 | 어디가 정하는가 |
|---|---|---|
| 쿠키 수명 | 브라우저가 인증 쿠키를 들고 있는 기간 | 서버가 쿠키를 심을 때 함께 지정한다 |
| 클라이언트 저장 만료 | 이용자 화면이 로그인 상태를 기억하는 기간 | 화면이 로그인 시각에 더해 저장한다 |

두 계열의 값은 같다. **그러나 같은 상수를 나눠 쓰는 것이 아니라 각자 자기 자리에 따로 박혀 있다** — 한쪽을 고쳐도 다른 쪽은 따라오지 않는다.

**세 번째 만료는 여기 없다.** 토큰 자체의 유효기간은 배포 환경에서 주입되며 저장소에서 판정되지 않는다. 그래서 그 축은 이 문서 부록으로 격리했고, 실제로 이용자가 겪는 만료는 세 값 중 가장 짧은 것이 정한다는 사실만 여기 남긴다.

### SESSION_RENEWAL

**갱신 경로는 앱 환경 로그인에만 있다.** 로그인이 앱 환경에서 이루어졌을 때만 갱신 토큰이 발급되고 서버에 저장된다. 웹 브라우저 로그인과 관리자 콘솔 로그인에는 갱신 토큰이 발급되지 않으며, **그 두 경우에는 갱신 경로 자체가 없다.**

**갱신은 둘을 함께 바꾼다.** 갱신 요청이 성공하면 접근 토큰과 갱신 토큰이 모두 새로 발급되고, 서버에 저장된 갱신 토큰도 새 값으로 덮인다. 이전 갱신 토큰은 저장 자리에서 사라진다.

**갱신 경로가 없는 표면에서는 만료 뒤에 다시 로그인한다.** 이것이 관리자 콘솔 이용자가 하루에 한 번 다시 로그인하게 되는 이유이며, 결함이 아니라 그 표면에 갱신 설계가 없다는 사실의 귀결이다.

### AUTH_FAILURE

**인증에 실패하면 두 표면 모두 401 로 차단한다.** 응답 상태는 같다.

**그러나 응답에 실리는 코드값은 표면마다 다르다.** 두 값은 서로 다른 상수 표에 **같은 이름**으로 선언돼 있고 값만 갈린다. 그래서 코드값을 보고 "인증 실패" 임은 알 수 있어도, 이름만 보고 어느 표면의 실패인지는 정해지지 않는다. 이것이 `../01-foundation/03-error-codes.md` 가 쓴 **코드값은 조회 키가 아니다** 라는 규칙의 인증 축 사례다.

실패 판정의 입력은 표면마다 다르다 — 이용자 표면은 후보를 전부 시도한 뒤에도 성공한 것이 없을 때 실패로 보고, 관리자 콘솔 표면은 자기 쿠키 하나의 검증이 실패하면 그대로 실패다.

### ACCOUNT_STATE_REVOCATION

**토큰이 유효해도 계정 상태로 차단된다.** 이용자 표면은 토큰 검증을 통과한 뒤에도 기업 회원에 대해 계정 상태를 다시 확인하고, 소속 기업이 정지 상태이거나 본인이 탈퇴 상태이면 인증 층에서 막는다.

**재확인은 로그인 시점에도 한 번 일어난다.** 로그인 자체가 같은 두 상태를 보고 거부하므로, 세션이 살아 있는 동안 상태가 바뀐 경우를 요청 시점 재확인이 받는다.

**이 차단은 인가가 아니라 인증 층이다.** 무엇을 할 수 있는가를 따지기 전에 "이 사람이 지금도 이용 가능한 계정인가" 를 보는 것이며, 그래서 응답도 401 계열이다.

### TOKEN_CLIENT_COPY

**이용자 표면은 접근 토큰의 사본을 응답 본문으로도 내려준다.** 쿠키만으로 충분하지 않은 환경이 있기 때문이다 — 일부 앱 웹뷰가 다른 출처로 가는 요청에 쿠키를 싣지 않아 로그인 직후 인증이 깨지는 사례가 있었고, 사본은 그 경우의 대체 경로다.

**화면은 그 사본을 브라우저 저장소에 암호화해 보관하고, 요청마다 헤더로 함께 보낸다.** 서버가 쿠키와 헤더를 같은 후보 목록으로 받기 때문에 쿠키가 정상인 환경에서는 동작이 달라지지 않는다.

**그 암호화의 키는 화면 코드 안에 있다.** 환경변수에서 읽고 하드코딩 폴백 값을 가지므로 저장 사본을 읽는 쪽이 키도 함께 갖는다. 이 문서는 그 보관이 어느 수준의 보호인지 판정하지 않는다 — 자리는 `TOKEN_CLIENT_COPY` 의 저장·판독 두 행이 연다.

**관리자 콘솔 표면에는 이 사본이 없다.** 그 표면은 쿠키만으로 인증하며, 저장 사본도 헤더 전송도 두지 않는다.

## 강제 지점 (개발)

### SESSION_ESTABLISHMENT

- **이용자 표면 쿠키 이름과 후보 추출** — 쿠키 이름 상수와 후보 추출 헬퍼가 한 파일에 있다. `insuniverse-server/apps/api/src/helpers/cookie.helper.ts:3` · `insuniverse-server/apps/api/src/helpers/cookie.helper.ts:38`
- **이용자 표면 가드의 후보 소비** — `insuniverse-server/apps/api/src/modules/auth/user/jwt.guard.ts:38`
- **이용자 표면 쿠키 심기** — `insuniverse-server/apps/api/src/modules/auth/user/auth.controller.ts:63`
- **관리자 콘솔 표면 가드의 단일 쿠키 판독** — `insuniverse-server/apps/api/src/modules/auth/admin/jwt.guard.ts:26`
- **관리자 콘솔 로그인 4입구의 쿠키 심기** — `insuniverse-server/apps/api/src/modules/auth/admin/auth.controller.ts:28` · `insuniverse-server/apps/api/src/modules/auth/admin/auth.controller.ts:55` · `insuniverse-server/apps/api/src/modules/auth/admin/auth.controller.ts:75` · `insuniverse-server/apps/api/src/modules/auth/admin/auth.controller.ts:96`
- **클라이언트 전송 방식** — 이용자 화면은 저장 토큰을 우선 얹고 쿠키를 폴백으로 삼는 요청 인터셉터를 두고, 관리자 콘솔 화면은 자격 증명 동봉 옵션만 둔다. `insuniverse-site/front/src/shared/api/index.ts:68` · `insuniverse-admin/src/shared/api/index.ts:12`

### SESSION_LIFETIME

- **쿠키 수명 — 이용자 표면 공통 옵션** — `insuniverse-server/apps/api/src/helpers/cookie.helper.ts:25`
- **쿠키 수명 — 관리자 콘솔 로그인 4입구** — `insuniverse-server/apps/api/src/modules/auth/admin/auth.controller.ts:31` · `insuniverse-server/apps/api/src/modules/auth/admin/auth.controller.ts:58` · `insuniverse-server/apps/api/src/modules/auth/admin/auth.controller.ts:78` · `insuniverse-server/apps/api/src/modules/auth/admin/auth.controller.ts:99`
- **클라이언트 저장 만료 — 로그인 · 앱 자동로그인 두 자리** — `insuniverse-site/front/src/entities/user/model/store.ts:18` · `insuniverse-site/front/src/entities/user/model/store.ts:30`
- **계수를 적지 않는다.** 이 축의 자리 수는 `../_meta/extraction-manifest.yaml` 이 동결하지 않았고, 자체로 센 수를 문서에 싣지 않는 것이 이 PRD 의 제약이다. 위 자리 집합 자체가 앵커이며 계획서 게이트 `#5` 가 동결 SHA 에서 같은 집합을 다시 유도해 대조한다. 시험 코드가 같은 리터럴을 기대값으로 들고 있는 자리는 강제 지점이 아니므로 그 대조에서 제외된다.

### SESSION_RENEWAL

- **앱 환경 분기** — `insuniverse-server/apps/api/src/modules/auth/user/auth.service.ts:315`
- **갱신 토큰 서명** — `insuniverse-server/apps/api/src/modules/auth/user/auth.service.ts:321` · `insuniverse-server/apps/api/src/modules/auth/user/auth.service.ts:613`
- **관리자 접근 토큰 서명** — `insuniverse-server/apps/api/src/modules/auth/admin/auth.service.ts:49` · `insuniverse-server/apps/api/src/modules/auth/admin/auth.service.ts:84` · `insuniverse-server/apps/api/src/modules/auth/admin/auth.service.ts:123` · `insuniverse-server/apps/api/src/modules/auth/admin/auth.service.ts:180`
- **관리자 축 갱신 토큰 서명** — 없다. 그 축에 서명 자리가 0건이라 이 행은 앵커를 열지 않는다. 부재를 종료코드가 아니라 **값으로** 확인하는 것이 게이트 `#6` 의 몫이며, 그 검사는 재유도 결과와 이 행을 함께 0 으로 찍는다
- **갱신 요청 입구** — `insuniverse-server/apps/api/src/modules/auth/user/auth.controller.ts:143`
- **저장 자리와 삭제** — 저장 칸은 회원 테이블의 열이고 로그아웃이 그 칸을 비운다. `insuniverse-server/drizzle/schema.ts:1500` · `insuniverse-server/apps/api/src/modules/auth/user/auth.service.ts:658`

### AUTH_FAILURE

- **이용자 축 실패 코드** — `E0001` · `insuniverse-server/apps/api/src/constants/api.constant.ts:16`
- **관리자 축 실패 코드** — `E0601` · `insuniverse-server/apps/api/src/constants/admin.constant.ts:58`
- **이용자 축 들여오기 출처** — `insuniverse-server/apps/api/src/modules/auth/user/jwt.guard.ts:5`
- **관리자 축 들여오기 출처** — `insuniverse-server/apps/api/src/modules/auth/admin/jwt.guard.ts:1`
- **차단 지점** — 두 가드가 각각 자기 축의 상수를 실어 던진다. `insuniverse-server/apps/api/src/modules/auth/user/jwt.guard.ts:72` · `insuniverse-server/apps/api/src/modules/auth/admin/jwt.guard.ts:47`

### ACCOUNT_STATE_REVOCATION

- **요청 시점 재검증** — 기업 회원 분기에서 두 상태를 차례로 본다. `insuniverse-server/apps/api/src/modules/auth/user/jwt.guard.ts:89` · `insuniverse-server/apps/api/src/modules/auth/user/jwt.guard.ts:93`
- **로그인 시점 판정** — `insuniverse-server/apps/api/src/modules/auth/user/auth.service.ts:289` · `insuniverse-server/apps/api/src/modules/auth/user/auth.service.ts:295`
- **상태 칸의 스키마 정의** — `insuniverse-server/drizzle/schema.ts:403` · `insuniverse-server/drizzle/schema.ts:1496`

### TOKEN_CLIENT_COPY

- **응답 본문 동봉** — 같은 자리 주석이 브리지 전달용임을 선언한다. `insuniverse-server/apps/api/src/modules/auth/user/auth.controller.ts:77`
- **네이티브 브리지 전달** — `insuniverse-site/front/src/entities/user/model/store.ts:41`
- **브라우저 저장소 보관** — `insuniverse-site/front/src/entities/user/model/store.ts:63`
- **저장 사본 판독과 헤더 전송** — `insuniverse-site/front/src/shared/api/index.ts:34` · `insuniverse-site/front/src/shared/api/index.ts:70`

### TOKEN_EXPIRY_CONFIG

- **세 만료값의 주입 지점** — 접근 토큰 · 관리자 접근 토큰 · 갱신 토큰이 각각 자기 환경변수를 읽는다. `insuniverse-server/apps/api/src/config/configuration.ts:87` · `insuniverse-server/apps/api/src/config/configuration.ts:89` · `insuniverse-server/apps/api/src/config/configuration.ts:90`

### NONPROD_AUTH_BYPASS

- **이용자 축 우회의 게이트와 통과 지점** — `insuniverse-server/apps/api/src/modules/auth/user/jwt.guard.ts:44` · `insuniverse-server/apps/api/src/modules/auth/user/jwt.guard.ts:65`
- **관리자 축 우회의 통과 지점** — `insuniverse-server/apps/api/src/modules/auth/admin/jwt.guard.ts:42`
- **두 우회가 공유하는 판정 헬퍼와 운영 환경 진술** — 자리는 부록의 근거 자리 행이 연다

## 적용 범위 (actor × 서비스)

| 정책 id | server | site | admin |
|---|---|---|---|
| SESSION_ESTABLISHMENT | 쿠키 심기 · 가드 판독 2벌 | 요청 인터셉터의 토큰 얹기 | 자격 증명 동봉 옵션 |
| SESSION_LIFETIME | 쿠키 수명 지정 2계열 | 저장 만료 기록 | — |
| SESSION_RENEWAL | 발급 · 회전 · 저장 | 앱 자동로그인 호출 | — |
| AUTH_FAILURE | 상수 선언 2벌 · 차단 2벌 | 실패 응답 소비 | 실패 응답 소비 |
| ACCOUNT_STATE_REVOCATION | 가드 재검증 · 로그인 판정 · 스키마 | — | — |
| TOKEN_CLIENT_COPY | 응답 본문 동봉 | 저장 · 브리지 전달 · 헤더 전송 | — |
| TOKEN_EXPIRY_CONFIG | 환경변수 주입 3자리 | — | — |
| NONPROD_AUTH_BYPASS | 게이트 조건 · 통과 지점 2벌 | — | — |

`—` = 그 서비스가 이 정책의 경로에 등장하지 않음.

이 표의 열은 서비스 3종이며 actor 는 열이 아니다. 인증은 로그인 주체별로 갈리는 것이 아니라 **어느 표면의 세션인가**로 갈리고, 표면과 서비스는 이 축에서 같은 것을 가리킨다. 열 이름 집합과 순서는 `../_meta/template-config.yaml` 의 `services` 를 그대로 따르며 계획서 게이트 `#9` 가 그 대조를 돌린다.

## 예외·폴백

| 정책 id | 예외 조건 | 폴백 동작 | 확인 근거 |
|---|---|---|---|
| SESSION_ESTABLISHMENT | 인증 쿠키가 여러 개 실려 올 때 | 첫 개만 읽지 않는다. 후보 전부를 시도해 검증에 성공하는 것을 쓴다 | `insuniverse-server/apps/api/src/helpers/cookie.helper.ts:38` |
| SESSION_ESTABLISHMENT | 쿠키가 아예 실리지 않는 기기일 때 | 요청 헤더의 토큰을 같은 후보 목록에 넣는다 | `insuniverse-server/apps/api/src/helpers/cookie.helper.ts:52` |
| SESSION_ESTABLISHMENT | 공개로 표시된 처리기일 때 | 두 표면 모두 토큰을 보지 않고 통과시킨다 | `insuniverse-server/apps/api/src/modules/auth/admin/jwt.guard.ts:33` |
| SESSION_LIFETIME | 쿠키를 지울 때 옵션이 심을 때와 다르면 | 옛 쿠키가 지워지지 않고 남는다. 같은 옵션으로 지우는 것이 그 폴백이다 | `insuniverse-server/apps/api/src/modules/auth/user/auth.controller.ts:132` |
| SESSION_LIFETIME | 클라이언트 저장 만료를 지나 화면을 다시 열 때 | 만료 시각으로 자동 로그아웃하는 처리는 주석 처리돼 있고 동작하지 않는다 | `insuniverse-site/front/src/entities/user/model/store.ts:79` |
| SESSION_RENEWAL | 웹 브라우저 로그인 · 관리자 콘솔 로그인일 때 | 갱신 토큰이 발급되지 않는다. 만료 뒤에는 재로그인이다 | `insuniverse-server/apps/api/src/modules/auth/user/auth.service.ts:315` |
| SESSION_RENEWAL | 갱신 토큰이 가리키는 회원과 조회된 회원이 다를 때 | 갱신하지 않고 401 로 막는다 | `insuniverse-server/apps/api/src/modules/auth/user/auth.service.ts:596` |
| AUTH_FAILURE | 코드값만으로 어느 표면의 실패인지 조회하려 할 때 | 정해지지 않는다. 선언 자리를 함께 지정한다 | `insuniverse-server/apps/api/src/constants/admin.constant.ts:58` |
| AUTH_FAILURE | 우회 조건이 성립할 때 | 검증 실패가 401 이 되지 않고 대체 주체로 통과한다. 부록 참조 | `insuniverse-server/apps/api/src/modules/auth/user/jwt.guard.ts:68` |
| ACCOUNT_STATE_REVOCATION | 기업 회원이 아닌 계정일 때 | 요청 시점 재검증을 하지 않는다 | `insuniverse-server/apps/api/src/modules/auth/user/jwt.guard.ts:77` |
| TOKEN_CLIENT_COPY | 저장 사본을 읽지 못할 때 | 쿠키 값으로 폴백한다 | `insuniverse-site/front/src/shared/api/index.ts:68` |
| TOKEN_EXPIRY_CONFIG | 운영에 실린 값을 저장소에서 확인하려 할 때 | 확인되지 않는다. 값을 지어내지 않고 미상으로 둔다 | `insuniverse-server/apps/api/src/config/configuration.ts:90` |
| NONPROD_AUTH_BYPASS | 두 우회가 같은 환경변수에 대해 서로 다른 값을 조건으로 삼을 때 | 두 조건이 그 변수의 한 값에서 동시에 성립할 수 있다. 실제 배포에서 켜져 있는지는 이 문서가 판정하지 않는다. 관측은 `../_meta/findings/20-foundation-access.md` 가 싣는다 | `insuniverse-server/apps/api/src/helpers/cookie.helper.ts:12` |

## 부록

이 절의 정책은 **활성 여부가 저장소 밖 값에 걸려 있어 현행을 판정하지 못한다.** 조건과 자리는 코드가 말하지만 그 조건을 성립시키는 값은 배포 환경에 있다. 그 상태로 본문에 두면 독자가 확정된 동작으로 읽는다. 배치의 정본은 `../_meta/template-config.yaml` 의 `statuses` 이며, 값이 확인되면 상태가 바뀌고 배치도 함께 옮긴다.

### TOKEN_EXPIRY_CONFIG

**토큰 자체의 만료값은 세 개이고 셋 다 배포 환경변수다.** 이용자 접근 토큰 · 관리자 접근 토큰 · 갱신 토큰이 각각 자기 값을 갖는다. 저장소에 있는 것은 그 값을 읽어 서명에 넘기는 자리뿐이고, 실제로 무엇이 주입되는지는 여기서 판정되지 않는다.

**그래서 "12시간" 이 세션 수명이라고 말할 수 없다.** 본문이 여는 두 계열은 쿠키와 화면 저장의 수명이며, 토큰이 그보다 먼저 죽으면 쿠키가 남아 있어도 요청은 실패한다. 이용자가 실제로 겪는 만료는 세 값 중 가장 짧은 것이 정한다.

### NONPROD_AUTH_BYPASS

**개발 편의를 위한 우회가 둘 있고, 둘 다 배포 환경변수 하나의 값을 조건으로 삼는다.** 하나는 특정 발신 주소 목록에서 온 요청의 토큰 검증 실패를 대체 주체로 통과시키고, 다른 하나는 API 문서 화면에서 온 요청을 같은 방식으로 통과시킨다. 뒤엣것은 두 표면 모두에 걸려 있다.

**두 우회가 같은 변수에 대해 서로 다른 값을 본다.** 하나는 그 변수가 운영 값이 **아닐 때** 켜지고, 다른 하나는 개발 값과 **정확히 같을 때** 켜진다. 그런데 같은 저장소의 다른 자리는 운영 배포의 그 변수가 개발 값이라고 적는다. 세 진술이 동시에 참이면 두 우회의 게이트 조건이 **동시에 성립한다** — 운영 배포에서 둘 다 켜진다.

**이 문서는 그 어긋남을 판정하지 않는다.** 관측·재현·앵커는 `../_meta/findings/20-foundation-access.md` 가 싣고, 여기서는 조건과 자리만 연다.

- 근거 자리 — `insuniverse-server/apps/api/src/modules/auth/user/jwt.guard.ts:44` · `insuniverse-server/apps/api/src/helpers/request.helper.ts:20` · `insuniverse-server/apps/api/src/helpers/cookie.helper.ts:12`

## 플랫폼 차이 (웹 / 웹뷰)

앱 웹뷰에서 달라지는 것은 셋이다.

**갱신 토큰이 앱 환경 로그인에만 발급된다.** 로그인 요청의 클라이언트 식별 문자열에 앱 표식이 들어 있으면 갱신 토큰이 함께 발급돼 서버에 저장되고, 그렇지 않으면 발급되지 않는다. 그래서 자동 로그인은 앱에만 있는 기능이고 웹 브라우저는 만료 뒤 재로그인한다. 강제 지점은 `SESSION_RENEWAL` 의 앱 환경 분기 행이다.

**앱 최소버전 강제가 인증과 같은 축에 있다.** 화면이 앱 버전을 확인해 최소버전 미만이면 업데이트를 요구한다. 이 판정은 인증 층이 아니라 화면 층이지만 조건이 같은 앱 환경이라 여기 함께 적는다 — 앱에서만 발동하고 웹에서는 발동하지 않는다. `insuniverse-site/front/src/shared/hooks/App/useAppVersionCheck.ts:104`

**쿠키를 싣지 않는 기기의 헤더 폴백이 앱 웹뷰 때문에 생겼다.** 다른 출처로 가는 요청에 쿠키를 싣지 않는 웹뷰가 있어 로그인 직후 인증이 깨졌고, 그 대체 경로가 `TOKEN_CLIENT_COPY` 다. 쿠키가 정상인 환경에서는 동작이 달라지지 않는다.

관리자 콘솔에는 웹뷰 표면이 없다. 위 셋은 전부 이용자 표면의 이야기다.

## 연혁

릴리스 태그가 존재하지 않는다. 릴리스 열은 `—` 로 두고 커밋 SHA 로 추적한다.

| 릴리스 | 날짜 | SHA | 변경 요약 |
|---|---|---|---|
| — | 2026-02-04 | `aa6087cd` | 갱신 토큰의 비정상 포맷 방어 추가 — 갱신 경로의 실패 처리가 처음 명시됨 |
| — | 2026-06-20 | `b5498764` | 기업 회원 요청 시점 재검증의 조회 키를 로그인 아이디에서 회원 식별자로 변경 |
| — | 2026-06-23 | `ccb459a6` | 인증 쿠키 심기·지우기 옵션 통일 — 로그인 직후 401 튕김 수정 |
| — | 2026-06-24 | `a6de8cf4` | 중복 인증 쿠키 중 검증에 성공하는 토큰을 고르도록 후보 목록 도입 |
| — | 2026-06-24 | `f74117bb` | 이용자 화면이 저장 토큰을 헤더로 전송 — 쿠키 미전송 기기 인증 보조 |
| — | 2026-06-30 | `c3760627` | 발신 주소 화이트리스트 우회에 환경 조건을 붙임 — 커밋의 의도는 운영 비활성이다. 그 조건이 운영에서 성립하는지는 부록 참조 |
| — | 2026-07-01 | `f0ce46c8` | 조직관리자 로그인 입구 신설 — 관리자 콘솔 로그인 입구가 넷이 됨 |
