1
0
forked from baron/baron-sso

레포 업데이트

This commit is contained in:
Lectom C Han
2026-04-01 20:32:09 +09:00
parent 8bab8d44cc
commit 4b0fbdde98
31 changed files with 1636 additions and 43 deletions

View File

@@ -0,0 +1,74 @@
# Backend Log Policy
## 1. 목적
- Backend 서버 로그의 기본 레벨과 운영 중 일시적 디버그 확장 규칙을 정의합니다.
- 운영 환경에서 과도한 로그 노출을 피하면서, 장애 분석이 필요한 경우에는 명시적으로 진단 로그를 확장할 수 있게 합니다.
## 2. 기준 변수
- `APP_ENV`
- `BACKEND_LOG_LEVEL` (선택 override)
## 3. 기본 동작
### 3.1 기본 레벨 결정
- `APP_ENV=dev|local|development`
- 기본 로그 레벨: `debug`
- 기본 출력 형식: text
- 그 외 환경(`stage`, `production`, `prod` 등)
- 기본 로그 레벨: `info`
- 기본 출력 형식: JSON
### 3.2 명시 override
- `BACKEND_LOG_LEVEL`이 설정되면 `APP_ENV` 기본값보다 우선합니다.
- 허용 값:
- `debug`
- `info`
- `warn`
- `error`
예시:
```env
APP_ENV=stage
BACKEND_LOG_LEVEL=debug
```
위 설정이면 stage 환경이더라도 backend `slog``debug`로 동작합니다.
## 4. 운영 가이드
- 운영/스테이징에서 장애 분석이 필요할 때만 `BACKEND_LOG_LEVEL=debug`를 일시적으로 설정합니다.
- 이슈 분석이 끝나면 즉시 기본값으로 되돌리는 것을 권장합니다.
- 기본 레벨(`info`)에서는 핵심 상태 변화와 경고/오류를 중심으로 남기고, 디버그 전용 진단 필드는 숨깁니다.
## 5. Headless Login 진단 로그
- `POST /api/v1/auth/headless/password/login` 같은 headless 로그인 경로는 기본적으로 `reason_code` 중심의 구조화 로그를 남깁니다.
- `BACKEND_LOG_LEVEL=debug` 또는 `APP_ENV=dev|local|development`일 때만 아래 진단 필드를 추가로 남깁니다.
- `expected_audiences`
- `received_audiences`
- `received_kid`
- `claim_issuer`
- `claim_subject`
- `claim_expires_at`
- `claim_not_before`
- `claim_issued_at`
- 민감 정보는 계속 로그에 남기지 않습니다.
- raw `client_assertion`
- password
- session token / cookie
## 6. 구현 위치
- logger 초기화: `backend/cmd/server/main.go`
- 레벨 결정 로직: `backend/internal/logger/logger.go`
- headless 로그인 debug 진단 필드: `backend/internal/handler/auth_handler.go`
## 7. 검증
```bash
cd backend
go test ./internal/logger -v
go test ./cmd/server -run 'TestNewErrorHandler_' -v
go test ./internal/handler -run 'TestHeadlessPasswordLogin_(DebugLogIncludesDiagnostics|InfoLogOmitsDebugDiagnostics)$' -v
```
## 8. 관련 문서
- `README.md`
- `docs/client-log-policy.md`
- wiki update draft: `docs/wiki-error-handling-policy-backend-log-update.md`

View File

@@ -65,3 +65,7 @@ flutter test test/log_policy_test.dart
## 6. 운영 가이드
- 운영에서 디버그 로그가 필요하면 `CLIENT_LOG_DEBUG=true`를 명시적으로 설정하고, 이슈 해결 후 즉시 원복합니다.
- 운영에서도 민감정보 마스킹은 항상 강제되며 비활성화할 수 없습니다.
## 7. 참고
- Backend 서버 자체의 `slog` 레벨 정책은 별도로 관리합니다.
- 관련 문서: `docs/backend-log-policy.md`

View File

@@ -0,0 +1,150 @@
# Headless Login Debug Improvement Implementation Plan
> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Make headless password login failures return safe reason-specific response codes and produce debug-only diagnostic logs.
**Architecture:** Keep the change local to `AuthHandler` by introducing a headless-password-specific error classification helper and reusing it across response mapping and structured logging. Preserve current success-path behavior while adding explicit tests for reason codes and debug-field gating.
**Tech Stack:** Go, Fiber, `log/slog`, `stretchr/testify`, `go-jose`
---
### Task 1: Add failing response-classification tests
**Files:**
- Modify: `backend/internal/handler/auth_handler_login_test.go`
- Reference: `backend/internal/handler/auth_handler.go`
- [ ] **Step 1: Write failing tests for classified 401 responses**
Add tests for:
- audience mismatch -> `code=invalid_client_assertion_audience`
- signature mismatch -> `code=invalid_client_assertion_signature`
- iss/sub mismatch -> `code=invalid_client_assertion_iss_sub`
- expired assertion -> `code=invalid_client_assertion_expired`
- invalid credentials -> `code=password_or_email_mismatch`
- [ ] **Step 2: Run only the new response tests and verify they fail for the expected reason**
Run:
```bash
cd backend && go test ./internal/handler -run 'TestHeadlessPasswordLogin_(AudienceMismatchReturnsDetailedCode|SignatureMismatchReturnsDetailedCode|IssSubMismatchReturnsDetailedCode|ExpiredAssertionReturnsDetailedCode)|TestPasswordLogin_InvalidCredentials_ReturnsCode' -v
```
Expected:
- new headless response tests fail because the handler still returns generic codes/messages
- existing invalid-credentials test remains green unless intentionally updated
### Task 2: Add failing log-gating tests
**Files:**
- Modify: `backend/internal/handler/auth_handler_login_test.go`
- Reference: `backend/internal/logger/logger.go`
- [ ] **Step 1: Write tests for debug-only diagnostic logging**
Add tests that install a temporary `slog` handler backed by a buffer and assert:
- debug logger emits `expected_audiences` and `received_audiences`
- info logger does not emit those debug-only fields
- [ ] **Step 2: Run only the new log tests and verify they fail**
Run:
```bash
cd backend && go test ./internal/handler -run 'TestHeadlessPasswordLogin_(DebugLogIncludesDiagnostics|InfoLogOmitsDebugDiagnostics)' -v
```
Expected:
- tests fail because the current handler does not emit reason-specific structured logs yet
### Task 3: Implement classified headless assertion failures
**Files:**
- Modify: `backend/internal/handler/auth_handler.go`
- [ ] **Step 1: Add a local classified error type and helpers**
Implement a small helper shape for:
- `status`
- `code`
- `safeMessage`
- `logMessage`
- `debugAttrs`
- [ ] **Step 2: Make claim validation return classified reasons**
Replace generic string errors from `validateHeadlessClientAssertionClaims` with typed/classified failures for:
- iss/sub mismatch
- expired
- not active yet
- iat in future
- audience mismatch
- [ ] **Step 3: Make assertion verification return signature/jwks-specific failures**
Ensure parse, jwks load, claims, and signature-verification paths map to distinct safe codes/messages.
### Task 4: Implement structured logging and handler integration
**Files:**
- Modify: `backend/internal/handler/auth_handler.go`
- [ ] **Step 1: Add a helper to detect whether debug logging is enabled**
Use the default `slog` logger handler to gate diagnostic fields without changing global logger initialization.
- [ ] **Step 2: Log classified assertion failures in `HeadlessPasswordLogin`**
Log:
- reason code
- client id
- path
- debug-only attrs when debug logging is enabled
- [ ] **Step 3: Log classified credential failures**
Keep response behavior safe while adding a structured log line for credential mismatch.
### Task 5: Verify and document
**Files:**
- Modify: `docs/test-plan/backend-test-inventory.md` if headless password login inventory needs expansion
- Modify: issue `#502` comment with failing-test evidence and implementation summary
- [ ] **Step 1: Run focused handler tests**
Run:
```bash
cd backend && go test ./internal/handler -run 'TestHeadlessPasswordLogin_|TestPasswordLogin_InvalidCredentials_ReturnsCode' -v
```
Expected:
- all targeted tests pass
- [ ] **Step 2: Run broader backend handler regression tests if practical**
Run:
```bash
cd backend && go test ./internal/handler -v
```
Expected:
- no regressions in auth handler tests
- [ ] **Step 3: Update issue `#502` with what failed first, what changed, and what passed**
- [ ] **Step 4: Review whether `docs/test-plan/backend-test-inventory.md` or troubleshooting docs should be updated**

View File

@@ -0,0 +1,146 @@
# Headless Login Debug and Error Classification Design
## Scope
This design covers issue `#502` only.
- Target endpoint: `POST /api/v1/auth/headless/password/login`
- Goal: classify 401 failure reasons for headless password login, return a safe response to RP clients, and emit richer diagnostics only when debug logging is enabled
- Non-goal: introduce a shared auth-wide error/observability layer across all handlers. That follow-up work is tracked separately in issue `#503`.
## Problem
Current behavior makes production debugging slow.
- Multiple assertion failures collapse into `invalid_client_assertion`
- The handler often returns early without emitting a reason-specific log line
- RP clients cannot distinguish safe failure categories such as audience mismatch vs signature mismatch
- Server operators cannot quickly tell whether the failure came from client assertion validation or user credential validation
## Constraints
- Response policy must follow the agreed level `2`: return `code + short safe message`
- Debug logs may include level `3`-style diagnostics, but only when the logger is configured for debug level
- Sensitive values must never be logged: raw `client_assertion`, password, session token, cookies
- Existing successful headless password login flow must remain unchanged
- Existing policy requires failing tests first
## Recommended Approach
Introduce a small headless-password-specific failure classification layer inside `auth_handler.go`.
The classification layer should map each failure into:
- `status`
- `code`
- `safeMessage`
- `logMessage`
- `debugFields`
This stays local to the current handler so the change remains small, but the structure should be reusable enough to extract later in issue `#503`.
## Failure Categories
### Client assertion failures
- `invalid_client_assertion_parse`
- `invalid_client_assertion_signature`
- `invalid_client_assertion_iss_sub`
- `invalid_client_assertion_expired`
- `invalid_client_assertion_not_before`
- `invalid_client_assertion_iat_future`
- `invalid_client_assertion_audience`
- `invalid_client_assertion_jwks_load`
### Credential failure
- `password_or_email_mismatch`
## Response Contract
Responses should expose only safe details.
Examples:
- `{"code":"invalid_client_assertion_audience","error":"Client assertion audience mismatch"}`
- `{"code":"invalid_client_assertion_signature","error":"Client assertion signature verification failed"}`
- `{"code":"password_or_email_mismatch","error":"Invalid credentials"}`
The response must not echo:
- expected vs received audience values
- full claim payload
- kid mismatch internals beyond the safe message
- raw token values
## Logging Strategy
### Normal log level
Emit one structured warning/error log per classified failure with:
- `reason_code`
- `client_id`
- `path`
- `req_id` if present in context/log pipeline
Message examples:
- `headless password login client assertion failed`
- `headless password login credential authentication failed`
### Debug log level
Add diagnostic fields that are useful for root cause analysis:
- `expected_audiences`
- `received_audiences`
- `received_kid`
- `jwks_refreshed`
- `claim_issuer`
- `claim_subject`
- `claim_expires_at`
- `claim_not_before`
- `claim_issued_at`
- `login_challenge_prefix`
The `login_challenge` should be truncated before logging.
## Implementation Shape
Inside `backend/internal/handler/auth_handler.go`:
1. Add a small internal type for classified headless assertion errors.
2. Make `validateHeadlessClientAssertionClaims` return structured reasons instead of generic strings.
3. Make `verifyHeadlessClientAssertion` return a classified failure that the handler can both:
- convert to a safe HTTP response
- log with optional debug fields
4. Add a small helper that checks whether debug logging is enabled for the current default `slog` logger.
5. Log credential authentication failures with their reason code as well.
## Testing
Add failing tests first in `backend/internal/handler/auth_handler_login_test.go`.
Required tests:
- audience mismatch returns `401 + invalid_client_assertion_audience`
- signature mismatch returns `401 + invalid_client_assertion_signature`
- iss/sub mismatch returns `401 + invalid_client_assertion_iss_sub`
- expired assertion returns `401 + invalid_client_assertion_expired`
- invalid credentials still returns `401 + password_or_email_mismatch`
- debug logger captures diagnostic fields for assertion audience mismatch
- non-debug logger does not emit debug-only fields
## Risks
- Over-classifying too much detail into RP responses could create unnecessary information disclosure
- Logging helper implementation must not assume a specific handler type beyond standard `slog`
- Existing tests may assume the older generic `invalid_client_assertion` code and will need intentional updates
## Success Criteria
- Operators can distinguish signature, claims, and credential failures from logs
- RP clients get safe but actionable reason codes
- Debug mode includes additional diagnostics without leaking secrets
- Existing success path and previously covered headless login behavior remain green

View File

@@ -9,6 +9,8 @@
| `backend/cmd/server/error_handler_test.go:48` | `TestNewErrorHandler_ProductionPassesClientError` | 오류/예외/거부 경로 검증 |
| `backend/cmd/server/error_handler_test.go:73` | `TestNewErrorHandler_DevelopmentReturnsOriginalServerError` | 오류/예외/거부 경로 검증 |
| `backend/cmd/server/error_handler_test.go:98` | `TestNewErrorHandler_MapsUnauthorizedCode` | 오류/예외/거부 경로 검증 |
| `backend/cmd/server/headless_login_e2e_test.go:338` | `TestHeadlessPasswordLogin_E2E_ResponseIncludesDetailedCodeAndLogs` | 실제 app 경로에서 headless login 401 응답 본문과 구조화 로그를 함께 검증 |
| `backend/cmd/server/headless_login_e2e_test.go:382` | `TestHeadlessPasswordLogin_E2E_DebugLogsIncludeDiagnostics` | 실제 app 경로에서 debug 레벨일 때만 headless 진단 필드가 로그에 포함되는지 검증 |
| `backend/internal/handler/api_key_handler_test.go:19` | `TestApiKeyHandler_CreateApiKey` | 핵심 CRUD/서비스 동작 검증 |
| `backend/internal/handler/api_key_handler_test.go:41` | `TestApiKeyHandler_Validation` | 유효성/정책/유틸 검증 |
| `backend/internal/handler/auth_handler_async_test.go:198` | `TestSignup_AsyncDB_Isolation` | 복구/격리/회복 탄력성 검증 |
@@ -23,6 +25,13 @@
| `backend/internal/handler/auth_handler_login_test.go:114` | `TestPasswordLogin_OIDC_Success` | 인증/OIDC 플로우 검증 |
| `backend/internal/handler/auth_handler_login_test.go:201` | `TestPasswordLogin_OIDC_InactiveClient` | 오류/예외/거부 경로 검증 |
| `backend/internal/handler/auth_handler_login_test.go:255` | `TestPasswordLogin_NoOIDC_Success` | 인증/OIDC 플로우 검증 |
| `backend/internal/handler/auth_handler_login_test.go:940` | `TestHeadlessPasswordLogin_InvalidClientAssertionRejected` | headless 로그인 서명 검증 실패 응답 코드 검증 |
| `backend/internal/handler/auth_handler_login_test.go:1033` | `TestHeadlessPasswordLogin_AudienceMismatchReturnsDetailedCode` | headless 로그인 audience mismatch 분류 검증 |
| `backend/internal/handler/auth_handler_login_test.go:1062` | `TestHeadlessPasswordLogin_IssSubMismatchReturnsDetailedCode` | headless 로그인 iss/sub mismatch 분류 검증 |
| `backend/internal/handler/auth_handler_login_test.go:1102` | `TestHeadlessPasswordLogin_ExpiredAssertionReturnsDetailedCode` | headless 로그인 만료 assertion 분류 검증 |
| `backend/internal/handler/auth_handler_login_test.go:1142` | `TestHeadlessPasswordLogin_DebugLogIncludesDiagnostics` | debug 로그에서만 진단 필드가 노출되는지 검증 |
| `backend/internal/handler/auth_handler_login_test.go:1174` | `TestHeadlessPasswordLogin_InfoLogOmitsDebugDiagnostics` | 일반 로그에서 debug 진단 필드가 숨겨지는지 검증 |
| `backend/internal/handler/auth_handler_login_test.go:1442` | `TestPasswordLogin_InvalidCredentials_ReturnsCode` | 비밀번호 불일치 응답 코드 검증 |
| `backend/internal/handler/auth_handler_oidc_test.go:106` | `TestAcceptOidcLoginRequest_TokenFallbackToCookie` | 복구/격리/회복 탄력성 검증 |
| `backend/internal/handler/auth_handler_oidc_test.go:21` | `TestAcceptOidcLoginRequest_CookieOnly` | 인증/OIDC 플로우 검증 |
| `backend/internal/handler/auth_handler_otp_test.go:14` | `TestHandleKratosCourierRelay_Email` | 인증/OIDC 플로우 검증 |

View File

@@ -0,0 +1,307 @@
# Wiki Update Draft: Error-Handling-Policy
대상 위키 페이지:
- `Error-Handling-Policy`
이 문서는 위키에 바로 반영할 수 있도록, 기존 `Error Handling Policy` 원문 구조를 유지하면서 최근 backend 로그 정책과 headless login 디버그 규칙까지 함께 정리한 초안입니다.
반영 의도:
- 기존 whitelist 중심의 prod 에러 노출 정책을 유지합니다.
- 최근 추가된 headless login 세분화 오류 코드와 backend debug log 규칙을 같은 문맥에서 관리합니다.
- UI 노출 정책과 backend API 응답 계약을 분리해 해석합니다.
---
# Error Handling Policy
본 문서는 이슈 `#164`([UserFront] 에러 노출 whitelist 정의 및 적용)를 기준으로 정리한 **프로덕션 에러 노출 정책**입니다.
## 0) 범위와 해석 기준
- 본 정책은 `userfront`, `adminfront`, `devfront`, `backend`가 공통으로 참고하는 에러 노출 기준입니다.
- `Backend`는 기계 판독 가능한 `code`와 안전한 짧은 `error` 문자열을 내려주는 책임을 가집니다.
- `Front``code`를 기준으로 사용자 문구를 번역/매핑해 표시합니다.
- 따라서 아래 표에서 한국어 문구는 **최종 사용자 노출 기준**, 영문 `error` 문자열은 **backend 응답 예시**로 해석합니다.
## 1) 기본 원칙
- 프로덕션은 **whitelist 방식**만 허용합니다.
- whitelist에 없는 에러는 **일반 오류 메시지**로 대체합니다.
- 상세 원문 메시지는 **프로덕션에서 비노출**합니다.
- **Ory Stack(Kratos/Hydra/Oathkeeper)에서 발생한 에러 코드는 그대로 pass-through** 합니다.
- **Custom 에러만 whitelist로 관리**합니다. (Ory 코드 제외)
## 2) 노출 정책 및 Ory 에러 처리 (통합)
### 2.1 Ory 에러 pass-through 원칙
- **Ory Stack(Kratos/Hydra/Oathkeeper)에서 발생한 에러 코드는 그대로 pass-through** 합니다.
- Ory 에러는 **whitelist 대상에서 제외**합니다.
- 단, 보안/UX 관점의 **blacklist 후보**는 예외적으로 `unknown_error`로 치환할 수 있습니다.
### 2.2 Custom whitelist (Ory 매핑 없음)
Ory 스택에서 발생하지 않으며 **매핑 대상이 없는 Custom 에러**만 관리합니다.
이 중 **HTTP 상태 코드와 일치하는 항목**은 별도 표로 구분합니다.
#### 2.2.1 HTTP status와 일치하는 Custom 에러
| error_code | http_status | 사용자 메시지 (기본) | Backend `error` 예시 | 설명 |
|---|---:|---|---|
| `not_found` | 404 | 요청한 페이지를 찾을 수 없습니다. | Not found | 경로 오류 |
| `rate_limited` | 429 | 요청이 많습니다. 잠시 후 다시 시도해 주세요. | Too many requests | 제한 초과 |
#### 2.2.2 HTTP status와 무관한 Custom 에러
| error_code | 사용자 메시지 (기본) | Backend `error` 예시 | 설명 |
|---|---|---|
| `password_or_email_mismatch` | 이메일 혹은 비밀번호가 일치하지 않습니다. | Invalid credentials | 비밀번호 입력 오류 |
| `invalid_client_assertion_parse` | 클라이언트 인증 정보 형식이 올바르지 않습니다. | Client assertion format is invalid | headless login assertion 형식 오류 |
| `invalid_client_assertion_signature` | 클라이언트 인증 정보 검증에 실패했습니다. | Client assertion signature verification failed | headless login assertion 서명 검증 실패 |
| `invalid_client_assertion_iss_sub` | 클라이언트 인증 정보 발급 주체가 올바르지 않습니다. | Client assertion issuer or subject mismatch | headless login assertion `iss/sub` 불일치 |
| `invalid_client_assertion_expired` | 클라이언트 인증 정보가 만료되었습니다. | Client assertion has expired | headless login assertion 만료 |
| `invalid_client_assertion_not_before` | 클라이언트 인증 정보가 아직 활성 상태가 아닙니다. | Client assertion is not active yet | headless login assertion 활성 시각 전 |
| `invalid_client_assertion_iat_future` | 클라이언트 인증 정보 발급 시각이 올바르지 않습니다. | Client assertion issued-at time is invalid | headless login assertion `iat` 미래 시각 |
| `invalid_client_assertion_audience` | 클라이언트 인증 정보 대상이 일치하지 않습니다. | Client assertion audience mismatch | headless login assertion `aud` 불일치 |
| `invalid_client_assertion_jwks_load` | 클라이언트 공개키 검증에 실패했습니다. | Headless login jwks verification failed | headless login `jwksUri` 조회/파싱 실패 계열 |
### 2.3 HTTP status 핸들링 정책
- **Ory error 코드가 존재하면 pass-through 우선**합니다.
- Ory error 코드가 없고, **HTTP status가 404/429**인 경우:
- `404` -> `not_found`
- `429` -> `rate_limited`
- 위 조건에 해당하지 않는 Custom 에러는 **기본 정책(`unknown_error`)**을 적용합니다.
### 2.4 비노출(기본) 에러 처리
whitelist에 없는 모든 **Custom 에러**는 아래 공통 처리 규칙을 따릅니다.
- 사용자 메시지: **"일시적인 오류가 발생했습니다. 잠시 후 다시 시도해 주세요."**
- 오류 종류는 `unknown_error`로 고정합니다.
- 상세 원문 메시지는 사용자에게 표시하지 않습니다.
### 2.5 Ory 에러 blacklist 후보 (검토용)
Ory 에러를 pass-through 하더라도, 아래 유형은 **보안/UX 관점에서 숨김 처리(blacklist)** 후보입니다.
- `security_csrf_violation`
- `security_identity_mismatch`
- `browser_location_change_required`
- `server_error`
- `temporarily_unavailable`
> **제안**: 위 코드는 prod에서 `unknown_error`로 치환하고, log/audit에만 원문을 남기는 방식이 안전합니다.
### 2.6 Oathkeeper 경유 에러 처리
현재 설정(`docker/ory/oathkeeper/oathkeeper.yml`)에는 에러 변환 로직이 없고, `errors.fallback: json`만 정의되어 있습니다.
즉, Oathkeeper는 **에러 코드를 변환하지 않고 JSON으로 그대로 반환**합니다.
따라서 Ory Stack 에러는 **Oathkeeper를 통과하더라도 그대로 유지**된다고 가정합니다.
## 3) UI 정책
- 공통 에러 화면에는 아래 항목을 표시합니다.
- 제목
- 사용자 메시지
- **오류 종류(`error_code`)**
- **홈으로 이동 버튼**
- `error_id`가 있는 경우에만 표시합니다.
- Backend의 `error` 문자열은 최종 사용자 문구의 Source of Truth가 아닙니다.
- Front는 가능하면 `code` 기준으로 번역 리소스를 선택하고, `error`는 fallback 또는 운영 진단 보조 텍스트로만 사용합니다.
## 4) 구현 가이드
- 에러 표시 로직은 **whitelist 검사 후** 결정합니다.
- 예시:
- `if error_code in whitelist: message = whitelist_message`
- `else: error_code = "unknown_error", message = default_message`
## 5) 프로덕션 전용 동작 및 테스트 요구사항
### 5.1 프로덕션 전용 동작
- whitelist 적용(비노출/`unknown_error` 치환)은 **프로덕션에서만** 동작해야 합니다.
- **Ory Stack 에러는 prod에서도 pass-through** 합니다. (단, blacklist 후보는 예외 처리 가능)
- 프로덕션 판정 기준:
- Front: `APP_ENV``prod` 또는 `production`일 때만 활성화
- 테스트/로컬: override 옵션을 사용해 프로덕션/비프로덕션 동작을 강제할 수 있어야 합니다.
### 5.2 테스트 요구사항
최소 아래 케이스를 자동 테스트로 보장합니다.
- **Prod + whitelist 코드**: 사용자 메시지는 whitelist 메시지, `error_code`는 원래 코드 유지
- **Prod + 비-whitelist 코드**: 사용자 메시지는 기본 메시지, `error_code``unknown_error`
- **Prod + `error_id` 없음**: `error_id` 표시 없음
- **Non-prod + `error_code` 존재**: 원본 에러 코드/설명 표시
- **Non-prod + `description` 없음**: 기본 설명 노출
권장 테스트 위치:
- `userfront/test/error_screen_test.dart`
권장 테스트 방식:
- `ErrorScreen(isProdOverride: true/false, ...)`로 환경을 강제하여 동작 검증
- `AuthProxyService.isProdEnv``APP_ENV`에 의존하므로, 테스트에서 직접 환경 변수에 의존하지 않도록 override 사용
### 5.3 재현 테스트 우선 원칙
- 에러 처리 로직 변경 시, 먼저 재현 테스트를 작성합니다.
- 테스트는 최소한 `status`, `code`, `error` 응답 계약을 검증해야 합니다.
- 사용자 노출 메시지는 번역 리소스로 처리하고, API는 기계 판독 가능한 `code`를 우선 계약으로 유지합니다.
- 원문(한글/영문) 에러 문자열이 바뀌어도 `code` 기반 동작은 깨지지 않아야 합니다.
- 운영 이슈에서 확보한 `req_id`, 로그 패턴, 실제 응답 payload는 가능하면 테스트 케이스 설명에 남겨 회귀 근거를 보존합니다.
### 5.4 회귀 테스트 기준
- 인증 실패(예: password mismatch)에서 `code`가 기대값으로 반환되는지 검증합니다.
- 4xx/5xx 주요 에러 경로에 대해 최소 1개 이상의 핸들러 테스트를 유지합니다.
- 운영 이슈로 확인된 에러 케이스는 반드시 회귀 테스트 케이스로 승격합니다.
### 5.5 Headless Login 실패 코드 회귀 기준
Headless login 경로는 기존 generic `invalid_client_assertion`만으로는 운영 진단이 느렸기 때문에, 아래 코드를 별도 회귀 대상으로 유지합니다.
- `invalid_client_assertion_parse`
- `invalid_client_assertion_signature`
- `invalid_client_assertion_iss_sub`
- `invalid_client_assertion_expired`
- `invalid_client_assertion_not_before`
- `invalid_client_assertion_iat_future`
- `invalid_client_assertion_audience`
- `invalid_client_assertion_jwks_load`
- `password_or_email_mismatch`
권장 테스트 위치:
- `backend/internal/handler/auth_handler_login_test.go`
최소 검증 항목:
- 응답 `status`
- 응답 `code`
- 응답 `error`
- debug 레벨에서만 진단 필드가 로그에 포함되는지 여부
## 6) 변경 관리
- 에러 코드 추가/삭제는 **이슈 등록 후** 반영합니다.
- 사용자 메시지는 제품 문구 기준에 따라 수정합니다.
## 7) Ory 에러 코드(참고)
아래는 Ory(Kratos/Hydra)에서 **기본 제공되는 에러 코드**를 참고용으로 정리합니다.
### 7.1 Ory Kratos `error.id`
Kratos Self-Service Flow의 `error.id`는 다음 코드들이 공식 문서/SDK에 명시되어 있습니다.
- `session_inactive`
- `session_already_available`
- `session_aal1_required`
- `session_refresh_required`
- `security_csrf_violation`
- `security_identity_mismatch`
- `browser_location_change_required`
### 7.2 Ory Hydra / OAuth2·OIDC 표준 에러
Hydra는 OAuth2/OIDC 표준 에러 코드(`error` 필드)를 사용합니다.
- OAuth2 표준:
- `invalid_request`
- `unauthorized_client`
- `access_denied`
- `unsupported_response_type`
- `invalid_scope`
- `server_error`
- `temporarily_unavailable`
- OIDC 표준:
- `consent_required`
## 8) 에러 코드 관리 위치 제안 (아키텍처 기준)
에러 코드는 **Backend에서 표준화하고, Front에서 사용자 문구로 매핑**하는 구조가 가장 안전합니다.
### 8.1 Backend (단일 진입점, 표준화의 Source of Truth)
- 외부(Ory Kratos/Hydra/Oathkeeper) 및 내부 에러를 **표준 `error_code`로 변환**하는 로직을 Backend에 둡니다.
- 권장 위치:
- `backend/internal/handler/` 또는 `backend/internal/service/` 하위에 `error_mapper.go` 성격의 모듈
- 예시: `backend/internal/service/error_mapper.go`
- Backend 응답은 다음을 보장합니다.
- `error_code``error_id`를 일관 포맷으로 내려줌
- whitelist 외 코드는 `unknown_error`로 치환 (프로덕션 기준)
### 8.2 Front (문구 매핑, 표현 계층)
- 사용자 메시지와 UI 표시는 Front에서 담당합니다.
- 현재 위치(유지 또는 통합 권장):
- `userfront/lib/core/constants/error_whitelist.dart`
- `userfront/lib/features/auth/presentation/error_screen.dart`
- Admin/DevFront에서도 같은 whitelist를 사용해야 하므로, 아래 중 하나로 통일을 권장합니다.
1. Backend에서 error whitelist 리스트를 내려주는 API 제공
2. 공용 패키지/공용 파일로 관리 후 각 Front에서 참조
### 8.3 Ory Stack / Gateway
- Ory(Kratos/Hydra)와 Oathkeeper는 **원문 에러만 발생시키고 표준화는 하지 않음**을 원칙으로 합니다.
- Gateway/Proxy 레이어는 **에러 코드를 변환하지 않음**이 안전합니다.
## 9) Backend Log Level Policy
에러 응답 정책과 운영 디버깅 정책은 분리하되, 실제 운영에서 함께 보게 되는 경우가 많으므로 backend 로그 레벨 규칙을 같이 관리합니다.
### 9.1 기준 변수
- `APP_ENV`
- `BACKEND_LOG_LEVEL` (optional override)
### 9.2 기본 규칙
- `APP_ENV=dev|local|development`
- backend `slog` 기본 레벨은 `debug`
- text handler 사용
- 그 외 환경(`stage`, `production`, `prod` 등)
- backend `slog` 기본 레벨은 `info`
- JSON handler 사용
### 9.3 운영 override
- 운영/스테이징에서 장애 분석이 필요한 경우에만 `BACKEND_LOG_LEVEL=debug`를 일시적으로 설정합니다.
- 허용 값:
- `debug`
- `info`
- `warn`
- `error`
예시:
```env
APP_ENV=stage
BACKEND_LOG_LEVEL=debug
```
### 9.4 Headless Login 디버그 필드
- headless login 경로는 기본적으로 `reason_code` 중심으로 실패 원인을 기록합니다.
- `debug` 레벨일 때만 추가 진단 필드를 남깁니다.
- `expected_audiences`
- `received_audiences`
- `received_kid`
- `claim_issuer`
- `claim_subject`
- `claim_expires_at`
- `claim_not_before`
- `claim_issued_at`
- `login_challenge_prefix`
### 9.5 응답과 로그의 역할 분리
- API 응답은 `2번 정책`에 따라 `code + 짧은 안전 메시지`까지만 포함합니다.
- 상세 실패 원인은 구조화 로그에서 확인합니다.
- 같은 실패라도 응답에는 축약된 정보만, debug 로그에는 운영 진단용 필드를 남기는 것이 기본 원칙입니다.
### 9.6 민감 정보 비노출 원칙
- 아래 값은 로그에 직접 남기지 않습니다.
- raw `client_assertion`
- password
- session token
- cookie
### 9.7 운영 메모
- 운영에서는 기본적으로 `info`를 유지합니다.
- 장애 분석이 끝나면 `BACKEND_LOG_LEVEL` override는 즉시 제거합니다.
- 클라이언트 로그 정책(`CLIENT_LOG_DEBUG`)과 backend logger 정책(`BACKEND_LOG_LEVEL`)은 별도입니다.
## 10) 부록: Ory UI Error Codes 처리 원칙 (요약)
Ory Kratos UI 문서의 원칙을 기반으로, Baron 정책에 반영해야 할 핵심 처리 방침을 요약합니다.
- 메시지는 **root / method / field** 레벨에 붙을 수 있으며, UI에서 범위를 고려해 표시해야 합니다.
- UI 메시지는 **`id`, `text`, `type`, `context`** 형태로 전달되며, `id`는 **고정된 값**입니다.
- 메시지 `id`는 **7자리 규칙(xyyzzzz)**을 따릅니다.
- `x`: 메시지 타입 (1=info, 4=input validation error, 5=generic error)
- `yy`: 모듈/플로우 (01=login, 02=logout, 03=MFA, 04=registration, 05=settings, 06=recovery, 07=verification)
- `zzzz`: 구체 메시지 ID
- SPA/Native UI에서는 Ory가 **에러 응답을 직접 반환**하는 경우가 있으므로, **UserFront에 에러 ID별 처리 로직**이 필요합니다. (예: flow 만료/재시작, 인증 단계 재진입 등)
- Ory는 React 레퍼런스 구현에서 에러 처리 로직 예시를 제공합니다.
- UI 메시지 목록은 **machine readable JSON**으로 제공합니다.
## 11) References
- Ory Kratos UI error codes: https://www.ory.com/docs/kratos/concepts/ui-user-interface#ui-error-codes
- Ory Kratos React error handling example: https://github.com/ory/kratos-react-nextjs-ui/blob/master/pkg/errors.tsx
- Ory Kratos UI messages (machine readable): https://github.com/ory/docs/blob/master/docs/kratos/concepts/messages.json
- Ory Kratos User-facing errors: https://www.ory.com/docs/kratos/self-service/flows/user-facing-errors
- Ory Kratos Advanced Integration (SPAs and 422 error, `browser_location_change_required`): https://www.ory.sh/docs/kratos/bring-your-own-ui/custom-ui-advanced-integration
- Ory Kratos API (Postman) - Create Login Flow for Native Apps (`session_already_available`, `session_aal1_required`, `security_csrf_violation`): https://www.postman.com/ory-docs/ory/request/uy54y0r/create-login-flow-for-native-apps
- Ory Kratos API (Postman) - Create Registration Flow for Native Apps (`session_already_available`, `security_csrf_violation`): https://www.postman.com/ory-docs/ory/request/5vmu1ui/create-registration-flow-for-native-apps
- Ory Kratos API (Postman) - Create Settings Flow for Browsers (`session_inactive`, `security_csrf_violation`, `security_identity_mismatch`): https://www.postman.com/ory-docs/ory/request/pyfglhb/create-settings-flow-for-browsers
- Ory Kratos Client Docs (`session_refresh_required` 등): https://docs.rs/crate/ory-client/latest/source/docs/FrontendApi.md
- RFC 6749 (OAuth2 error codes): https://www.rfc-editor.org/rfc/rfc6749.txt
- OpenID Connect Core 1.0 (`consent_required`): https://openid.net/specs/openid-connect-core-1_0-31.html
---
## 로컬 참조 문서
- `docs/backend-log-policy.md`
- `docs/client-log-policy.md`
- `docs/test-plan/backend-test-inventory.md`