This document describes an enterprise SSO implementation for EY using Auth0 and Azure Entra ID. Phase 1 is complete and validated end-to-end on staging as of February 23, 2026.
- Email-first discovery: Users enter email, backend checks org memberships AND email domain for SSO configuration
- Dual-path discovery: Both membership-based and domain-based checks always run, results are merged with deduplication
- Domain-based SSO: New users (no existing membership) see SSO options if their email domain matches an org's
sso.allowed_domains - JIT provisioning: First-time SSO users are auto-added to the org on initial
/organizations/mecall - SSO organizations: Redirect through Auth0 -> Azure Entra ID -> Microsoft login
- Non-SSO organizations: Continue using existing OTP flow (unchanged)
- JWT enrichment: Auth0 post-login Action injects
org_idclaim into access token - Token validation: Platform API validates JWT via JWKS, extracts organization context
Auth0 Configuration:
- SPA Application (Authorization Code + PKCE)
- Azure AD Enterprise Connection (
waadstrategy) - Auth0 Organization (
ernst_young) - Post-Login Actions for JWT enrichment and access control
- Resource Server:
https://platform-api.capitol.ai
Backend Changes:
POST /public/auth/discover- Returns available auth methods per email (membership + domain check)GET /public/organizations/me- Supports Auth0 Bearer tokens, JIT membership provisioning- Auth0 JWT validation middleware (alongside existing token validation)
OrgMembersClient.add_member()- Creates membership records withsourcetracking
Frontend Changes:
@auth0/auth0-reactSDK integration- Email-first login UI with SSO detection and org branding
/callbackroute for OAuth redirect handling- Token management with refresh rotation
Organizations with SSO have this structure in DynamoDB:
{
"orgid": "62c7fff9-...",
"name": "EY",
"sso": {
"provider": "azure_ad",
"auth0_org_id": "org_wDw6ehkmFCKuJT6R",
"auth0_connection_id": "capitol-azure",
"allowed_domains": ["ey.com", "uk.ey.com"]
}
}sso.allowed_domains: Email domains that qualify for SSO via domain-based discovery. Enables new users to see SSO before having a membership.sso.auth0_org_id: Auth0 Organization ID, used to redirect to the correct Auth0 org.sso.auth0_connection_id: Auth0 connection name, passed to the/authorizecall.sso.provider: Display identifier for the frontend (e.g."azure_ad"renders as "Sign in with Microsoft").
The POST /public/auth/discover endpoint runs two checks in parallel:
- Membership-based: Query
org_members_v1by email. For each org, check if it hasssoconfig. - Domain-based: Scan all orgs. For each with
sso.allowed_domains, check if the email domain matches.
Results are deduplicated by auth0_org_id. This ensures:
- Existing SSO members see SSO (membership path)
- New employees see SSO before first login (domain path)
- Users with only non-SSO memberships but matching domain see both SSO and OTP
- Unknown emails get OTP only (prevents enumeration)
When an SSO-authenticated user hits GET /organizations/me:
- Existing memberships are loaded normally
- After the membership loop, check the Auth0 JWT for an
org_idclaim - If the claim points to an SSO org the user doesn't already belong to:
- Create membership:
{orgid, email, role: "member", source: "sso_jit"} - Add the org to the response
- Create membership:
- User lands in the SSO org on first login without manual provisioning
The source field on org_members_v1 tracks how memberships were created:
"invite"- manually added via platform UI"sso_jit"- auto-created on first SSO login
Completed (Phase 1):
- Auth0 infrastructure deployed via Terraform (SPA app, Azure AD connection, Organization, post-login Action)
- Platform API: dual-path auth discovery, JIT provisioning, Auth0 JWT middleware (PR #363)
- Platform Frontend: Auth0 SPA SDK, email-first login with branding, callback route (PR #1063)
- End-to-end validated on staging at
ey-sso.vercel.appwith Capitol test Azure tenant - Domain-based discovery verified: new users see SSO option based on email domain
- JIT membership provisioning verified: first-time SSO users auto-added to EY org
Unchanged Systems:
- OTP authentication flow
- M2M service authentication
- Foreign API authentication (X-User-ID + X-API-Key)
- All existing DynamoDB configurations
Requirements from EY:
- App secret for Client ID
264ae0dd-... - Confirmation of OIDC configuration with
openid profile emailscopes
Process:
- Update AWS SSM parameters with credentials
- Run
terraform applyto update Azure AD connection - Test with EY accounts on staging
- Verify Capitol admin OTP access still works
- Obtain EY production Azure app registration
- Update SSM parameters and apply Terraform
- Deploy to production environment
- Configure EY org record with SSO block and
allowed_domains - Validation testing (JIT provisioning, domain discovery)
Currently Auth0 Organization metadata stores a single capitol_org_id. For multi-environment support:
- Create per-environment Auth0 SPA clients
- Store environment-keyed org IDs in metadata (e.g.
capitol_org_id_staging,capitol_org_id_prod) - Update post-login Action to resolve environment from client ID
Architecture supports group-based role mapping:
- Azure Entra provides group claims
- Auth0 Action maps AD groups to Capitol roles
- Platform API reads role claims from JWT
To switch to EY's production Azure AD tenant:
aws ssm put-parameter \
--name "/internal/prod/auth0/azure-ad/client-id" \
--value "<EY_CLIENT_ID>" \
--type SecureString --overwrite
aws ssm put-parameter \
--name "/internal/prod/auth0/azure-ad/client-secret" \
--value "<EY_CLIENT_SECRET>" \
--type SecureString --overwrite
aws ssm put-parameter \
--name "/internal/prod/auth0/azure-ad/tenant-domain" \
--value "<EY_TENANT_DOMAIN>" \
--type SecureString --overwriteThen apply: cd terraform/auth0 && terraform apply
- Phased rollout: EY-specific first, generalize later
- Data-driven SSO: Controlled by org record
ssoconfig, no feature flags - Dual-path discovery: Both membership and domain checks always run, preventing blind spots
- Zero impact: Existing OTP flow completely untouched
- Privacy: Unknown emails return OTP option (prevents enumeration)
- Scale-ready: JIT provisioning handles hundreds of EY employees without manual onboarding
- Direct validation: JWT validation via JWKS without legacy dependencies
- EY non-prod app secret
- EY production app registration details
- AD group mapping requirements specification
- Per-environment org ID mapping in Auth0 metadata (Phase 4)
- PR merges to main branches