Skip to content

feat(auth): EasyAuth browser identity as a source inside entra mode (spike, isolation)#42

Draft
colombod wants to merge 1 commit into
mainfrom
step2-auth-spike
Draft

feat(auth): EasyAuth browser identity as a source inside entra mode (spike, isolation)#42
colombod wants to merge 1 commit into
mainfrom
step2-auth-spike

Conversation

@colombod

@colombod colombod commented Jul 3, 2026

Copy link
Copy Markdown
Collaborator

Summary

Add EasyAuth (Azure platform-identity middleware) as a third identity source inside auth_mode=entra, coexisting with delegated-user Bearer JWTs and service-account Bearer JWTs. The spike covers the isolation half — config, trust gate, resolver, middleware, hardening fixes — but defers the real Azure topology half (actual role:web EasyAuth service deployment + anon→302 gate).

This PR is stacked on #41 (Neo4j client split). Base branch is step1-neo4j-client-split; merge order must preserve the stack.

Changes

Config & Trust Gate

  • trust_easyauth_principal (bool, default=false, fail-closed) in AuthSettings
  • Startup composition gate: trust_easyauth_principal=true requires auth_mode=entra (no other mode supports EasyAuth)
  • Settings frozen=True to prevent runtime mutations

Identity Extraction

  • EntraResolver.resolve_principal_id: New resolver inside entra mode
  • _extract_easyauth_oid: Scalar-only extraction from X-MS-CLIENT-PRINCIPAL-ID header
  • Public: GUID_RE / ALL_ZEROS_GUID validation rules (prevents spoofed/zero OIDs → 401)
  • Bearer-wins precedence: if a Bearer token exists, EasyAuth header is ignored (same precedence model as Team Pulse)

Middleware & Auth Flow

  • EasyAuth branch in auth middleware (after Bearer check, before fallback-to-anon)
  • Dual-log on co-presence (Bearer + EasyAuth header both present): auth_event=easyauth_header_with_bearer (signal to rates/tune in ops)
  • Fail-closed: duplicate principal-id headers → 401

Adversarial Hardening

  • Fixed TB-5 real hole: admin:"false" / admin:1 passed truthy check → now isinstance(bool)
  • TB-2: duplicate headers → 401 (caught by header dict behavior)
  • TB-3: keys already lowercased on receive
  • All-zeros GUID rejected as spoofed identity
  • Bytes-to-str id coercion explicit
  • ASCII/encoding regression tests added

Test Infrastructure (Step 3 Pre-Flight)

  • Additive boot guard _assert_capability_routes_not_exempt: ensures no exempt route carries require_read / require_write (prevents conftest-fixture fail-open, addresses :app vs :asgi_app footgun)
  • Strict-xfail tripwires in test_authz_empty_state.py: pinned xfail on read/write/admin empty-state tests; will flip to XPASS→FAIL when /is_write_capable tightening lands in Step 3 (enforces the blocker; triple-checks won't be silently missed)
  • Admin-deny strict pins (no xfail on admin 403)

Testing & Review

  • Council review: No FAIL; §10 binding resolutions folded in (C1–C9 all addressed).
  • Adversarial pass (tester-breaker + crusty): CONCERN+GO; fixes applied (TB-1 through TB-8 resolved).
  • Isolation-verified: 1810 passed, 2 skipped, 2 xfailed, 0 failed (no DB/network). EasyAuth integration file 20/20 green.

Tracked Blockers (Must Land Before Merge/Deploy)

[Step 3 BLOCKER, CI-enforced] Tighten _is_write_capable to fail-closed on absent scope state (currently default-allow). The strict-xfail pair in test_authz_empty_state.py will flip to XPASS→FAIL the suite the moment this lands, forcing removal of the markers. Do it in the SAME commit that moves routes (dead-letter → require_admin, /status behind auth), M2 matrix (~1200 tests) re-verified. Also: fix conftest client fixture to route through asgi_app (stop relying on default-allow) and address the :app vs :asgi_app dual-export footgun (prod must serve asgi_app).

[DEPLOY gate, HIGH] trust_easyauth_principal=true MUST NOT ship until Test B (deploy-time sole-ingress + edge header-stripping probe) is a HARD gate with a defined failure response (alert/auto-disable) + prod config gated on a recorded recent pass. App does NO crypto verify of the header — the config startup gate only checks auth_mode+web_ui, NOT that EasyAuth is really the ingress. Accepted known bet (same as Team Pulse), but the gate must be real.

[Step 3 ops — signal tuning] Co-presence WARNING (auth_event=easyauth_header_with_bearer) may fire on normal browser+bearer traffic → decide metric/rate-limit vs WARNING so it stays actionable.

[Step 3 UX — onboarding paths] identity_unbound 403 needs an actionable onboarding/contact path; no-header 401 is least-informative for the most-innocent failure; session-lifecycle UX TBD.

NEEDS AZURE (deferred to Step 2 Azure half)

This PR is the isolation half — config, trust, resolvers, tests in a single process. The real Azure topology half lives in a separate work item (Step 2 Azure half):

  • Stand up the real role: web EasyAuth service (not this repo — infrastructure-level)
  • Entra app-reg changes on 53aa4ffd (enable ID-token implicit grant + add .auth/login/aad/callback redirect URI)
  • Prove anon → 302 → login, real login → app reads oid
  • Build Test B (sole-ingress + edge header-stripping probe) with a real failure response

Generated with Amplifier

Co-Authored-By: Amplifier 240397093+microsoft-amplifier@users.noreply.github.com

@colombod colombod force-pushed the step1-neo4j-client-split branch from 3815e58 to 3f39a4d Compare July 3, 2026 15:58
@colombod colombod force-pushed the step2-auth-spike branch from 7abce16 to 8218590 Compare July 3, 2026 15:58
@colombod colombod force-pushed the step1-neo4j-client-split branch from 3f39a4d to 2eb73ab Compare July 6, 2026 14:48
@colombod colombod force-pushed the step2-auth-spike branch from 8218590 to 8dd171f Compare July 6, 2026 14:48
@colombod colombod marked this pull request as ready for review July 6, 2026 23:58
@colombod colombod force-pushed the step1-neo4j-client-split branch from 2eb73ab to 262ff71 Compare July 8, 2026 11:16
@colombod colombod force-pushed the step2-auth-spike branch 2 times, most recently from 12c1e92 to af46484 Compare July 8, 2026 12:05
Base automatically changed from step1-neo4j-client-split to main July 9, 2026 00:25
…spike, isolation)

Add Azure EasyAuth X-MS-CLIENT-PRINCIPAL-ID as a third identity source inside
auth_mode=entra, coexisting with delegated-user and service Bearer JWTs, all
resolving via the same entra_identities oid->contributor map. Bearer-wins
precedence; scalar-only header extraction; EntraResolver.resolve_principal_id
(GUID/all-zeros/unmapped -> 401/403); fail-closed trust_easyauth_principal flag
+ startup composition gate; Settings frozen=True; named EasyAuthResolveFn Protocol.

Hardening from adversarial + council review: strict bool/str validation of
entra_identities entries (admin/id), duplicate principal-id header -> 401,
co-presence WARNING, no-header 401 logging. TB-7: additive
_assert_capability_routes_not_exempt boot guard (no exempt route may carry
require_read/require_write) + strict-xfail pins on read/write empty-state;
the fail-closed _is_write_capable tightening is deferred to Step 3 (CI-enforced
by the xfail tripwire). No _is_write_capable/authz decision-logic change here.

Isolation-verified: 1810 passed, 2 skipped, 2 xfailed, 0 failed (no DB/network).
Needs-Azure parts (role:web service, anon->302, Test B sole-ingress) deferred.

🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier)

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
@colombod colombod force-pushed the step2-auth-spike branch from af46484 to f53b9a9 Compare July 9, 2026 00:34
@colombod colombod marked this pull request as draft July 9, 2026 13:29
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.

1 participant