feat(auth): emit RFC 6750 insufficient_scope WWW-Authenticate challenges from PostHog REST API
Nobody has claimed this yet.
- Dominant language
- Python
- Stars
- 39.9k
- Forks
- 3.4k
- Avg merge
- 6h 51m
- Merged PRs (30d)
- 232
Description
Context
Today PostHog's REST API rejects scope-protected requests with a bare `HttpResponseForbidden` and no information about what scope was missing. Specifically:
- `OAuthAccessTokenAuthentication.authenticate_header()` at `posthog/auth.py:655-656` returns just `"Bearer"` with no challenge parameters
- Django OAuth Toolkit's `ProtectedResourceMixin.dispatch()` returns plain `HttpResponseForbidden()` (no `WWW-Authenticate` header at all)
This is non-compliant with RFC 6750 § 3 (Bearer Token Usage). Spec-compliant OAuth clients can't discover what scope they need to re-request, which is what makes the "drop scopes_supported, rely on runtime hints" approach impossible today.
Change
Update `OAuthAccessTokenAuthentication` (and any sibling resource-server authn classes) to:
- On insufficient-scope rejection, return a `WWW-Authenticate` header in the form:
`Bearer realm="posthog", error="insufficient_scope", scope="", error_description="..."` - On invalid/expired token, return:
`Bearer realm="posthog", error="invalid_token", error_description="..."` - On missing token (not authenticated):
`Bearer realm="posthog"`
Implementation notes
- The decorator pattern PostHog uses (`@required_scopes`) should populate the missing-scope list onto the response so the auth class can read it
- Make sure the header survives Cloudflare / Envoy / any downstream proxy (audit as part of #57528)
- Keep the existing API JSON body shape (don't break clients that rely on it) — `WWW-Authenticate` is supplementary
Why
Today: any OAuth-driven client that doesn't pre-discover scopes from `scopes_supported` fails opaquely on scope mismatch with no recovery path.
After this: clients can implement spec-compliant runtime scope discovery. Required infrastructure for #57530 (drop `scopes_supported`).
Acceptance
- All scope-protected endpoints return RFC 6750-compliant `WWW-Authenticate` on 401/403
- Test coverage for each error class (insufficient_scope, invalid_token, missing_token)
- A documented client-side recovery flow: receive 401 with `scope="x:read"`, re-authorize requesting `x:read`, retry
Tracking
Parent: #57524
Blocks: #57530
Project: https://github.com/orgs/PostHog/projects/194
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 with posthog/auth.py:655-656 and trace the @required_scopes decorator plus Django OAuth Toolkit's ProtectedResourceMixin.dispatch() to understand how each rejection reaches the response. Audit the downstream proxy path mentioned in the issue and add coverage for insufficient_scope, invalid_token, and missing-token responses. Done means all scope-protected endpoints emit the specified RFC 6750 headers while retaining the existing JSON body, with the client recovery flow documented.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- django, python
- Domain
- api, authentication, backend
- Issue type
- Feature
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 48/100