Skip to content

feat(viewer-context): Restore ViewerContext from JWT in middleware#112875

Merged
gricha merged 7 commits intomasterfrom
gricha/feat/viewer-context-jwt-middleware
Apr 14, 2026
Merged

feat(viewer-context): Restore ViewerContext from JWT in middleware#112875
gricha merged 7 commits intomasterfrom
gricha/feat/viewer-context-jwt-middleware

Conversation

@gricha
Copy link
Copy Markdown
Member

@gricha gricha commented Apr 13, 2026

Middleware decodes X-Viewer-Context on incoming requests.

  • Auth user always wins; header only used when no user is authenticated
  • Dual-mode: accepts both JWT (new) and JSON + HMAC signature (legacy)
  • Org ID mismatch between header and auth user logged as error
  • Parsing logic lives in viewer_context.py as viewer_context_from_header()
  • Verification keys in _get_verification_keys() — currently just SEER_API_SHARED_SECRET

Depends on #112765.

@github-actions github-actions bot added the Scope: Backend Automatically applied to PRs that change backend components label Apr 13, 2026
When an incoming request carries a valid X-Viewer-Context JWT header,
the middleware now decodes it. Authenticated user always takes
precedence — JWT is only used when no user is authenticated (the
service-to-service path, e.g. Seer calling back into Sentry).

When both are present and org IDs disagree, a warning is logged.

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
@gricha gricha force-pushed the gricha/feat/viewer-context-jwt-middleware branch from aebd74a to 51db436 Compare April 13, 2026 22:34
Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
…, add verification key list

Move _viewer_context_from_jwt logic into viewer_context.py as
viewer_context_from_header(). Middleware now delegates to it.

decode_viewer_context now tries all known verification keys
(kid-matched first). Keys are collected in _get_verification_keys()
— currently just SEER_API_SHARED_SECRET; add new service keys there.

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
@gricha gricha marked this pull request as ready for review April 13, 2026 22:43
@gricha gricha requested a review from a team as a code owner April 13, 2026 22:43
``request.user`` and ``request.auth`` are already populated.
Placed after ``AuthenticationMiddleware``. Authenticated user always
takes precedence; ``X-Viewer-Context`` JWT is only used for
unauthenticated service-to-service calls (e.g. Seer → Sentry).
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this is unauthenticated

Copy link
Copy Markdown
Contributor

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 50eb38f. Configure here.

elif jwt_ctx is not None:
ctx = jwt_ctx
else:
ctx = request_ctx
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JWT overrides token-authenticated requests without user

Low Severity

The middleware determines whether a request is "authenticated" solely by checking request_ctx.user_id is not None, but a request can be authenticated via an org auth token without having a user (e.g., org-scoped API tokens). In that case, user_id is None and the JWT context silently overrides the org auth token's organization_id and drops the token field entirely — with no mismatch warning logged, since the org-ID comparison only runs inside the user_id is not None branch. The PR description states JWT is only for "unauthenticated service-to-service calls," but this condition doesn't account for token-based authentication without a user.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 50eb38f. Configure here.

… list

Dual-mode for migration: middleware accepts both JWT and legacy
JSON + X-Viewer-Context-Signature HMAC format.

Verification keys collected in _get_verification_keys() — currently
just SEER_API_SHARED_SECRET. Add new service keys there.

Fix docstring: service-to-service calls are authenticated (via HMAC),
just not user-authenticated.

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
Pre-compute {kid: key} mapping in _get_verification_keys() instead of
iterating the list on every decode. decode_viewer_context now does a
single dict lookup by kid.

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
if not keys_by_kid:
raise ValueError("No verification keys available.")

kid = pyjwt.get_unverified_header(token).get("kid")
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not a big fan of the magic key here, maybe better as a const somewhere. I know it's a jwt thing but still weird to read

Address PR feedback: extract magic "kid" string to _JWT_KEY_ID_HEADER
const, and clarify middleware docstring — service-to-service calls are
authenticated (via HMAC), just not user-authenticated.

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
@gricha gricha merged commit 894f929 into master Apr 14, 2026
77 checks passed
@gricha gricha deleted the gricha/feat/viewer-context-jwt-middleware branch April 14, 2026 03:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Scope: Backend Automatically applied to PRs that change backend components

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants