인증·계정·관리자 서버 API 계약 (이슈 #1331, v0.18.0 B01)
규칙 한 문장 — 로그인·계정 연결·계정 삭제·관리자 열람은 서버가 판정한 신원으로만 움직인다: 서버 API 는 호출자의 토큰을 인증 서버에 물어 누구인지 확인하고, 앱은 자기가 시작한 요청의 결과만 받아들이며, 어느 쪽도 토큰·비밀을 일반 응답이나 로그에 싣지 않는다. 실패는 "어느 단계가 왜" 를 말하는 명시 결과다.
- 이슈: #1331 (계획 B01 · 총괄 카드)
- 보존하는 정책: ADR §2 P10(owner/RLS/grants·개인정보 삭제 연쇄·명시 신원 전달) · 앱 계정 권한 · 명시적 신원 전달 · 소셜 로그인 운영 문서 · 네이티브 브리지
- 구현: 서버
api/auth/_shared.js(공용 경계) ·api/account/{delete,_apple,_kakao}.js·api/admin/{_shared,impersonate,users}.js·api/auth/test-admin/session.js— 앱src/react/services/supabaseAuth.ts·services/auth/{authErrors,nativeAuthTransactions,accountDeletionResult,impersonationSession,socialAuthFlow,socialIdentityLinking}.ts·services/auth/providers/*·services/accountDeletionClient.ts - 게이트:
tests/auth/{serverApiShared,accountDeleteApi,adminApi,testAdminSessionApi,accountDeletionResult,impersonationSession,nativeAuthTransactions,nativeAuthCallbackBridge,authErrors}.test.mjs+ 기존socialOAuthProviders·socialIdentityLinking·accountDeletionProviderDisconnect·authPkceHardCut·iosNativeAuthContract·authSessionReadyGate.native· e2e CASE-017(삭제 왕복)·CASE-007(연쇄)·CASE-009 · pgTAPauth_user_delete_cascade·account_deletion_residue_scan - 서버(DB) 변경: 없음. 마이그레이션 없음.
1. 읽는 법 — 유저 A 의 계정 삭제 한 건
유저 A 가 프로필에서 [계정 삭제] → 안내 → 확인 문구 입력 → [삭제] 를 누른다. 앱은 A 의 로그인 토큰과 확인 문구를 POST /api/account/delete 로 보낸다. 서버는 ① 그 토큰을 인증 서버에 물어 "A 가 맞고 세션이 살아 있다" 를 확인하고, 그 세션이 관리자가 대신 발급한 열람 세션이 아닌지 본 뒤 ② Kakao·Apple 에 연결 해제를 요청하고(실패해도 경고로만) ③ A 의 사진·인입 파일 폴더를 비우고 ④ 인증 서버에서 A 를 지운다(DB 의 A 데이터는 FK 연쇄로 함께 사라진다). 앱은 200 을 받으면 기기의 세션 흔적·초안·대기열을 지우고 로그인 화면으로 간다.
같은 흐름에서 응답만 잃은 경우: 서버가 ④까지 마쳤는데 네트워크가 끊겨 앱이 200 을 못 받았다. A 가 다시 [삭제] 를 누르면 서버는 ①에서 "토큰은 유효한데 그 유저가 없다" 를 보고 410 account_already_deleted 를 돌려준다. 앱은 이것을 완료로 보고 같은 기기 정리를 한다 — A 에게는 첫 시도와 똑같이 보인다. 반대로 ③에서 스토리지가 답하지 않으면 502/504 storage_purge_*(재시도 가능) 로 끝나고 A 의 계정은 그대로 남는다 — 같은 요청을 다시 보내면 된다.
2. 서버 API 4개 — 입력·권한·출력·오류·시간 제한
2-1. 공용 경계 (api/auth/_shared.js)
모든 handler 는 이 모듈로 시작한다. 새 endpoint 도 여기서 시작한다.
| 항목 | 규칙 |
|---|---|
| 본문 | JSON 객체만. 4KB 상한 초과 → 413 body_too_large(연결 끊음). 깨진 JSON·배열·문자열 → 400 invalid_body. 본문 없음 → 빈 객체. 읽는 도중 연결 끊김 → 400 request_aborted. 항상 결말이 난다(종전: 상한 초과 때 응답 없이 함수 시간 초과까지 매달림) |
| 호출자 | Authorization: Bearer <access token>. 서버는 호출자의 토큰으로 GET /auth/v1/user 를 부른다 — 서명·만료·세션 폐기·유저 존재를 인증 서버가 판정한다. 서비스 키로 호출자를 믿어 주지 않는다 |
| 호출자 검증 결과 | 401 not_authenticated(토큰 없음·무효·만료·폐기; 내부 reason 에 인증 서버 error_code) · 503 auth_unavailable(인증 서버 응답 없음, 재시도) · 정상 = userId |
| 시간 제한 | Supabase(Auth·PostgREST·Storage) 호출 8초, 외부 provider(Kakao·Apple) 6초. 넘기면 UpstreamTimeoutError(code UPSTREAM_TIMEOUT) 로 끝나고 handler 가 단계 이름을 붙여 504 *_timeout 을 돌린다. 플랫폼(Vercel 함수) 상한은 별도이며 이 값들은 그 안에서 단계별 결과를 내기 위한 것이다 |
| 재시도 | 서버는 재시도하지 않는다(한 번 시도 → 명시 결과). 재시도 주체는 앱이며 응답의 retryable: true 가 그 신호다 |
| 오류 로그 | logServerError(event, error, metadata): 오류 이름·코드 + 허용 목록 메타데이터(소문자 스네이크 키, 숫자·불리언·짧은 토큰 문자열)만. 키 이름에 token·secret·password·authorization·cookie·email·phone·refresh·access 가 들어가면 값과 무관하게 버린다 |
| 서비스 키 | SUPABASE_SERVICE_ROLE_KEY 는 관리 API(유저 조회·삭제·링크 발급)와 스토리지 정리에만. 호출자 검증·권한 판정에는 쓰지 않는다 |
2-2. POST /api/account/delete — 본인 계정 삭제
| 항목 | 값 |
|---|---|
| 입력 | 헤더 bearer(본인 토큰) · 본문 { confirmation: "delete-my-account", appleAuthorizationCode?: string }. 대상 id 를 받지 않는다 — 대상은 언제나 호출자 본인 |
| 행위자 | 인증 서버가 확인한 본인. 관리자 열람 세션 거부: 토큰 클레임 amr 의 method 가 전부 otp(관리자가 magic link 로 발급한 세션이 만들어지는 유일한 방법, 로컬 실측 2026-09-07) → 403 impersonated_session. amr 이 없거나 다른 방법이 섞여 있으면 실사용자로 보고 진행(모르는 값으로 실사용자의 삭제를 막지 않는다) |
| 순서 | ① 호출자 검증 → ② provider 연결 해제(경고) → ③ 스토리지 3버킷(profile-images·inbody-imports·wodup-imports)의 {uid}/ 폴더 정리 → ④ Auth 유저 삭제(DB 연쇄). §3 |
| 200 | { ok: true, warnings: string[] } — warnings: kakao_unlink_skipped_no_admin_key·kakao_unlink_skipped_no_provider_id·kakao_unlink_failed·kakao_unlink_timeout·apple_revoke_skipped_no_config·apple_revoke_skipped_no_reauth·apple_revoke_failed·apple_revoke_timeout·provider_identities_unavailable·provider_disconnect_failed |
| 410 | account_already_deleted (+ok: true, warnings: []) — 토큰 유효·유저 없음(user_not_found). 앱은 완료로 처리 |
| 4xx | 405·401 not_authenticated·403 impersonated_session·400 confirmation_required·400 invalid_body·413 body_too_large |
| 5xx | 500 server_not_configured·503 auth_unavailable·502 storage_purge_failed·504 storage_purge_timeout·502 auth_delete_failed·504 auth_delete_timeout — 5xx 는 전부 retryable: true(계정 그대로) |
| 앱 | accountDeletionClient.requestAccountDeletion() → interpretAccountDeletionResponse 가 200/410 을 "삭제됨", 나머지를 "실패(code, retryable)" 로 분류. 삭제됨이면 기기 세션 흔적 정리(clearAuthSessionAfterAccountDeletion, 네트워크 0회) 뒤 controller 가 owner 데이터·초안·대기열을 지운다 |
2-3. POST /api/admin/impersonate · GET /api/admin/users — 관리자 열람
| 항목 | 값 |
|---|---|
| 관문 순서 | ① ADMIN_IMPERSONATION_ENABLED=true 아니면 404 impersonation_disabled → ② 서버 설정(500 impersonation_not_configured) → ③ 호출자 토큰으로 유저 확인(401) → ④ 같은 토큰으로 is_lift_guild_admin() RPC(403 forbidden). 네 관문을 지나야 서비스 키를 한 번이라도 쓴다 |
| impersonate 입력 | { user_id } 또는 { email }(둘 다 없으면 400 target_required). 대상 없음 404 target_not_found, 이메일 없는 유저 422 target_has_no_email |
| impersonate 출력 | { access_token, refresh_token, target: { id, email, display_name } } — magic link 발급 + verify 로 만든 대상의 진짜 세션. 앱은 읽기 전용 가드(adminImpersonationGuard)를 씌우고 관리자 세션은 기기에 보관 |
| impersonate 실패 | 502 target_lookup_failed·502 impersonation_link_failed·502 impersonation_verify_failed — 토큰을 돌려주지 않는다 |
| 감사 로그 | impersonation_issued { actor_user_id, target_user_id, occurred_at } — id 만. 이메일·토큰·해시 없음 |
| users | ?q= 부분 일치(이메일·표시 이름), 최대 25명, 응답 { users: [{ id, email, display_name }] }. 메타데이터 원문 없음. 조회 실패 502 user_search_failed |
| 열람 종료 | 앱 stopImpersonation(): 관리자 세션으로 되돌리기 전에 대상 세션을 서버에서 폐기(POST /auth/v1/logout?scope=local, 그 세션 하나만 — 실측 204 → 이후 refresh 400). 폐기 실패는 종료를 막지 않고 디버그 로그에 남는다 |
| 열람 세션의 한계 | 열람 중 앱이 하는 쓰기는 가드가 막지만, 직접 호출은 대상 토큰이 진짜라 서버가 구분하지 못한다. 계정 삭제만 서버가 amr 로 거른다(§2-2). 그 밖의 쓰기 RPC 에 같은 판정을 넣는 것은 A05(관리자 경계) 범위 |
2-4. POST /api/auth/test-admin/session — e2e 전용 시험 관리자 로그인
| 항목 | 값 |
|---|---|
| 관문 순서 | ① TEST_ADMIN_LOGIN_ENABLED=true 아니면 404 test_admin_login_disabled → ② 서버가 Production Supabase 프로젝트(kobxeylancdimqhfkbnl, deploy.yml 의 PRODUCTION_SUPABASE_PROJECT_REF 와 같은 값, 테스트가 대조)를 가리키면 플래그·비밀과 무관하게 404 + 오류 로그 test_admin_login_blocked_production → ③ 설정(500) → ④ 본문 { secret } 을 상수 시간 비교(403 test_admin_login_forbidden) |
| 하는 일 | 서비스 키가 있으면 TEST_ADMIN_EMAIL 계정을 app_metadata.role=admin 으로 만들거나 갱신 → 비밀번호 로그인 → { access_token, refresh_token, expires_in, expires_at, token_type, user } |
| 실패 | 401 test_admin_sign_in_failed·500 test_admin_login_failed — 토큰 없음 |
| 앱 | ?test-admin=<secret> URL 로만 켜진다(sharedShell 의 버튼). 비밀은 번들에 없고 e2e 가 주입한다 |
3. 계정 삭제의 단계·재개 지점
| 단계 | 어디서 | 실패 시 결과 | 계정 상태 | 재개 |
|---|---|---|---|---|
| ① 호출자 검증 | 서버 → 인증 서버 | 401·403 impersonated_session·503 auth_unavailable | 그대로 | 앱 재시도(503 만) |
| ② provider 연결 해제 | 서버 → Kakao Admin API / Apple auth/token+auth/revoke | 경고만(warnings), 진행 | 그대로 | 재시도 때 Kakao 는 이미 해제라 kakao_unlink_failed 경고가 날 수 있다 — 무해 |
| ③ 스토리지 정리 | 서버 → Storage(서비스 키), 폴더 걷기 상한 200 | 502 storage_purge_failed / 504 storage_purge_timeout, retryable: true | 그대로(파일 일부만 지워졌을 수 있음 — 다음 시도가 이어 지운다) | 같은 요청 재전송 |
| ④ Auth 유저 삭제 | 서버 → Auth Admin API | 502 auth_delete_failed / 504 auth_delete_timeout, retryable: true. 404 = 이미 없음 = 성공 | 그대로 | 같은 요청 재전송 |
| ④ 완료 뒤 응답 유실 | — | 재시도가 ①에서 410 account_already_deleted | 삭제됨 | 앱이 완료로 처리 |
| ⑤ 기기 세션 정리 | 앱 clearAuthSessionAfterAccountDeletion | 로그 | 삭제됨 | 앱 재시작 시 세션 복원 실패(refresh 400) → 로그인 화면 |
| ⑥ 기기 데이터 정리 | 앱 controller: owner 데이터·초안·보관함·대기열 | 로그(실패해도 삭제는 끝난 것) | 삭제됨 | 남은 행은 옛 owner 키 아래 고립 — 새 로그인은 다른 uid |
서로 다른 시스템의 작업이라 원자적이지 않다. 위 표가 그 사실을 그대로 적은 것이며, 각 단계는 멱등(같은 요청을 다시 보내면 이어서 진행)이다. ④ 만이 되돌릴 수 없다.
삭제된 계정이 부활하지 않는 근거: 유저 키를 가진 모든 public 표는 auth.users 로 FK 연쇄(pgTAP account_deletion_residue_scan 이 표·열을 전수 발견해 잔존 0 을 단언). 삭제 뒤 아직 만료 안 된 access token 으로 오는 쓰기 RPC 는 FK 위반으로 거부되고, refresh 는 refresh_token_not_found 로 끝난다(실측). 같은 사람이 같은 provider 로 다시 로그인하면 새 uid 가 생기며 옛 데이터·기기 대기열 행(옛 owner 키)은 그 계정에 붙지 않는다. 새 auth 유저의 프로필 행은 트리거 handle_new_auth_user_profile 이 만든다 — 옛 프로필을 되살리는 경로는 없다.
4. 로그인·계정 연결·콜백·토큰 갱신·로그아웃의 상태 전이
4-1. provider 별 차이 vs 공통 정책
| 층 | 소유 | 내용 |
|---|---|---|
| provider 정의(작은 adapter) | services/auth/providers/{kakao,google,apple}.ts | 버튼 문구·계정/프로필 라벨·OAuth scope(kakao: openid)·실패 문구. 정책·분기 없음 |
| 공통 시작 | socialAuthFlow.beginSocialAuth | 웹: signInWithOAuth/linkIdentity(PKCE) → provider 로 이동. 네이티브: 같은 authorize URL 을 startSocialLogin 메시지로 셸에 주고 거래 ID 를 등록(§4-3) |
| 공통 콜백 | supabaseAuth.completeSupabaseOAuthRedirect | ?code= 를 exchangeCodeForSession 으로 교환. 동시 교환은 coordinator 가 하나로 합친다(authOAuthCallbackPolicy) |
| provider 별 실제 차이 | 유지 | Apple iOS 는 시스템 시트의 id_token + 원문 nonce 로 signInWithIdToken(콜백 URL 없음). Kakao 는 삭제 때 Admin 키 unlink, Apple 은 삭제 때 재인증 code 로 revoke, Google 은 revoke 의무 없음 |
범용 boolean 엔진·복제 콜백 판단은 두지 않는다 — provider 마다 다른 것은 위 표의 "실제 차이" 뿐이다.
4-2. 웹 계정 연결(link)이 다른 계정에 적용되지 않는 이유 — PKCE 검증값이 결합자다
유저 A 가 프로필에서 [Google 연결] 을 누르면 SDK 가 PKCE 검증값(code verifier)을 이 브라우저 저장소에 적고 provider 로 간다. 돌아온 ?code= 는 그 검증값이 있어야만 세션으로 바뀐다. 그 사이 A 가 로그아웃하거나(forceClearPersistedAuthState 가 검증값 키까지 지운다) 다른 탭에서 B 로 로그인하면(새 검증값이 덮어쓴다) A 의 옛 콜백은 교환에 실패하고 앱은 현재 세션(B)을 유지한 채 "연결 실패" 표시만 한다(markSocialIdentityLinkResult(error)). 성공 표시는 콜백 query 가 아니라 getUserIdentities() 재조회로만 한다. 이미 다른 Barbelic 계정에 붙은 identity 는 병합하지 않고 LG_SOCIAL_IDENTITY_LINK_CONFLICT 로 끝난다.
4-3. 네이티브 콜백의 앱 쪽 거래 대조 (services/auth/nativeAuthTransactions.ts)
| 콜백 | 판정 | 앱 동작 |
|---|---|---|
| 진행 중 거래와 ID·provider·action 이 같다 | accepted | code 교환 → Keychain 저장 ACK → native-auth-complete { ok: true } → 거래 소비 |
| 진행 중 거래와 ID 가 다르다(늦은 옛 콜백·뒤바뀐 순서) / provider·action 이 다르다 / 진행 중 거래가 10분을 넘겼다 | stale | 거부: 교환 0회, native-auth-complete { ok: false, error: "앱 로그인 결과가 현재 요청과 일치하지 않습니다…" }(LG_SOCIAL_AUTH_STALE_CALLBACK). 진행 중 거래는 살려 둔다 — 올바른 콜백이 아직 올 수 있다 |
| 이미 소비한 거래 ID(같은 콜백 재전달) | duplicate | 조용히 무시: 교환 0회, 이벤트 0건 |
| 진행 중 거래가 없다(콜백 도중 WebView 재시작) | accepted(transaction null) | 네이티브 셸의 1회 소비·TTL 검증을 신뢰하고 진행 |
네이티브 셸(N01)의 검증(native-bridge.md startSocialLogin·barbelic:native-auth)은 그대로다 — 이 장부는 앱이 자기 몫을 하는 두 번째 그물이다. 한 시점에 하나의 소셜 요청만 허용(LG_SOCIAL_AUTH_ALREADY_PENDING)하는 규칙도 그대로다.
4-4. 토큰 갱신·로그아웃
| 사건 | 동작 |
|---|---|
| 토큰 갱신 | SDK autoRefreshToken. 네이티브는 갱신 세션을 persistNativeAuthSession 으로 Keychain 에 비동기 저장(같은 refresh token 은 1회). 앱 재시작 시 restoreNativeAuthSession 이 Keychain 의 refresh token 으로 복원 — 실패가 종료형(§6 세션 종료)이면 저장물을 지우고, 재시도형이면 1회 재시도 |
| 로그아웃(이 기기) | signOut() = scope local: 서버가 이 세션의 refresh token 폐기(10초 제한, 실패해도 로컬은 지움) → 기기 세션 흔적(SDK 저장 키·PKCE·barbelic:auth-owner:v1)·Keychain 삭제 → SIGNED_OUT |
| 모든 기기 로그아웃 | signOutAllDevices() = scope global: 서버가 그 유저의 모든 세션 폐기, 나머지는 같다 |
| 세션 만료 | 갱신이 refresh_token_not_found 등 종료형으로 끝나면 앱 셸이 세션 재확인 뒤 로그아웃 화면으로(authSessionFailurePolicy). 서버 요청은 없다 |
5. 로그아웃·세션 만료·계정 삭제의 보존 정책 (A07 인계)
| 사건 | 서버 | 기기 세션 흔적 | owner 데이터 사본(화면 cache·기기 사본) | 초안·보관함·미전송 대기열 | 다음 로그인 |
|---|---|---|---|---|---|
| 로그아웃(local/global) | 세션 폐기 | 삭제 | purgeAuthenticatedOwnerData(owner) — 화면·기기 사본 정리 | 보존(같은 owner 로 재로그인하면 이어짐 — runSignOut 은 운동 데이터를 지우지 않는다) | 같은 uid → 대기열 계속 전송 |
| 세션 만료 | 없음(이미 없음) | SDK 가 비움 | 셸 전환(transitionToSignedOut) | 보존 | 같은 uid → 이어짐 |
| 계정 삭제 | 유저·DB 연쇄·스토리지 삭제 | 삭제(네트워크 0회) | purgeAuthenticatedOwnerData(owner) | 삭제(purgeLocalWorkoutDataForDeletedAccount(owner) — 초안·보관함·오프라인 대기열) | 새 uid(다른 계정). 옛 owner 행은 붙지 않는다 |
| 관리자 열람 시작/종료 | 대상 세션 발급 / 폐기 | 관리자 세션 보관 → 복원 | 대상 데이터는 읽기 전용 | 대상 초안을 만들지 않는다(가드) | 관리자 세션 그대로 |
A07 은 이 표를 owner epoch 전환의 입력으로 쓴다: 로그아웃·만료는 "같은 owner 가 돌아올 수 있다", 삭제는 "이 owner 는 끝났다" 다. 삭제 성공(200/410) 이전에 회복 가능한 로컬 데이터를 지우지 않는다(accountDeletionClient 가 성공을 돌려준 뒤 controller 가 지운다).
6. 인증 오류 분류와 앱 공개 오류 코드 (services/auth/authErrors.ts)
classifyAuthError(error) → { kind, code, retryable, message }:
| kind | 뜻 | 판정 | 앱 반응 |
|---|---|---|---|
retryable | 네트워크·5xx·408/425/429·로그인 준비 중(LG_AUTH_SESSION_NOT_READY)·부팅 시간 초과 | isRetryableAuthError | 로그아웃하지 않고 재시도 |
session_terminal | session_expired·session_not_found·refresh_token_not_found·refresh_token_already_used·invalid_refresh_token 또는 그 뜻의 메시지 | isAuthSessionError(재시도형이 아닐 때만) | 세션 재확인 뒤 로그아웃 화면. 로컬 데이터 보존(§5) |
user_facing | PublicSocialAuthError — 앱이 정한 코드·문구 | instanceof | 문구 그대로 표시 |
unknown | 그 밖(401 단독 포함) | — | 문맥 기본 문구. 401 만으로 로그아웃하지 않는다 |
공개 오류 코드(PublicSocialAuthError.code): LG_SOCIAL_AUTH_UNAVAILABLE(file: 주소) · LG_SOCIAL_AUTH_PROVIDER_DISABLED · LG_SOCIAL_AUTH_ALREADY_PENDING · LG_SOCIAL_AUTH_APP_UPDATE_REQUIRED · LG_SOCIAL_AUTH_CALLBACK_CONFIG · LG_SOCIAL_AUTH_SECURE_STORAGE · LG_SOCIAL_AUTH_STALE_CALLBACK(신규, §4-3) · LG_SOCIAL_IDENTITY_ALREADY_LINKED · LG_SOCIAL_IDENTITY_LINK_DISABLED · LG_SOCIAL_IDENTITY_LINK_CONFLICT · LG_SOCIAL_IDENTITY_LINK_SESSION · LG_SOCIAL_IDENTITY_LINK_FAILED. 계정 삭제 클라이언트 코드: IMPERSONATION_READ_ONLY · ACCOUNT_DELETE_NO_SESSION · 서버 오류 코드 그대로(§2-2) + retryable.
7. 토큰·비밀이 실리지 않는 곳 (점검 결과)
| 곳 | 결과 |
|---|---|
| 서버 오류 로그 | 허용 목록 필드만 + 비밀 모양 키 차단(§2-1). 테스트가 고정 |
| 임퍼서네이션 감사 로그 | actor/target id 만 |
앱 인증 디버그 로그(authDebug) | code 길이·이벤트 이름·host 만. code·토큰 없음(viteServices 테스트) |
| 클라이언트 번들 | app-config.js 는 공개 키만. 시험 관리자 비밀은 e2e 가 URL 로 줄 때만 존재하고 처리 뒤 URL 에서 지운다 |
| 네이티브 콜백 URL | access/refresh/id token 없음, code 만(authPkceHardCut·iOS 계약 테스트) |
| 일반 DTO | 서버 응답에 토큰이 실리는 것은 세션 발급이 목적인 세 곳(impersonate·test-admin·SDK 교환)뿐 |
8. 인계
| 받는 작업 | 이 문서에서 확정된 것 | 그 작업이 하는 것 |
|---|---|---|
| A07 (#1335) | §5 보존 정책 표, §6 오류 분류(classifyAuthError), 삭제 결과(200/410 = 삭제됨) | AuthSessionRuntime 입력으로 소비. owner epoch 전환 계기 = 로그인 성공·로그아웃·만료·삭제 성공 |
| N01 | §4-3 거래 장부(앱은 셸 검증을 신뢰하되 자기 장부와 대조), Keychain 저장 ACK 계약 불변 | native/bridge/secure storage 구현 |
| A05 | §2-3 관리자 열람 경계·amr 행위자 판정(삭제만 적용) | 다른 관리자·쓰기 경계에 같은 판정을 넣을지 |
| R02·R05 | 실 provider 시험 미실행 목록: Kakao unlink 실호출, Apple 재인증 revoke 실호출, Google/Apple/Kakao 실계정 로그인·연결 | 승인된 시험 계정으로 실행. 재현법: external-auth-smoke.yml(test:e2e-provider-assumed-auth) + 로컬 handler 는 scratchpad 프로브처럼 샌드박스 Auth 에 대고 실행 |
9. 검증 (2026-09-07)
- 단위(네트워크 없음): 서버 handler 4개 전 분기 39건, 거래 장부 6, 오류 분류 3, 삭제 응답 해석 2, 열람 세션 폐기 3, 실제
supabaseAuth배선 콜백 대조 1. - 로컬 Supabase 샌드박스 실측: 삭제된 유저 토큰
/auth/v1/user→403 user_not_found, refresh →400 refresh_token_not_found; magic link 세션amr=[{method:"otp"}](refresh 뒤 유지), 비밀번호 로그인 =password;logout?scope=local204 → refresh 400; 실제api/account/delete.js를 대고 200 → 410, 확인 문구 틀림 400(유저 유지), magic link 세션 403(유저 유지). - 미실행: 실 provider(Kakao/Google/Apple) 호출. §8 R02·R05.
심사 계정의 숨김 이메일 로그인 (#1568)
일반 화면은 소셜 버튼을 유지한다. 로그인 로고5회 활성화로만 이메일/비밀번호 창을 연다. 인증은 AuthPort.startReviewLogin → 일반 Supabase password Auth이며, 서버 관리 app_metadata에 barbelic_review_account: true가 없는 계정은 앱 세션에 전달하지 않는다. 임시 client는 session persistence/auto refresh/URL detection을 끄고, 승인된 세션만 기존 client의 setSession과 기존 auth runtime으로 넘긴다. 소셜 인증·권한·쓰기 정책·기존 로그아웃/계정 삭제의 의미는 유지한다. UI 진입을 숨긴 것은 인증 수단을 대체하는 보안 조건이 아니다. 절차는 심사 계정 안내를 참고한다.