feat(oauth): structured logging for invalid_scope rejections at the AS
Nobody has claimed this yet.
- Dominant language
- Python
- Stars
- 39.9k
- Forks
- 3.4k
- Avg merge
- 6h 51m
- Merged PRs (30d)
- 232
Description
Context
Per the OAuth scope drift RFC's pre-flight #4, PostHog's AS doesn't emit invalid_scope rejections to the standard log stream. They come back as HTTP 302 redirects with ?error=invalid_scope in the redirect URL (per RFC 6749 § 4.1.2.1), but nothing logs the rejection on our side.
This means:
- The RFC's success criterion ("zero
invalid_scopeover 30-day window") is unmeasurable today - We've been counting incidents by Slack reports + GitHub PRs, which is a lower bound
- Carmen's case (#57509) was caught manually in review, not surfaced by alerting
Confirmed via query-logs against posthog-web-django: zero results for searchTerm invalid_scope over the past 24h, despite the in-flight #57509 PR being the resolution to a customer-reported invalid_scope event in the same window.
Change
In posthog/api/oauth/views.py, add structured logging at the point where the AS returns an invalid_scope redirect.
Two places to instrument depending on where DOT raises:
- The
OAuthAuthorizationView(or its parent in DOT) — whenvalidate_authorization_request()raisesInvalidScopeError - The exception handler that converts the OAuth error into the redirect URL
Add:
import structlog
import posthoganalytics
logger = structlog.get_logger(__name__)
# At the rejection point:
logger.warning(
\"oauth.authorize.invalid_scope\",
requested_scopes=requested_scopes_list,
grantable_scopes=grantable_scopes_list,
missing_scopes=list(set(requested_scopes_list) - set(grantable_scopes_list)),
client_id=request.GET.get(\"client_id\", \"\"),
user_agent=request.headers.get(\"User-Agent\", \"\")[:200],
)
posthoganalytics.capture_exception(
Exception(\"OAuth invalid_scope rejection\"),
properties={
\"missing_scopes\": list(set(requested_scopes_list) - set(grantable_scopes_list)),
\"client_id\": request.GET.get(\"client_id\", \"\"),
},
)
Confirm the WARN-severity Loki query produces results within 7 days of deploy.
Acceptance
- A query like
SELECT count() FROM logs WHERE service.name = 'posthog-web-django' AND severity_text = 'warn' AND body ILIKE '%oauth.authorize.invalid_scope%'produces non-zero results once a real rejection occurs - The baseline metric for the RFC's success criterion is available within 7 days of this landing
- Capture in error tracking surfaces the spike if there's a regression
Why
You can't measure what you don't observe. The RFC commits to "zero invalid_scope over 30 days" as the success metric for the structural cleanup; this issue makes that measurable.
Tracking
Parent: #57524
Project: https://github.com/orgs/PostHog/projects/194
RFC: https://github.com/PostHog/requests-for-comments-internal/blob/matt/oauth-scope-drift-rfc/engineering/2026-05-04-oauth-scope-drift.md
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in posthog/api/oauth/views.py at OAuthAuthorizationView and trace where validate_authorization_request() raises InvalidScopeError or where DOT converts the error into a redirect. Review the existing query-logs search and verify that a WARN query for oauth.authorize.invalid_scope returns a real rejection after deployment. Done means the structured log, error-tracking capture, and seven-day baseline are available.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python
- Domain
- api, authentication, backend
- Issue type
- Feature
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 52/100