Legacy services proxy
Purpose: stable façade to legacy C# microservices with circuit breaker, caching, limits, and metrics.
- Code:
ms_bff_spike/ms_bff/src/api/v1/endpoints/legacy_proxy.py - Config:
ServiceConfigs/BFF/config/legacy_services.yamllegacy_services: service→base URL (override viaLEGACY_SERVICE_{NAME}_URL)legacy_service_timeouts: per‑service timeoutscircuit_breaker.threshold,circuit_breaker.reset_timeresponse_cache.enabled,default_ttl,max_sizerequest_limits.max_body_size
Auth: BFF session or bearer token. The legacy route accepts either an authenticated BFF session (cookie) or a caller-supplied EmpowerID bearer token. BFF injects downstream bearer and headers.
Authentication model
- Front-door: the legacy route is protected by
Depends(get_current_user); callers can authenticate with either a valid BFF session (cookie) or by sendingAuthorization: Bearer <access_token>from EmpowerID. - Downstream: the proxy forwards the caller's token unchanged in
Authorization: Bearer ...whenrequest.state.tokenexists; forservice=empowerid, it also injectsX-EmpowerID-Api-Keyif configured. - Identity propagation: when a validated user ARN is available,
X-Original-Useris added for auditing/attribution. The provider segment in ARNs prefers the IdP entryprovideralias (falls back toname), stabilizing identities across audiences of the same issuer.
Endpoints
- Any method
/api/v1/proxy/{service}/{path} - GET
/api/v1/proxy/health(proxy health and cache stats)
Headers injected
X-Correlation-IDwhen availableX-Original-Userwhen ARN presentAuthorization: Bearer ...from BFF state when available (caller token pass-through)- For
service=empowerid,X-EmpowerID-Api-Keyif configured
How SPA, BFF, and EmpowerID work together (legacy route)
-
SPA login (EmpowerID IdP)
- Use OIDC Authorization Code + PKCE to authenticate against EmpowerID.
- After login, you have two options to call the BFF:
- Bearer mode: SPA sends
Authorization: Bearer <access_token_from_EmpowerID>to the BFF. - Session mode (recommended for SPAs): BFF does the code exchange server-side and issues an HttpOnly secure session cookie; SPA calls the BFF without storing tokens.
- Bearer mode: SPA sends
-
Auth to the BFF
- The legacy route requires
Depends(get_current_user); the BFF accepts either the user's bearer token or an authenticated session. - The BFF then exposes the user info as
TokenPayloadand setsrequest.state.token(when available).
- The legacy route requires
-
Calling EmpowerID through the legacy route
- SPA calls the BFF at
/{service_name}/{path}withservice_name = empowerid(e.g.,/api/v1/proxy/empowerid/v1/users/me). - The BFF's proxy behavior:
- Forwards the caller's token unchanged in
Authorization: Bearer <token>ifrequest.state.tokenexists. - Injects
X-EmpowerID-Api-Keywhenservice_name == 'empowerid'andsettings.empowerid_api_keyis set. - Adds
X-Original-Userwith the validated user ARN for audit.
- Forwards the caller's token unchanged in
- There is no token exchange; it is pass-through of the user's EmpowerID token plus an API key.
- SPA calls the BFF at
Examples
curl "https://.../api/v1/proxy/res-admin/services/v1/resadmin/resources/people/getsearch?top=25" \
--cookie "_eid_sid=..."
curl -X POST "https://.../api/v1/proxy/res-admin/services/v1/resadmin/resources/People/create" \
-H "Content-Type: application/json" \
--cookie "_eid_sid=..." \
-d '{"FirstName":"Ada","LastName":"Lovelace","Email":"ada@example.com","UserName":"ada"}'
Minimal SPA example (legacy EmpowerID via BFF)
// After OIDC login with EmpowerID (Auth Code + PKCE)
// Bearer mode
const res = await fetch('/api/v1/proxy/empowerid/v1/users/me', {
method: 'GET',
headers: { Authorization: `Bearer ${accessToken}` }
});
// Session mode: omit Authorization, include cookies
const resSession = await fetch('/api/v1/proxy/empowerid/v1/users/me', {
credentials: 'include'
});
Mermaid
Failure modes
- Circuit open: 503 with
Retry-After - Body too large: 413 based on
max_body_size - Unknown service: 404 from proxy
Observability
- Prometheus counters/timers for requests/errors/CB state and cache hits/misses
Change control
- Add a service in
legacy_serviceswith base URL, optional timeout; adjust CB/cache as needed. Promote via config SOP.
See also: ../how-to/add-legacy-service, ../how-to/tune-legacy-circuit-cache
For SPA developers
-
How to call from React (same-origin recommended):
// GET with query params (cookies sent automatically)
const res = await apiClient.get(
'/api/v1/proxy/res-admin/services/v1/resadmin/resources/people/getsearch',
{ params: { top: 25 } }
);
// POST JSON
await apiClient.post(
'/api/v1/proxy/res-admin/services/v1/resadmin/resources/People/create',
{ FirstName: 'Ada', LastName: 'Lovelace', Email: 'ada@example.com', UserName: 'ada' }
);
// POST file upload (watch 10MB default body limit)
const form = new FormData();
form.append('file', file);
await apiClient.post('/api/v1/proxy/my-service/upload', form); -
Cross-origin dev: set
VITE_BFF_BASE_URLto the BFF origin and ensure your client usescredentials: 'include'so cookies flow. See Dev vs Prod setup. -
If using session mode, do not add Authorization headers in the browser; the BFF injects downstream credentials. If using bearer mode, include your EmpowerID access token in the
Authorizationheader. -
Prefer YAML-driven canonical routes under
/api/...when available; the legacy proxy is a compatibility bridge.
Common errors and UX
| Status | Meaning | Suggested UX |
|---|---|---|
| 401 | No/expired session | Redirect to login |
| 403 | PDP/authorization deny | Show Access Denied (no retry) |
| 413 | Body too large | Show guidance; split upload or contact admin |
| 502 | Upstream error | Toast error; log correlation ID; retry optional |
| 503 | Circuit open/backoff | Show retry with backoff; try later |
Helpful links: ../how-to/uploads-downloads-streaming, ../reference/frontend-errors, ../how-to/dev-vs-prod-setup, ./proxy-yaml-reference.