Skip to content

fix(auth): reject non-ID-token OIDC material in API bearer sessions#1077

Closed
seonghobae wants to merge 2 commits into
developfrom
fix/oidc-api-token-type-validation
Closed

fix(auth): reject non-ID-token OIDC material in API bearer sessions#1077
seonghobae wants to merge 2 commits into
developfrom
fix/oidc-api-token-type-validation

Conversation

@seonghobae

Copy link
Copy Markdown
Contributor

Description

Fixes the Strix HIGH finding (CVSS 8.8, surfaced org-wide after the gpt-5.6-luna Strix migration): _decode_cached_oidc_session_payload in backend/api/auth.py accepted any same-issuer RS256 JWT whose aud contains OIDC_CLIENT_ID as an API bearer session, with no token-type/authorized-party validation — so access tokens, and tokens minted for other clients that share the issuer and audience, were replayable as API sessions.

The enterprise IdP mints naruon session claims (role/org/groups/workspace) directly into the ID token, so the ID token remains the intended bearer credential by design. This change pins the accepted material to exactly that shape instead of changing the architecture:

  • JOSE header typ, when present, must be JWT (case-insensitive). RFC 9068 access tokens declare at+jwt and are rejected before decode.
  • token_use, when present, must be "id" (Cognito/Azure-style access-token discriminator).
  • scope/scp claims present → reject. ADFS and Azure access tokens keep header typ: JWT but always carry these; ID tokens do not.
  • azp, when present, must equal OIDC_CLIENT_ID (OIDC Core §3.1.3.7), so a token minted for another authorized party cannot ride a shared aud.

All checks fail closed with the existing 401 path. The deliberate tuple-audience contract is preserved (multi-audience tokens without azp are still accepted — test_oidc_session_accepts_tuple_audience unchanged).

This finding is pre-existing on develop and currently fails the required Strix check on multiple open PR heads (#1074/#1068/#1066/#1059/#964 as of 09:1xZ); landing this unblocks them at the root cause.

Type of change

  • Bug fix (non-breaking change which fixes an issue)

Checklist:

  • My code follows the style guidelines of this project
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes
  • Any dependent changes have been merged and published in downstream modules

Local evidence: python -m pytest tests/test_auth_real.py -q → 97 passed (7 new cases: at+jwt + non-string typ headers, token_use=access, scope, scp, azp mismatch, and a positive ID-token case with matching azp/token_use and no typ header); ruff check and ruff format --check clean.

🤖 Generated with Claude Code

Comment thread backend/api/auth.py Fixed
@github-actions

github-actions Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

PR governance metadata gate is not ready for 0abe19748fc637fe49aca45d5a741775a76fc09e:

  • Draft PR: merge automation is paused.
  • Branch is BEHIND the base branch; update the branch and re-run checks.

seonghobae and others added 2 commits July 13, 2026 20:36
Strix (post gpt-5.6-luna migration) flags a HIGH finding in
backend/api/auth.py: _decode_cached_oidc_session_payload accepted any
same-issuer RS256 JWT whose aud contains OIDC_CLIENT_ID as an API bearer
session, with no token-type or authorized-party validation. Access tokens
and tokens minted for other clients can share the issuer and carry this
API's client_id in aud, so they were replayable as API sessions.

The enterprise IdP mints naruon session claims directly into the ID token,
so the ID token remains the intended bearer credential; the fix pins the
accepted material to exactly that shape instead of changing the
architecture:

- JOSE header typ, when present, must be JWT (case-insensitive); RFC 9068
  access tokens declare at+jwt and are rejected before decode.
- token_use, when present, must be "id" (Cognito/Azure-style access-token
  discriminator).
- Payloads carrying scope/scp claims are rejected — ADFS and Azure access
  tokens keep header typ JWT but always carry these claims; ID tokens do
  not.
- azp, when present, must equal OIDC_CLIENT_ID (OIDC Core 3.1.3.7), so a
  token minted for another authorized party cannot ride a shared aud.

All checks fail closed with the existing 401 and preserve the deliberate
tuple-audience contract (multi-audience tokens without azp are still
accepted, matching test_oidc_session_accepts_tuple_audience).

Tests: 7 new cases in tests/test_auth_real.py (at+jwt and non-string typ
headers, token_use=access, scope, scp, azp mismatch, plus a positive
ID-token case with matching azp/token_use and no typ header). Full
tests/test_auth_real.py passes (97) and ruff check/format are clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
bandit flags `token_use != id` as a possible hardcoded password because
the variable name contains token (B105, code-scanning alert #269). The
comparison is an OIDC claim-value check, not credential material; rename
the local to usage_claim instead of suppressing the rule.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@seonghobae seonghobae force-pushed the fix/oidc-api-token-type-validation branch from dd31c7f to 0abe197 Compare July 13, 2026 11:36
@seonghobae

Copy link
Copy Markdown
Contributor Author

⚠️ Hold — this fix inverts the credential direction and breaks Keycloak SSO login

naruon's API credential is the OIDC access token, not the ID token:

  • frontend/src/app/auth/oidc/callback/route.ts:112 reads tokenJson.access_token and sets it as the session bearer; frontend/src/app/auth/session/route.ts:288 does the same. There is no id_token usage as a credential anywhere in the frontend.
  • The IdP is Keycloak 24 (docker-compose.infra.yml, docker-compose.gateway.yml). Keycloak access tokens always carry a scope claim and body typ: "Bearer".

Running this PR's own predicates against realistic Keycloak tokens:

token _reject_non_id_token_payload result
Keycloak access token (frontend sends this; has scope) REJECT (scope/scp present) all SSO logins break
Keycloak ID token (the replay attack) ACCEPT vulnerability stays open

So this rejects the legitimate credential and admits the very token the Strix finding is about. It clears the Strix gate only because Strix's forged PoC trips these heuristics, not because the token-confusion class is closed.

The correct direction is the inverse: require the access-token marker (Keycloak body typ: "Bearer", or RFC 9068 header typ: at+jwt) and reject ID-token material. Converting to draft to prevent an auto-merge; a corrected PR follows.

@seonghobae seonghobae marked this pull request as draft July 13, 2026 11:40
@opencode-agent

Copy link
Copy Markdown
Contributor

OpenCode Review Overview

  • Head SHA: 0abe19748fc637fe49aca45d5a741775a76fc09e
  • Workflow run: 29246864410
  • Workflow attempt: 1
  • Gate result: APPROVE (exit 0)

Changed-File Evidence Map

flowchart LR
  PR["PR changed files"] --> Evidence["OpenCode bounded evidence"]
  Evidence --> S1["Backend (2 files)"]
  S1 --> I1["API and service runtime"]
  I1 --> R1["Review risk: Backend (2 files)"]
  R1 --> V1["backend tests"]
Loading

@seonghobae

Copy link
Copy Markdown
Contributor Author

Corrected fix opened as #1078 (require access-token type / reject ID-token replay), verified against realistic Keycloak tokens with a load-bearing regression test. Recommend closing this PR in favor of #1078.

@seonghobae

Copy link
Copy Markdown
Contributor Author

Closing as superseded by #1078. Independent re-verification confirms the design premise here was inverted: the frontend's API credential is the Keycloak access token (frontend/src/app/auth/oidc/callback/route.ts:112, session/route.ts:290; Keycloak 24 in docker-compose.infra.yml), which carries scope and typ=Bearer — this PR's scope/scp rejection would 401 every real login while Keycloak ID tokens still pass its checks, leaving the reported replay open. #1078's access-token-marker requirement is the correct direction.

@seonghobae seonghobae closed this Jul 13, 2026
@seonghobae seonghobae deleted the fix/oidc-api-token-type-validation branch July 13, 2026 11:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants