{
  "schema_version": "1.0",
  "baseline": "A-feature-flags-phase-2b-plus",
  "phase": "2b+",
  "updated_at": "2026-04-19",
  "owner": "session_a",
  "purpose_th": "บันทึกแนวทาง JWT verification สำหรับ FF Service Phase 2b+ · ครอบคลุม resolution order · modes · claims · error modes · dev-fallback · honest gap ว่ายังไม่มี JWKS wire",
  "purpose_en": "Verification notes for FF Service Phase 2b+. Describes resolution order, verification modes, expected claims, error modes, dev-fallback behaviour, and the honest gap that JWKS fetching is not wired.",
  "honest_note": "Phase 2b+ DOES NOT verify JWT signatures. Bearer tokens are DECODED (base64 · claims read) but never verified against JWKS. This is deliberately non-verified to match Admin 2b's decision-only stance. Production deployment will require Phase 2c to enable verified-mode.",
  "resolution_order": [
    { "priority": 1, "source": "Authorization: Bearer <token>", "parser": "decode (no verify)", "produces": "auth_source=jwt_unverified OR jwt (if verified-mode on)" },
    { "priority": 2, "source": "X-PTT-User-Id / X-PTT-Tenant-Id / X-PTT-Role headers", "parser": "direct header read · dev-only", "produces": "auth_source=dev_headers" },
    { "priority": 3, "source": "Request body {user,tenant,roles}", "parser": "JSON on POST only", "produces": "auth_source=body" },
    { "priority": 4, "source": "Query string user= / tenant= / role=", "parser": "URL-readable · audit-friendly", "produces": "auth_source=query" },
    { "priority": 5, "source": "Defaults (anonymous)", "parser": "built-in", "produces": "auth_source=none" }
  ],
  "mode_matrix": [
    {
      "mode": "verified",
      "enabled": false,
      "condition_th": "ต้องมี JWKS fetch + signature verify + aud/iss match · Phase 2c",
      "condition_en": "Requires JWKS fetch, signature verify, aud/iss match — Phase 2c",
      "current_state": "documented · not implemented",
      "expected_output": { "auth_source": "jwt", "warnings": [], "verified": true }
    },
    {
      "mode": "decode-only",
      "enabled": true,
      "condition_th": "Bearer token ถูกจัดรูปแบบถูก · base64 decode สำเร็จ · ไม่มี verify",
      "condition_en": "Bearer token well-formed · base64 decode succeeds · no verify performed",
      "current_state": "active in 2b+ runtime",
      "expected_output": { "auth_source": "jwt_unverified", "warnings": ["auth_not_verified"], "verified": false }
    },
    {
      "mode": "dev-fallback",
      "enabled": true,
      "condition_th": "ไม่มี Bearer · ใช้ X-PTT-* headers หรือ body/query",
      "condition_en": "No Bearer · use X-PTT-* headers or body/query",
      "current_state": "active in 2b+ runtime · flagged dev-only",
      "expected_output": { "auth_source": "dev_headers|body|query|mixed", "warnings": ["dev_mode"], "verified": false }
    },
    {
      "mode": "anonymous",
      "enabled": true,
      "condition_th": "ไม่มี source ใด · ใช้ defaults",
      "condition_en": "No source present · defaults applied",
      "current_state": "active",
      "expected_output": { "auth_source": "none", "warnings": [], "verified": false }
    }
  ],
  "claim_expectation": {
    "algorithm": "RS256 (planned) · HS256 accepted in dev for convenience",
    "issuer": "https://auth.pattayatogether.internal/ (planned)",
    "audience": "pty-feature-flags",
    "required_claims": {
      "sub": "user_id",
      "tenant_id": "string",
      "roles": "array<string>",
      "iat": "issued_at · unix",
      "exp": "expiry · unix"
    },
    "optional_claims": {
      "scope": "array<string>",
      "approvals": "array<approval_id>",
      "kid": "key_id for JWKS lookup"
    },
    "on_missing_claim": "treat as anonymous if sub missing · warnings add 'missing_claim:<name>' for other gaps"
  },
  "error_modes": [
    { "code": "token_malformed", "http": 400, "message": "Authorization header present but not parseable" },
    { "code": "token_expired", "http": 401, "message": "exp claim in the past · caller must refresh" },
    { "code": "token_audience_mismatch", "http": 401, "message": "aud does not match pty-feature-flags" },
    { "code": "token_issuer_mismatch", "http": 401, "message": "iss does not match expected issuer" },
    { "code": "token_signature_unverifiable", "http": 200, "message": "Decode-only mode active · signature could not be verified · warning surfaced · response returned" },
    { "code": "dev_mode_rejected", "http": 401, "message": "Production config rejects dev-fallback · active only in dev" }
  ],
  "dev_headers": {
    "X-PTT-User-Id": "string",
    "X-PTT-Tenant-Id": "string",
    "X-PTT-Role": "string (comma-separated for multi)",
    "warning": "Never enable in production · FF_ALLOW_DEV_HEADERS env-var must be false in prod"
  },
  "security_bank": {
    "current_risks_honest": [
      "Decode-only mode accepts any syntactically valid JWT — no signature check",
      "Dev headers accepted without authentication — relies on deployment hardening",
      "No rate limiting on verification path — relies on upstream gateway",
      "No audit of auth failures — relies on Admin 2b audit sink (also boundary-only)"
    ],
    "mitigations_not_yet_applied": [
      "JWKS fetch + cache · planned Phase 2c",
      "FF_ALLOW_DEV_HEADERS=false default · planned config switch",
      "Per-IP + per-subject rate limit · platform gateway",
      "Structured auth.failure events on audit sink · requires Phase 2c delivery wiring"
    ]
  },
  "pair_with": {
    "contract": "docs/runtime/feature-flags-service/phase_2b_plus_contract.json",
    "persistence": "docs/runtime/feature-flags-service/persistence_backend_notes.json",
    "admin_2b_notes": "docs/runtime/admin-control-plane-service/phase_2b_contract.json (auth_context_resolution section)"
  }
}
