PostHog / PostHog/posthog

feat(auth): emit RFC 6750 insufficient_scope WWW-Authenticate challenges from PostHog REST API

Open
#57,529 0 comments 0 reactions 0 assignees View on GitHub

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:

  1. On insufficient-scope rejection, return a `WWW-Authenticate` header in the form:
    `Bearer realm="posthog", error="insufficient_scope", scope="", error_description="..."`
  2. On invalid/expired token, return:
    `Bearer realm="posthog", error="invalid_token", error_description="..."`
  3. 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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.