Skip to content

Commit 3e752ff

Browse files
committed
Merge branch 'dev'
2 parents 34dc23c + d0990bd commit 3e752ff

4 files changed

Lines changed: 424 additions & 20 deletions

File tree

HANDHELD_AUTH_RECOVERY.md

Lines changed: 394 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,394 @@
1+
# Auth Recovery Handheld
2+
3+
## 현재 주요 문제 상황
4+
5+
Cloud Run 배포 후 로그인 흐름이 다음 형태로 무너지고 있었다.
6+
7+
- 로그인 화면에서 무한 로딩이 발생한다.
8+
- 로그인 서비스에서 다른 게임 서비스로 이동하면 인증 상태가 풀린다.
9+
- 서비스 이동 후 인증이 꼬여 무한 리다이렉션이 발생한다.
10+
- 어떤 경우에는 로그인 직후 다음 API 호출에서 바로 401이 난다.
11+
- 로그아웃 후에도 일부 세션은 남아 있거나, 반대로 정상 세션이 중간에 끊긴다.
12+
13+
이 문제는 단일 버그가 아니라, 현재 인증 구조가 Cloud Run `run.app` 멀티 서비스 배포 방식과 맞지 않는 데서 시작됐다.
14+
15+
## 원인 분석
16+
17+
### 1. Cloud Run `run.app` 간 쿠키 공유 전제 자체가 깨져 있었다
18+
19+
현재 서비스들은 예를 들어 아래처럼 서로 다른 Cloud Run 기본 도메인을 쓰고 있다.
20+
21+
- `discoverex-game-hub-329947062450.asia-northeast3.run.app`
22+
- `discoverex-magic-eye-329947062450.asia-northeast3.run.app`
23+
24+
이 구조에서는 서비스 간 쿠키 공유 기반 SSO를 기대하면 안 된다.
25+
즉 한 서비스에서 심은 쿠키를 다른 서비스에서 자연스럽게 이어받는 구조가 아니다.
26+
27+
그런데 서버는 운영 환경에서도 `access_token` 쿠키를 주요 인증 매체처럼 다루고 있었고, 이 전제가 무너지면서 서비스 이동 시 인증 소실과 루프가 발생했다.
28+
29+
### 2. JWT 발급 키와 검증 키가 서로 달라질 수 있었다
30+
31+
기존 코드에서는:
32+
33+
- `src/domains/auth/auth_service.py` 가 로컬 하드코딩 `SECRET_KEY` 로 JWT를 발급
34+
- `src/domains/auth/utils/verify_token.py` 가 환경변수 `SECRET_KEY` 로 JWT를 검증
35+
36+
이 상태에서 Cloud Run 환경에 `SECRET_KEY` 가 주입돼 있으면, 로그인 직후 발급한 JWT를 다음 요청에서 서버가 자기 손으로 검증 실패시킬 수 있었다.
37+
38+
이건 로그인 성공 직후 다시 로그인 화면으로 튕기거나, `/auth/users/me` 가 401을 내는 직접 원인이 된다.
39+
40+
### 3. Firebase 토큰과 백엔드 쿠키를 혼용하고 있었다
41+
42+
기존 `/auth/users/me` 는 들어온 토큰을 기반으로 인증한 뒤, 다시 `access_token` 쿠키를 심고 있었다.
43+
44+
- Firebase 토큰도 쿠키에 다시 저장될 수 있었음
45+
- 이후 요청은 헤더 기반일 수도 있고 쿠키 기반일 수도 있었음
46+
- 서비스마다 인증 소스가 달라지면서 상태가 꼬이기 쉬웠음
47+
48+
결과적으로 인증 기준이 하나가 아니라 둘 이상이었고, 브라우저/서비스 이동/SSR 상황에 따라 동작이 달라졌다.
49+
50+
### 4. Redis 세션 검사가 Firebase 흐름까지 과도하게 강제되고 있었다
51+
52+
기존 로직은 Firebase 토큰이 유효해도 Redis 상의 사용자 세션이 없으면 401을 낼 수 있었다.
53+
54+
멀티 서비스 이동, 첫 진입, 세션 동기화 타이밍 차이 같은 상황에서 이 조건은 지나치게 빡빡했고, 정상 사용자를 오인 차단해서 재로그인 루프를 만들 수 있었다.
55+
56+
### 5. Firebase 로그아웃 시 세션 정리가 누락될 수 있었다
57+
58+
기존 `/auth/logout` 은 자체 JWT는 잘 처리해도 Firebase 토큰 경로에서는 DB 사용자 UUID를 안정적으로 찾지 못해 세션 정리가 빠질 여지가 있었다.
59+
60+
이 경우 로그아웃 후 세션이 남거나, 반대로 상태가 어중간하게 꼬일 수 있다.
61+
62+
## 이번에 적용한 서버 수정
63+
64+
### 1. JWT 키 소스를 단일화했다
65+
66+
다음 파일들을 수정했다.
67+
68+
- [`src/configs/setting.py`](/home/esillileu/backend/src/configs/setting.py)
69+
- [`src/domains/auth/auth_service.py`](/home/esillileu/backend/src/domains/auth/auth_service.py)
70+
- [`src/domains/auth/utils/verify_token.py`](/home/esillileu/backend/src/domains/auth/utils/verify_token.py)
71+
72+
변경 내용:
73+
74+
- `SECRET_KEY` 를 설정 파일에서 환경변수 기반으로 통일
75+
- `ALGORITHM` 도 설정 파일 기준으로 통일
76+
- JWT 발급과 검증이 같은 키를 쓰도록 정리
77+
78+
효과:
79+
80+
- 로그인 성공 후 바로 401 나는 치명적인 불일치 제거
81+
- Cloud Run 환경에서 키 주입 여부 때문에 동작이 갈리는 문제 완화
82+
83+
### 2. 운영 환경 기본값에서 교차 서비스 쿠키 의존을 껐다
84+
85+
`src/configs/setting.py` 에 다음 플래그를 추가했다.
86+
87+
- `AUTH_COOKIE_ENABLED`
88+
- `ENFORCE_REDIS_SESSIONS`
89+
90+
기본값:
91+
92+
- `local` 에서는 둘 다 `true`
93+
- 그 외 환경에서는 둘 다 `false`
94+
95+
의도:
96+
97+
- 로컬 개발에서는 기존 쿠키/세션 흐름을 최대한 유지
98+
- Cloud Run 운영에서는 쿠키 기반 서비스 간 인증 전파를 기본적으로 기대하지 않음
99+
100+
### 3. `/auth/users/me` 가 Firebase 토큰을 쿠키로 다시 굽지 않도록 바꿨다
101+
102+
수정 파일:
103+
104+
- [`src/domains/auth/auth_router.py`](/home/esillileu/backend/src/domains/auth/auth_router.py)
105+
106+
기존 문제:
107+
108+
- 인증 성공 후 서버가 다시 `access_token` 쿠키를 심음
109+
- Firebase 토큰과 자체 JWT 경계가 흐려짐
110+
111+
변경 후:
112+
113+
- 쿠키 기능이 켜져 있어도 `local` 자체 JWT 사용자에게만 쿠키 재설정
114+
- Firebase 기반 운영 흐름에서는 헤더 기반 인증을 우선
115+
116+
효과:
117+
118+
- 서비스 이동 시 쿠키 상태 꼬임 감소
119+
- 인증 기준을 `Authorization` 헤더 쪽으로 정리
120+
121+
### 4. Firebase 경로의 Redis 세션 강제 검사를 완화했다
122+
123+
수정 파일:
124+
125+
- [`src/domains/auth/utils/verify_token.py`](/home/esillileu/backend/src/domains/auth/utils/verify_token.py)
126+
127+
변경 내용:
128+
129+
- Redis 세션 체크를 `ENFORCE_REDIS_SESSIONS` 플래그 뒤로 이동
130+
- 운영 환경 기본값은 강제하지 않도록 조정
131+
132+
효과:
133+
134+
- Firebase 토큰이 유효한데도 Redis 타이밍 문제로 401 나던 흐름 완화
135+
- 서비스 첫 진입/이동 시 로그인 루프 감소 기대
136+
137+
### 5. 로그아웃 경로에서 Firebase 사용자도 안정적으로 세션 정리되게 했다
138+
139+
수정 파일:
140+
141+
- [`src/domains/auth/auth_router.py`](/home/esillileu/backend/src/domains/auth/auth_router.py)
142+
143+
변경 내용:
144+
145+
- 자체 JWT 디코드 실패 시 Firebase 토큰 검증으로 fallback
146+
- payload 에 `uid` 가 없으면 `email` 로 DB 사용자를 조회해서 UUID 확보
147+
- 확보된 UUID 기준으로 `auth_service.logout(...)` 실행
148+
149+
효과:
150+
151+
- Firebase 기반 로그아웃에서도 사용자 세션 정리 누락 가능성 축소
152+
153+
## 지금 상태에서 기대되는 개선
154+
155+
서버 기준으로는 다음 문제가 완화되거나 제거됐다.
156+
157+
- 로그인 직후 바로 401 나는 문제
158+
- 서비스 이동 후 쿠키 기반 인증 꼬임
159+
- Firebase 토큰이 유효한데 Redis 세션 때문에 튕기는 문제
160+
- Firebase 로그아웃 세션 정리 누락
161+
162+
즉, 서버는 이제 Cloud Run `run.app` 멀티 서비스 환경에서 "쿠키 공유 SSO" 를 전제로 하지 않도록 방향을 바꿨다.
163+
164+
## 프론트까지 반영한 현재 상태
165+
166+
서버 수정만으로는 충분하지 않았고, 프론트도 같은 전제로 정리해야 했다.
167+
168+
현재 프론트 기준 핵심 원칙은 아래와 같다.
169+
170+
- 운영에서 인증의 기준은 쿠키가 아니라 `Authorization` 헤더다.
171+
- 토큰 소스 우선순위는 `명시 전달 토큰 -> Firebase ID token -> sessionStorage.sso_token` 이다.
172+
- 서비스 간 이동 시 `sso_token` 은 1회 부트스트랩 용도이고, 대상 앱은 동기화 후 URL 에서 즉시 제거한다.
173+
- 운영 환경의 SSR 은 다른 `run.app` 서비스 쿠키를 신뢰하지 않는다. 보호 페이지의 최종 인증 판단은 클라이언트 부팅 후 `/auth/users/me` 재동기화 결과를 기준으로 한다.
174+
- 로컬 개발만 `access_token` 쿠키 기반 SSR 보조 경로를 유지한다.
175+
176+
### 이번 프론트 정리에서 반영한 내용
177+
178+
- 공용 API 클라이언트의 기본 인증을 `Authorization` 헤더 주입 방식으로 통일
179+
- `AuthProvider` 의 세션 갱신 로직을 단일 토큰 해석 함수 기준으로 통합
180+
- 백엔드 응답의 사용자 식별자를 `user_id` 기준으로 읽도록 수정
181+
- `server-auth` 는 로컬 API URL 에서만 쿠키 SSR 복구를 시도하고, 운영에서는 즉시 `null` 반환
182+
- 허브/글로벌 네비게이션/로그인 리다이렉트 모두 동일한 `sso_token` 부트스트랩 규칙 사용
183+
- `vision_ai_logout_signal` 쿠키 기반 글로벌 로그아웃 전제를 제거하고, 백엔드 로그아웃 + 로컬 상태 정리로 단순화
184+
185+
### 여전히 남는 구조적 한계
186+
187+
Cloud Run 기본 `run.app` 를 여러 서비스에 그대로 쓰는 한, 교차 서비스 SSR 쿠키 공유는 계속 불가능하다.
188+
189+
장기적으로 가장 안정적인 방향은 아래 둘 중 하나다.
190+
191+
- 하나의 상위 커스텀 도메인 아래 서브도메인/경로 체계로 재배치
192+
- 하나의 프론트에서 여러 게임을 path 기반으로 통합
193+
194+
## 변경 파일 요약
195+
196+
- [`src/configs/setting.py`](/home/esillileu/backend/src/configs/setting.py)
197+
- [`src/domains/auth/auth_service.py`](/home/esillileu/backend/src/domains/auth/auth_service.py)
198+
- [`src/domains/auth/auth_router.py`](/home/esillileu/backend/src/domains/auth/auth_router.py)
199+
- [`src/domains/auth/utils/verify_token.py`](/home/esillileu/backend/src/domains/auth/utils/verify_token.py)
200+
201+
## 간단 검증 결과
202+
203+
- 서버 인증 관련 수정 파일은 기존 문서 기준 문법 검증 통과 상태
204+
- 현재 작업 환경에서는 `backend``uv run pytest``pytest` 실행 파일 부재로 실패
205+
- 현재 작업 환경에서는 `web` 의 타입체크가 `pnpm` 실행 파일 부재로 실패
206+
207+
## 운영자가 바로 확인할 것
208+
209+
배포 후 아래를 우선 확인하면 된다.
210+
211+
1. 로그인 직후 `/auth/users/me` 가 200인지 확인
212+
2. 다른 게임 서비스로 이동한 뒤 첫 보호 API가 200인지 확인
213+
3. 브라우저 네트워크 탭에서 요청이 쿠키가 아니라 `Authorization` 헤더 기준으로 가는지 확인
214+
4. 401이 나면 서버 로그에서 JWT decode 실패인지, Firebase 검증 실패인지, Redis 세션 체크 때문인지 구분해서 확인
215+
216+
## 운영자가 바로 확인할 프론트 포인트
217+
218+
1. Game Hub 로그인 후 다른 게임으로 이동했을 때 URL 의 `sso_token` 이 즉시 사라지는지 확인
219+
2. 이동 직후 첫 `/auth/users/me` 요청이 `Authorization` 헤더 포함 200으로 끝나는지 확인
220+
3. 운영 배포에서 SSR 시점 `initialUser` 가 없어도 클라이언트 복구 후 무한 로딩이나 로그인 루프가 없는지 확인
221+
4. 로그아웃 후 다른 게임 URL 직접 진입 시 이전 사용자 UI 가 남지 않는지 확인
222+
223+
1. Firebase 초기화 완료 대기
224+
2. `auth.currentUser` 확인
225+
3. 사용자가 있으면 `getIdToken()` 호출
226+
4. 해당 토큰으로 `/auth/users/me` 호출
227+
5. 200이면 앱 내부 사용자 상태 확정
228+
6. 401이면 로그인 화면 또는 재인증 흐름으로 이동
229+
230+
즉, "다른 서비스에서 이미 로그인했으니 여기서도 쿠키로 자동 인식되겠지" 라는 기대를 버려야 한다.
231+
232+
#### 3. 로그인 완료 판단 기준을 쿠키에서 Firebase 세션으로 옮기기
233+
234+
기존에 아래 같은 판단이 있으면 바꾸는 것이 좋다.
235+
236+
- 쿠키 존재 여부로 로그인 상태 판단
237+
- 특정 API가 쿠키를 자동 첨부해줄 것이라는 전제
238+
- 서비스 이동 후 로컬 스토리지/쿠키 값만 보고 로그인 완료로 간주
239+
240+
권장 기준:
241+
242+
- Firebase `onAuthStateChanged`
243+
- `auth.currentUser`
244+
- `getIdToken()` 성공 여부
245+
- `/auth/users/me` 성공 여부
246+
247+
#### 4. 로그인 화면 무한 로딩 방지 가드 추가
248+
249+
무한 로딩은 보통 아래 상황에서 생긴다.
250+
251+
- Firebase 초기화 대기 중
252+
- 토큰 재발급 대기 중
253+
- `/auth/users/me` 호출이 반복 실패
254+
- 실패했는데도 상태 머신이 종료되지 않음
255+
256+
프론트에서는 인증 부트스트랩 상태를 최소한 아래처럼 나눠야 한다.
257+
258+
- `loading`
259+
- `authenticated`
260+
- `unauthenticated`
261+
- `error`
262+
263+
특히 `loading -> loading -> loading` 으로 돌아가는 루프를 막아야 한다.
264+
265+
예시:
266+
267+
```ts
268+
type AuthBootState =
269+
| 'loading'
270+
| 'authenticated'
271+
| 'unauthenticated'
272+
| 'error';
273+
```
274+
275+
그리고 `/auth/users/me` 가 401이면 무조건 재시도 루프에 넣지 말고, 횟수 제한 후 로그인 화면으로 보내야 한다.
276+
277+
#### 5. 서비스 이동 시 토큰 재사용 전략 정리
278+
279+
Game Hub에서 Magic Eye나 다른 게임으로 넘어갈 때는 다음 원칙을 지킨다.
280+
281+
- 이동 전에 별도 쿠키 동기화를 기대하지 않는다.
282+
- 이동 후 도착한 서비스가 자기 Firebase 세션에서 `getIdToken()` 을 다시 꺼낸다.
283+
- 그 토큰으로 자기 API 초기화를 수행한다.
284+
285+
즉 "공유 쿠키 전달" 이 아니라 "공유 Firebase 로그인 상태를 각 서비스가 재해석" 하는 구조다.
286+
287+
### 프론트 구현 체크리스트
288+
289+
서비스별로 아래를 점검하면 된다.
290+
291+
1. 보호 API 호출 공통 함수가 있는지 확인
292+
2. 그 공통 함수가 항상 `Authorization` 헤더를 붙이는지 확인
293+
3. `credentials: 'include'` 에 로그인 유지가 의존하는지 확인
294+
4. 앱 최초 진입 시 `/auth/users/me` 동기화가 있는지 확인
295+
5. 401 응답 시 무한 재시도하지 않는지 확인
296+
6. 로그인 성공 후 다른 서비스로 이동할 때 별도 쿠키 기대 로직이 있는지 확인
297+
7. 로그아웃 시 Firebase 로그아웃과 백엔드 `/auth/logout` 호출 순서를 명확히 정했는지 확인
298+
299+
### 권장 로그인 부트스트랩 흐름
300+
301+
가장 단순한 권장 흐름은 아래다.
302+
303+
1. 앱 시작
304+
2. Firebase auth 상태 확인
305+
3. 유저 없음: 비로그인 상태로 전환
306+
4. 유저 있음: `getIdToken()` 호출
307+
5. `/auth/users/me` 호출
308+
6. 성공: 사용자 상태 저장 후 앱 렌더링
309+
7. 실패: 에러 기록 후 로그인 화면 또는 재로그인 유도
310+
311+
이 흐름을 서비스마다 동일하게 맞추면 서비스 이동 시 인증 꼬임이 크게 줄어든다.
312+
313+
### 로그아웃 권장 흐름
314+
315+
로그아웃도 일관되게 맞춰야 한다.
316+
317+
권장 순서:
318+
319+
1. Firebase `signOut()`
320+
2. 백엔드 `/auth/logout` 호출
321+
3. 프론트 로컬 사용자 상태 초기화
322+
4. 로그인 화면 이동
323+
324+
중요한 건, 로그아웃 완료 판단을 쿠키 삭제 성공 여부에 두지 않는 것이다.
325+
326+
### 프론트에서 바로 찾을 냄새나는 코드
327+
328+
아래 패턴이 있으면 지금 장애와 직접 연결될 가능성이 높다.
329+
330+
- `document.cookie` 또는 쿠키 값으로 로그인 상태 판단
331+
- `credentials: 'include'` 만 있고 `Authorization` 헤더는 없음
332+
- 401 응답 시 무조건 동일 요청 재호출
333+
- `/auth/users/me` 실패 시 상태 종료 없이 다시 로딩 시작
334+
- 서비스 이동 시 토큰 갱신 없이 라우팅만 수행
335+
- Firebase user 존재 여부와 백엔드 사용자 상태를 분리해서 관리하지 않음
336+
337+
### 프론트 작업 우선순위
338+
339+
가장 빠른 복구 순서는 아래가 좋다.
340+
341+
1. 공통 API 클라이언트에서 `Authorization` 헤더 주입
342+
2. 서비스 최초 진입 시 `/auth/users/me` 동기화 추가
343+
3. 로그인 로딩 상태 머신 정리
344+
4. 401 재시도 루프 제거
345+
5. 로그아웃 순서 통일
346+
347+
### 프론트 예시 구현 스케치
348+
349+
```ts
350+
export async function getAuthHeaders() {
351+
const user = auth.currentUser;
352+
if (!user) return {};
353+
354+
const token = await user.getIdToken();
355+
return {
356+
Authorization: `Bearer ${token}`,
357+
};
358+
}
359+
360+
export async function fetchMe() {
361+
const headers = await getAuthHeaders();
362+
return fetch(`${API_BASE_URL}/auth/users/me`, {
363+
method: 'GET',
364+
headers,
365+
});
366+
}
367+
```
368+
369+
이 패턴을 모든 게임 서비스에 공통으로 두는 게 가장 안전하다.
370+
371+
## 프론트까지 포함한 최종 결론
372+
373+
이번 장애의 본질은 "Cloud Run 멀티 `run.app` 환경에서 쿠키 기반 인증 공유를 기대한 구조" 에 있다.
374+
375+
백엔드는 이미 다음 방향으로 정리했다.
376+
377+
- JWT 키 통일
378+
- 운영 환경 쿠키 의존도 축소
379+
- Firebase 경로 세션 검증 완화
380+
- 로그아웃 정리 보강
381+
382+
프론트는 다음 방향으로 맞추면 된다.
383+
384+
- Firebase 로그인 상태를 신뢰
385+
- 매 요청 또는 보호 API 호출 시 `Authorization` 헤더 사용
386+
- 각 서비스가 자기 첫 진입 시 `/auth/users/me` 로 사용자 상태 동기화
387+
- 쿠키 기반 로그인 유지 전제 제거
388+
389+
이렇게 맞추면 지금 보고 있는 무한 로딩, 인증 초기화, 무한 리다이렉션 문제는 구조적으로 정리된다.
390+
391+
## 한 줄 결론
392+
393+
이번 정상화는 "Cloud Run 멀티 `run.app` 환경에서 쿠키 기반 인증 공유를 버리고, 헤더 기반 인증으로 정리" 하는 방향의 1차 복구다.
394+
서버 쪽 급한 불은 껐고, 완전한 안정화는 프론트가 같은 기준으로 맞춰질 때 완료된다.

0 commit comments

Comments
 (0)