Skip to content

fix(auth): populate project picker for tokens without scoped_organizations#3279

Open
dmarticus wants to merge 2 commits into
mainfrom
dylan/auth-picker-fallback-current-org
Open

fix(auth): populate project picker for tokens without scoped_organizations#3279
dmarticus wants to merge 2 commits into
mainfrom
dylan/auth-picker-fallback-current-org

Conversation

@dmarticus

@dmarticus dmarticus commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Problem

Signing in to PostHog Code against a local backend (localhost:8010) leaves the project picker stuck on "No projects" even though the user has membership in an org with teams. OAuth completes, the user profile renders (name, avatar, org), but the picker is empty and there's nothing to select.

Confirmed by log diagnostics on a local Hedgehog Inc. account:

PICKER-DIAG session build {
  scopedOrgsRaw: [],
  currentOrgId: '019e84db-eb34-0000-d2b6-741b6cc26341',
  orgProjectsMapKeys: [],
  orgProjectsMapSummary: {},
}

Root cause

Local PostHog backends mint project-scoped OAuth tokens (required_access_level=project on the authorize URL) with an empty scoped_organizations list — only scoped_teams gets populated. The old code at packages/core/src/auth/auth.ts:593 trusted scoped_organizations as the sole source of orgs to enumerate:

const scopedOrgIds = tokenResponse.scoped_organizations ?? [];
// ...
await this.buildOrgProjectsMap(..., scopedOrgIds, ...);

Empty array → zero orgs iterated → empty orgProjectsMap → "No projects."

Fix

Fall back to organization.id from /api/users/@me/ when the token doesn't specify orgs. We already fetch @me two lines later — we were just throwing the current org id away.

const orgIdsToFetch =
  scopedOrgIds.length > 0
    ? scopedOrgIds
    : currentOrgId ? [currentOrgId] : [];

Confirmed the org endpoint (/api/organizations/<id>/) returns 200 under a team-scoped token against a local backend, so this populates the picker without touching the OAuth consent flow, token scoping, or any existing session behavior.

What this doesn't change

  • OAuth consent scene: still requests required_access_level=project, still mints team-scoped tokens. Security posture unchanged.
  • Recovery loop from fix(auth): Auto-recover project-less session after re-auth #2655: the "empty scoped_organizations no-retry path" still holds. If @me also has no organization (truly orgless user), orgIdsToFetch stays empty, orgProjectsIncomplete stays false, and the recovery loop never spins.
  • Cloud users: scoped_organizations is populated for hosted PostHog tokens, so the fallback branch is dead code for them. Behavior identical to before.

Related PRs

  • PostHog/posthog#69102 — server-side sibling that unblocks the same local-dev OAuth flow from the other end (auto-provisioned OIDC RSA key, demo OAuth application ceiling revert so scope=* sign-in works). The picker fix here plus the server-side fixes there together give a one-shot working local sign-in for PostHog Code.

Test updates

  • Retitled does not attempt recovery when the token grants no scoped organizationsdoes not attempt recovery when the user has no organization at all. The old title/heuristic was too narrow — it equated empty scoped_organizations with "no orgs," when the correct discriminator is @me.organization. The test now stubs organization: null on @me and keeps the same 0-fetches / null-project assertions to defend Gustavo's stranded-session concern from fix(auth): Auto-recover project-less session after re-auth #2655.
  • Added falls back to the @me current org when the token has no scoped organizations to pin the fixed behavior.

How did you test this?

  • Reproduced locally: hoglet start, signed in against localhost:8010 on a Hedgebox Inc. account, watched the picker go from "No projects" to a populated list of 2 projects.
  • pnpm --filter @posthog/core test -- auth — 43 auth tests pass (2143 core total, +1 new)
  • pnpm --filter @posthog/core typecheck — clean
  • pnpm exec biome lint packages/core — clean

Automatic notifications

  • Publish to changelog?
  • Alert Sales and Marketing teams?

🤖 Generated with Claude Code

…nizations

Local PostHog backends mint project-scoped OAuth tokens with an empty
`scoped_organizations` list — only `scoped_teams` gets populated. The
old code trusted `scoped_organizations` as the sole source of orgs to
enumerate, so an empty array meant zero fetches, an empty
`orgProjectsMap`, and a project picker stuck on "No projects" even
though the user had a valid current org.

Fall back to the `organization.id` we already pull from
`/api/users/@me/` when the token doesn't specify orgs. The org endpoint
accepts team-scoped tokens fine (confirmed 200 against a local
backend), so this populates the picker without changing the OAuth
consent flow, token scoping, or any existing session behavior.

The "empty scoped_organizations no-retry path" from #2655 still holds:
if `@me` also has no organization (genuinely orgless user),
`orgIdsToFetch` stays empty and the recovery loop never spins. The test
that pinned that behavior is retitled to reflect its real defense (no
org anywhere, not just no scoped orgs) and a positive test is added for
the team-scoped fallback.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@trunk-io

trunk-io Bot commented Jul 8, 2026

Copy link
Copy Markdown

Merging to main in this repository is managed by Trunk.

  • To merge this pull request, check the box to the left or comment /trunk merge below.

After your PR is submitted to the merge queue, this comment will be automatically updated with its status. If the PR fails, failure details will also be posted here

@github-actions

github-actions Bot commented Jul 8, 2026

Copy link
Copy Markdown

React Doctor found no issues in the changed files. 🎉

Reviewed by React Doctor for commit 3aab1fe.

@greptile-apps

greptile-apps Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Reviews (1): Last reviewed commit: "fix(auth): fall back to @me current org ..." | Re-trigger Greptile

Comment thread packages/core/src/auth/auth.test.ts Outdated
Greptile spotted that the two new tests share `scopedOrgs: []` and
differ only in what /api/users/@me/ returns. Collapse them into an
it.each and absorb the inline vi.stubGlobal into stubOrgFetch via an
optional `orgless` param. Also asserts the "no drift after connectivity
emit" invariant for both rows instead of just the orgless one, so the
recovery-loop no-op is defended on the fallback path too.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

@daniloc daniloc left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

astute

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants