Skip to content

feat(oauth): structured logging for invalid_scope rejections at the AS #57535

@MattBro

Description

@MattBro

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:

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:

  1. The OAuthAuthorizationView (or its parent in DOT) — when validate_authorization_request() raises InvalidScopeError
  2. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions