Skip to content

Instantly share code, notes, and snippets.

@jordotech
Last active February 23, 2026 15:06
Show Gist options
  • Select an option

  • Save jordotech/82f71280953b79a5df8336601411a075 to your computer and use it in GitHub Desktop.

Select an option

Save jordotech/82f71280953b79a5df8336601411a075 to your computer and use it in GitHub Desktop.
EY SSO Integration: Technical Update (Auth0 + Azure Entra ID)

EY SSO Integration: Technical Update

Overview

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.

Architecture

Authentication Flow

  • 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/me call
  • 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_id claim into access token
  • Token validation: Platform API validates JWT via JWKS, extracts organization context

Key Components

Auth0 Configuration:

  • SPA Application (Authorization Code + PKCE)
  • Azure AD Enterprise Connection (waad strategy)
  • 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 with source tracking

Frontend Changes:

  • @auth0/auth0-react SDK integration
  • Email-first login UI with SSO detection and org branding
  • /callback route for OAuth redirect handling
  • Token management with refresh rotation

SSO Configuration

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 /authorize call.
  • sso.provider: Display identifier for the frontend (e.g. "azure_ad" renders as "Sign in with Microsoft").

Auth Discovery Logic

The POST /public/auth/discover endpoint runs two checks in parallel:

  1. Membership-based: Query org_members_v1 by email. For each org, check if it has sso config.
  2. 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)

JIT Membership Provisioning

When an SSO-authenticated user hits GET /organizations/me:

  1. Existing memberships are loaded normally
  2. After the membership loop, check the Auth0 JWT for an org_id claim
  3. 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
  4. 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

Implementation Status

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.app with 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

Deployment Phases

Phase 2: EY Non-Prod Tenant

Requirements from EY:

  • App secret for Client ID 264ae0dd-...
  • Confirmation of OIDC configuration with openid profile email scopes

Process:

  1. Update AWS SSM parameters with credentials
  2. Run terraform apply to update Azure AD connection
  3. Test with EY accounts on staging
  4. Verify Capitol admin OTP access still works

Phase 3: Production Deployment

  1. Obtain EY production Azure app registration
  2. Update SSM parameters and apply Terraform
  3. Deploy to production environment
  4. Configure EY org record with SSO block and allowed_domains
  5. Validation testing (JIT provisioning, domain discovery)

Phase 4: Multi-Environment Support

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

Phase 5: AD Group Mapping (Future)

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

Credential Management

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 --overwrite

Then apply: cd terraform/auth0 && terraform apply

Design Principles

  • Phased rollout: EY-specific first, generalize later
  • Data-driven SSO: Controlled by org record sso config, 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

Pending Items

  • 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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment