Commit Graph
2 Commits
Author SHA1 Message Date
shadowdaoandClaude Opus 5 bbea0f74f3 feat: support Microsoft Entra ID as an OIDC provider
Three defects stood between this codebase and a working Entra deployment.
All three fail silently, which is why they are grouped: each one masks the
next, and fixing any two still leaves a broken or dangerous install.

1. JWKS discovery. The key set URL was hardcoded to `${issuer}/jwks/`,
   which is Authentik's convention, not a standard. Entra serves keys at
   `/{tenant}/discovery/v2.0/keys`, so every Entra-issued MCP token failed
   verification on a 404 — authentication was impossible, not merely
   misconfigured. We now read `jwks_uri` from the issuer's discovery
   document and fall back to the old path, so Authentik is untouched.
   Discovery failure arms a 60s retry rather than pinning the wrong URL
   for the life of the container.

2. Identity. Entra's `sub` is pairwise — derived from the token
   recipient — so the Web UI and MCP app registrations emit different
   `sub` values for the same human. Keyed on (iss, sub), that person got
   two rows: sign into the Web UI, connect Claude Code, land in an empty
   account. Both paths upsert, so nothing errored. Identity now keys on
   `oid`, which Microsoft documents as constant across applications in a
   tenant, via one resolver both surfaces share. Rows created before the
   0005 migration adopt their `oid` on next sign-in.

3. Groups overage. Past 200 groups Entra omits `groups` entirely and
   substitutes a `_claim_names` pointer. `normalizeGroupsClaim` read that
   as "zero groups" and the sync deleted every membership the user had,
   revoking access to every shared project on both surfaces with no error
   raised. Both surfaces now refuse such a token instead — the Web UI
   fails the sign-in, MCP returns 401 — leaving memberships intact and
   naming the operator fix. An absent claim with no overage marker still
   clears memberships, which is unchanged and deliberate.

Verified against a real pgvector instance: 66 tests pass, and reverting
either new behaviour fails exactly the tests that cover it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 10:51:29 -07:00
shadowdaoandClaude Opus 4.7 7712023c32 feat(phase-4a): groups sync + X-Project-Key header substrate
Foundational work for the upcoming group-scoped sharing feature.

Schema (migration 0003_groups.sql + drizzle schema):
  - memory_access enum ('ro' | 'rw') reserved for Agent B's project_shares
  - groups (id, oidc_iss, name, display_name, …) keyed by (oidc_iss, name)
    so different IdPs can both have e.g. "platform" without colliding
  - user_groups (user_id, group_id, synced_at) PK (user_id, group_id)

Auth (auth.ts + lib/auth/sync-groups.ts):
  - jwt callback now syncs `profile.groups` after upserting the user
  - syncUserGroupsFromClaim runs in a single tx: upserts each group,
    inserts new memberships, deletes ones no longer in the claim
  - missing/empty claim → user has zero groups (wipe memberships)
  - EntraID GUID-vs-name edge case: we treat whatever strings the claim
    emits as names verbatim; groups overage (>200 groups → no claim)
    is documented as unsupported in v1

UserContext + JWT (lib/mcp/context.ts, lib/auth/jwt.ts):
  - AuthenticatedClaims.groups surfaced from verified JWT payload
  - UserContext.groups: string[] — live from OIDC token claim, falls
    back to DB snapshot for CLI (HMAC) tokens which carry no claim
  - UserContext.defaultProjectKey: optional, set from header

MCP route (app/api/mcp/route.ts):
  - reads X-Project-Key header, validates against ProjectKey Zod schema,
    400 on invalid; empty/missing leaves defaultProjectKey undefined
  - auto-upserts the header-supplied project so first-use works without
    a separate project.identify call

Tools (lib/mcp/tools.ts):
  - withDefaultProject helper injects ctx.defaultProjectKey when the
    caller omits `project`. Per-tool defaultScope hint avoids breaking
    snippet.put (user-scope default) while making memory.write
    (project-scope default) honor the header
  - applied to memory.write/list/search/update and all snippet.* tools

Web UI:
  - /settings/groups debug page lists current memberships with synced_at
    and a clear empty state pointing at README troubleshooting
  - /settings/tokens grows a "Pin to project" dropdown; selected key is
    baked into the generated `claude mcp add` snippet as
    `--header "X-Project-Key: <key>"`. The JWT itself stays
    identity-only — pinning is purely a UX shortcut
  - settings landing page links to /settings/groups
  - README troubleshooting bullet covers the empty-groups path for
    Authentik / EntraID / Keycloak

Refactor:
  - extracted resolveProjectId + upsertProject from memory-actions.ts
    into lib/projects.ts so the MCP route can reuse upsertProject

Verification:
  - pnpm typecheck clean
  - SKIP_ENV_VALIDATION=true pnpm build clean; /settings/groups in route table

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 09:39:47 -07:00