Wire alphaswarm_admin against the AlphaSwarm staff Entra tenant
End-to-end procedure for connecting the alphaswarm_admin service (backend
BFF + Next.js frontend) to Microsoft Entra ID, using the staff app
registration that the alphaswarm_entra_directory Terraform module
provisions.
The result: AlphaSwarm staff sign in to manage.alpha-swarm.ai with their
corporate Entra account; the admin BFF validates the resulting
api://alphaswarm-manage-api access tokens; the SPA mints tokens via
@azure/msal-browser and renews them silently with
acquireTokenSilent.
Companion runbooks:
- Bootstrap the AlphaSwarm Entra tenant — the prerequisite that creates the apps + groups + roles.
- Onboard a new staff member — group
- role assignment after the apps land.
- Rotate Entra secrets — federated credentials + break-glass procedures.
- Concept overview: Entra ID as the AlphaSwarm staff user pool.
- ADR: ADR-013 Entra ID as the AlphaSwarm staff first user pool.
What gets wired
| Surface | What it does |
|---|---|
alphaswarm_admin/src/alphaswarm_admin/settings.py | Reads the ALPHASWARM_AUTH_MSAL_INTERNAL_* env vars set by the helper script. Single-tenant when INTERNAL_TENANT_ID is set; multi-tenant otherwise. |
alphaswarm_admin/src/alphaswarm_admin/deps/identity.py | JWT validator pinned to the AlphaSwarm staff Entra v2.0 issuer; verifies aud=api://alphaswarm-manage-api, maps roles claim through the canonical RBAC lattice. |
alphaswarm_admin/src/alphaswarm_admin/api/routers/auth_setup.py | New GET /admin/auth/discovery + GET /admin/auth/health. Discovery feeds the SPA's PublicClientApplication; health confirms the IdP is reachable. |
alphaswarm_admin/frontend/components/auth/AuthProvider.tsx | Real MSAL flow (loginRedirect, acquireTokenSilent, acquireTokenPopup for step-up). No tenant id hard-coded in the bundle — everything comes from /admin/auth/discovery. |
scripts/identity/alphaswarm_admin_entra_setup.py | Operator helper: discovers values from Terraform outputs, prints + optionally writes the env vars, prints the runbook. |
Prerequisites
- The Terraform stack
entra-internalhas been planned + applied for the wiley-tech environment (see bootstrap runbook). - Admin consent has been granted on the staff app's Graph permissions
(
./scripts/identity/grant_admin_consent.sh "$STAFF_CID"). - The
EntraTenantLinkfor the AlphaSwarm staff tenant exists withmeta.kind = 'internal'(python scripts/identity/seed_entra_internal_tenant.py --apply).
Step 1 — Generate the env vars
Every scripts/identity/* script referenced on this page (including
grant_admin_consent.sh and seed_entra_internal_tenant.py above) lives
in the alphaswarm monolith repo, not alphaswarm_admin — run these
commands from an alphaswarm checkout.
# Auto-discover from the Terraform outputs in the wiley-tech env.
python scripts/identity/alphaswarm_admin_entra_setup.py
The script prints two env blocks. Sample output:
# --- Backend env (alphaswarm_admin BFF) ---
ALPHASWARM_ADMIN_AUTH_PROVIDER=msal_entra
ALPHASWARM_ADMIN_AUTH_REQUIRED=true
ALPHASWARM_AUTH_MSAL_INTERNAL_TENANT_ID=12345678-aaaa-bbbb-cccc-deadbeef0000
ALPHASWARM_AUTH_MSAL_INTERNAL_APP_ID=99999999-1111-2222-3333-444444444444
ALPHASWARM_AUTH_MSAL_INTERNAL_AUDIENCE=api://alphaswarm-manage-api
ALPHASWARM_AUTH_OIDC_AUDIENCE=api://alphaswarm-manage-api
ALPHASWARM_ADMIN_ENTRA_TENANT=12345678-aaaa-bbbb-cccc-deadbeef0000
ALPHASWARM_ADMIN_ENTRA_REDIRECT_PATH=/api/auth/entra/callback
# --- Frontend env (alphaswarm_admin/frontend) ---
NEXT_PUBLIC_AQP_AUTH_PROVIDER=msal_entra
NEXT_PUBLIC_AQP_ADMIN_API_URL=http://localhost:8900
To write a .env.alphaswarm_admin.entra file alongside the printout:
python scripts/identity/alphaswarm_admin_entra_setup.py --write-env
The script is intentionally additive: it never overwrites values that
weren't generated by it; the operator merges the block into their
existing Kubernetes manifests / Helm values / .env.local.
Step 2 — Verify the backend can reach Entra
Boot the admin BFF (or restart your existing instance) with the env vars sourced:
set -a; source .env.alphaswarm_admin.entra; set +a
uv run alphaswarm-admin # or: python -m alphaswarm_admin.main
Then hit the new health endpoint:
curl -fsSL http://localhost:8900/admin/auth/health | jq .
Expected output:
{
"ok": true,
"auth_enabled": true,
"issuer": "https://login.microsoftonline.com/12345678-aaaa-bbbb-cccc-deadbeef0000/v2.0",
"audience": "api://alphaswarm-manage-api",
"jwks_uri": "https://login.microsoftonline.com/12345678-.../discovery/v2.0/keys",
"discovery_url": "https://login.microsoftonline.com/12345678-.../v2.0/.well-known/openid-configuration",
"key_count": 7
}
If ok=false, the JSON body's stage field tells you what failed
(discovery, issuer-mismatch, jwks, jwks-empty). Common causes:
- Wrong
ALPHASWARM_AUTH_MSAL_INTERNAL_TENANT_ID→ fix the env var, restart. - Tenant restrictions block the BFF from reaching
login.microsoftonline.com→ talk to Network about egress.
Step 3 — Verify discovery returns the frontend config
curl -fsSL http://localhost:8900/admin/auth/discovery | jq .
Expected:
{
"provider": "msal_entra",
"auth_enabled": true,
"issuer": "https://login.microsoftonline.com/.../v2.0",
"audience": "api://alphaswarm-manage-api",
"scopes": ["api://alphaswarm-manage-api/.default"],
"jwks_uri": "...",
"authority": "https://login.microsoftonline.com/...",
"client_id": "99999999-...",
"tenant_id": "12345678-...",
"redirect_path": "/api/auth/entra/callback",
"claims_namespace": "https://alphaswarm.internal/"
}
The frontend fetches this on mount; no tenant ids land in the JS bundle.
Step 4 — Boot the frontend with MSAL
cd alphaswarm_admin/frontend
# .env.local picks up NEXT_PUBLIC_* automatically.
pnpm dev
open http://localhost:3001
The first page load triggers the AuthProvider to:
fetch('/admin/auth/discovery')against the BFF.- Lazy-import
@azure/msal-browser. - Construct a
PublicClientApplicationwith the discovered config. - Call
handleRedirectPromise()(consumes any pending login round-trip). - Surface the active account via
useAuth().
A signed-in user should see their name + roles in the dashboard header within a few seconds.
Step 5 — End-to-end smoke test
The repo's MSAL round-trip helper validates the full chain:
python scripts/identity/verify_entra_login.py
Expected:
INFO Got access token: eyJ0… (1456 chars)
INFO Claims look correct.
INFO CA policies found: AlphaSwarm-Admins-MFA-Required, AlphaSwarm-Block-Risky-Sign-Ins
INFO All checks passed.
How auth is enforced at runtime
Every subsequent call:
- SPA pulls the bearer via
acquireTokenSilent. - Backend
require_admindependency validates issuer + audience + signature against the cached JWKS, expandsrolesthroughalphaswarm_core.auth.rbac.expand_role. - Step-up routes (
require_admin_step_up) triggeracquireTokenPopupfor a fresh MFA evaluation.
Local dev (no Entra tenant needed)
Set:
export ALPHASWARM_ADMIN_AUTH_REQUIRED=false
# or:
export NEXT_PUBLIC_AQP_AUTH_PROVIDER=mock
Both backend and frontend fall back to a synthetic anonymous user
with admin:cluster scope. The dashboard renders without any IdP
round-trip — ideal for offline contributors.
Troubleshooting
| Symptom | Cause / Fix |
|---|---|
GET /admin/auth/health → 502 stage=discovery | BFF cannot reach login.microsoftonline.com. Check egress. |
GET /admin/auth/health → 502 stage=issuer-mismatch | The configured tenant id doesn't match the tenant that responded. Double-check ALPHASWARM_AUTH_MSAL_INTERNAL_TENANT_ID. |
| Frontend stuck on the loading spinner | Inspect the browser console. The most common message is discovery missing client_id/authority — the BFF returned an incomplete discovery doc, meaning ALPHASWARM_AUTH_MSAL_INTERNAL_APP_ID is empty. |
| Login completes but the user has no roles | The user isn't in any AlphaSwarm-* directory group, or the staff app's API permission consent wasn't granted. Re-run grant_admin_consent.sh. |
| 401 on every API call after login | The bearer's aud doesn't match what the BFF expects. Check that the SPA's scopes came from /admin/auth/discovery (so they include api://alphaswarm-manage-api/.default). |
| Step-up popup never appears | setStepUpSupported(false) was set because the SPA fell back to mock. Confirm NEXT_PUBLIC_AQP_AUTH_PROVIDER=msal_entra. |
Production deployment notes
The same env vars apply in production. In Kubernetes you typically:
- Sync the values into a
Secretvia the External Secrets operator, sourcing fromsecret/alphaswarm/admin/entra/*in Vault. - Mount the Secret as env on the
alphaswarm-adminDeployment. - Build the frontend image with
NEXT_PUBLIC_*baked in (Next.js inlines these at build time).
The Terraform module alphaswarm_entra_directory already creates the staff
app with the production redirect URI
https://manage.alpha-swarm.ai/api/auth/entra/callback; the helper script's
--admin-origin defaults to http://localhost:3001 for dev, override
to https://manage.alpha-swarm.ai for production manifests.
Audit trail
Every Entra-side mutation lands in:
- The Entra audit log — exported to the corporate SIEM via the existing log stream.
- The AlphaSwarm
terraform_runsledger for every Terraform apply on theentra-internalstack. - The AlphaSwarm audit log (Phase 7 §10) on the admin side —
require_adminattaches the user'soidto everyworkload_runsrow, so the admin's mutation surface is fully attributed.