forked from baron/baron-sso
147 lines
5.1 KiB
Markdown
147 lines
5.1 KiB
Markdown
# 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
|