feat(viewer-context): Restore ViewerContext from JWT in middleware#112875
feat(viewer-context): Restore ViewerContext from JWT in middleware#112875
Conversation
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>
aebd74a to
51db436
Compare
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>
| ``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). |
There was a problem hiding this comment.
I don't think this is unauthenticated
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ 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 |
There was a problem hiding this comment.
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.
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>
src/sentry/viewer_context.py
Outdated
| if not keys_by_kid: | ||
| raise ValueError("No verification keys available.") | ||
|
|
||
| kid = pyjwt.get_unverified_header(token).get("kid") |
There was a problem hiding this comment.
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>


Middleware decodes
X-Viewer-Contexton incoming requests.viewer_context.pyasviewer_context_from_header()_get_verification_keys()— currently justSEER_API_SHARED_SECRETDepends on #112765.