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:
- Detect their country from Azure AD
- Route them to the correct regional frontend
- Enforce on the backend that users can't access the wrong region's API
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
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.
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]
Add two optional fields to the organization model:
regional_deployment: bool- flag to enable region checksregion_config: { region_urls: { "eu-west-1": "https://..." } }- maps regions to frontend URLs
These are optional with None defaults, so existing orgs are unaffected.
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).
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.
In check_org_access (the auth dependency), after verifying org membership:
- Check if org has
regional_deployment: true - Get user's country from
idp_claims.country - Map to region using
country_region_map.get_region() - Compare against backend's
AWS_REGION - 403 if mismatch
Update the EY org record in all 3 regional DynamoDB tables with the region URLs.
After SSO callback, read country from JWT and call region lookup endpoint. Redirect if needed.
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]
Tasks 1-4 are complete (PR #381). Tasks 5 and 6 remain.
After SSO callback on ey.capitol.ai:
- Decode the JWT and read
idp_claims.country - Call
GET /public/organizations/{orgid}/region?country={ctry} - If
frontend_urldiffers from current origin, redirect viawindow.location - 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
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.
- Per-org country-to-region mapping overrides (shared
country_region_map.pyfor now) - Region selection UI for users (rely on Azure AD
ctryclaim)
Reach out in the thread if anything is unclear.