Skip to content

Instantly share code, notes, and snippets.

@jordotech
Last active March 6, 2026 17:32
Show Gist options
  • Select an option

  • Save jordotech/592e2ff5d431851c32eec42079db5349 to your computer and use it in GitHub Desktop.

Select an option

Save jordotech/592e2ff5d431851c32eec42079db5349 to your computer and use it in GitHub Desktop.
Multi-Regional Deployment: EY SSO Region Redirect - Implementation Plan

Multi-Regional Deployment: EY SSO Region Redirect

What Are We Building?

EY has 3 regional deployments, each with its own frontend and backend:

Region Frontend Backend
US (us-east-2) platform-ey-us-east-2.capitol.ai US backend
EU (eu-west-1) platform-ey-eu-west-1.capitol.ai EU backend
APAC (ap-southeast-1) platform-ey-ap-southeast-1.capitol.ai APAC backend

When an EY SSO user logs in, we need to:

  1. Detect their country from Azure AD
  2. Route them to the correct regional frontend
  3. Enforce on the backend that users can't access the wrong region's API

How SSO Regional Redirect Works

sequenceDiagram
    actor User
    participant EY as ey.capitol.ai
    participant Auth0 as Auth0
    participant Azure as Azure AD
    participant API as Platform API
    participant Regional as Regional Frontend

    User->>EY: Visit ey.capitol.ai
    EY->>Auth0: loginWithRedirect()
    Auth0->>Azure: OIDC authentication
    Azure-->>Auth0: ID token with ctry claim
    Auth0-->>EY: JWT with idp_claims.country
    Note over EY: Read country from JWT
    EY->>API: GET /public/organizations/{orgid}/region?country=GB
    API-->>EY: { region: "eu-west-1", frontend_url: "https://platform-ey-eu-west-1.capitol.ai" }
    alt Already on correct region
        Note over EY: No redirect needed
    else Needs different region
        EY->>Regional: Redirect via window.location
        Regional->>Auth0: getAccessTokenSilently() via refresh token
        Auth0-->>Regional: New access token (no re-login needed)
        Note over Regional: User is authenticated automatically
    end
Loading

Key insight: The user does NOT have to log in twice. Our Auth0 SDK is configured with useRefreshTokens={true} and cacheLocation='localstorage', so getAccessTokenSilently() on the regional frontend obtains a new token via a refresh token HTTP POST to Auth0's /oauth/token endpoint — no hidden iframe, no third-party cookie dependency. This is Auth0's recommended approach and works reliably across Chrome, Safari, and all browsers that block third-party cookies.

Backend Region Enforcement

The redirect handles the happy path, but we also need server-side enforcement to prevent API access from the wrong region. Since the EY org has the same UUID across all 3 regional backends, org-level checks alone aren't enough.

flowchart TD
    A[Request hits backend] --> B{Is this an Auth0/SSO user?}
    B -->|No - OTP user| C[Skip region check]
    B -->|Yes| D{Does org have regional_deployment: true?}
    D -->|No| C
    D -->|Yes| E[Get country from idp_claims]
    E --> F[Map country to region via country_region_map]
    F --> G{User's region matches this backend's AWS_REGION?}
    G -->|Yes| H[Allow access]
    G -->|No| I[403 Forbidden - wrong region]
Loading

Implementation Tasks

Task 1: Add region fields to org schema ✅

Add two optional fields to the organization model:

  • regional_deployment: bool - flag to enable region checks
  • region_config: { region_urls: { "eu-west-1": "https://..." } } - maps regions to frontend URLs

These are optional with None defaults, so existing orgs are unaffected.

Task 2: Expose region fields in org update API ✅

The existing PUT endpoint already handles optional fields via model_dump(exclude_none=True). Just need to verify it works and add clearing support (sending null to remove config).

Task 3: New region lookup endpoint ✅

GET /public/organizations/{orgid}/region?country=GB

Returns { "region": "eu-west-1", "frontend_url": "https://platform-ey-eu-west-1.capitol.ai" }.

The frontend calls this after SSO to determine where the user should be routed.

Task 4: Server-side region enforcement ✅

In check_org_access (the auth dependency), after verifying org membership:

  1. Check if org has regional_deployment: true
  2. Get user's country from idp_claims.country
  3. Map to region using country_region_map.get_region()
  4. Compare against backend's AWS_REGION
  5. 403 if mismatch

Task 5: Seed EY org config

Update the EY org record in all 3 regional DynamoDB tables with the region URLs.

Task 6: Frontend redirect logic

After SSO callback, read country from JWT and call region lookup endpoint. Redirect if needed.

Task Dependencies

flowchart LR
    T1[Task 1: Schema ✅] --> T2[Task 2: Update API ✅]
    T1 --> T3[Task 3: Region endpoint ✅]
    T1 --> T4[Task 4: Enforcement ✅]
    T2 --> T5[Task 5: Seed EY org]
    T4 --> T5
    T3 --> T6[Task 6: FE redirect]
Loading

Tasks 1-4 are complete (PR #381). Tasks 5 and 6 remain.

What the Frontend Needs to Do

After SSO callback on ey.capitol.ai:

  1. Decode the JWT and read idp_claims.country
  2. Call GET /public/organizations/{orgid}/region?country={ctry}
  3. If frontend_url differs from current origin, redirect via window.location
  4. On the regional frontend, getAccessTokenSilently() uses refresh token rotation to get a new access token — no second SSO prompt, no iframe, no third-party cookie issues

Why refresh tokens work here

Our Auth0Provider is configured with:

<Auth0Provider
  cacheLocation='localstorage'
  useRefreshTokens={true}
  ...
>

This means the Auth0 SDK stores a refresh token in localStorage (not a session cookie). When the user lands on the regional frontend, getAccessTokenSilently() exchanges the refresh token for a new access token via a direct HTTP POST to Auth0 — no hidden iframe needed. This avoids the third-party cookie restrictions in Chrome/Safari that would break the iframe-based approach.

Out of Scope

  • Per-org country-to-region mapping overrides (shared country_region_map.py for now)
  • Region selection UI for users (rely on Azure AD ctry claim)

Questions?

Reach out in the thread if anything is unclear.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment