Skip to main content

Custom domain and authentication Azure Static Web Apps

  1. https://docs.microsoft.com/en-us/azure/app-service/security-recommendations
  2. https://docs.microsoft.com/en-us/security/benchmark/azure/baselines/app-service-security-baseline
  1. https://docs.microsoft.com/en-us/azure/static-web-apps/custom-domain
  2. https://docs.microsoft.com/en-us/azure/static-web-apps/custom-domain-external
  3. https://docs.microsoft.com/en-us/azure/static-web-apps/custom-domain-azure-dns

Discussion with Viraj

Notes from a discussion with Viraj in June 2026.

GQC has two common authentication patterns:

  1. React frontend apps that use the shared EPARSMS FastAPI backend for login and API access.
  2. Apps that run inside Azure VMs and manage their own authentication.

Two models at a glance

  • React SPA stack: React apps on Azure Static Web Apps call rsms-api-fastapi, which is EPARSMS FastAPI on Azure App Service. Authentication is handled by EPARSMS FastAPI using JWT tokens.
  • VM-hosted stack: The backend and frontend run inside an Azure VM, often behind NGINX. Authentication is handled by the app running in that VM, for example with Django session auth or DRF tokens.

React SPAs do not authenticate through a VM backend. They call EPARSMS FastAPI for login and protected API routes.

EPARSMS FastAPI + React SPAs

EPARSMS FastAPI is the rsms-api-fastapi repo. It is deployed to Azure App Service, for example https://eparsms.azurewebsites.net.

React frontend apps that use this API for authentication include:

  • rsms-frontend-react
  • dwd-frontend-react
  • swm-frontend-react
  • Other -frontend-react apps in the rsms-suite that point at the same API

Each React app is hosted separately, usually in Azure Static Web Apps. At build time, the app is configured with the API URL:

VITE_API_BASE_URL=https://eparsms.azurewebsites.net

In local development, Vite usually proxies /api to http://localhost:8000. That means the browser sees the request as same-origin, so CORS is usually not involved during local development.

How login works

  1. User submits email and password on the login page.
  2. The React app calls POST /auth/login on EPARSMS FastAPI.
  3. API validates credentials (bcrypt password hash) and returns JWT tokens.
  4. SPA stores the access token in memory and the refresh token in localStorage.
  5. SPA calls GET /auth/me with Authorization: Bearer <access_token>.
  6. Protected routes use an auth guard; unauthenticated users are sent to /login.

Token lifecycle

  • Access token: Short lived, about 60 minutes. Stored in memory and sent as Authorization: Bearer <token> on API calls.
  • Refresh token: Longer lived, about 7 days. Stored in localStorage as rsms_refresh_token and used with POST /auth/refresh.

On app load, if a refresh token exists, the SPA silently refreshes the access token and fetches /auth/me. If refresh fails, tokens are cleared and the user is redirected to login.

On 401 from any API call, the client tries refresh once, retries the request, or logs the user out.

Auth endpoints

  • POST /auth/login: no auth required. Sends email and password, returns tokens.
  • POST /auth/refresh: no access token required. Sends refresh token in the body, returns a new access token.
  • POST /auth/logout: requires Bearer token. Server acknowledges logout.
  • GET /auth/me: requires Bearer token. Returns the current user, including id, email, and role.

Users and roles

  • There is no public registration.
  • Admins create users. The first user is created with a CLI script. After that, a super_admin can create users through POST /users.
  • Users live in Azure Table Storage for that deployment.
  • Roles are super_admin, analyst, and viewer.
  • Each protected endpoint checks the caller's role.

Implementation details: see rsms-api-fastapi/docs/auth-implementation.md.

VM-hosted apps (RSMS, EPARSMS on VMs)

Some deployments still run the full application on an Azure Linux VM. In this model, the backend, database, and frontend live together behind NGINX. Examples include older RSMS and EPARSMS setups and apps like Ohio River flows (riverflows_django on a VM).

These apps do not use EPARSMS FastAPI for authentication. Auth is handled inside the VM stack:

  • Django session authentication or DRF token/JWT, depending on the project
  • NGINX terminates SSL and may add CORS headers (not the FastAPI middleware)
  • Users, passwords, and sessions are managed by that VM app, not the shared EPARSMS API

When working on a VM-hosted app, look at that project's auth code and deployment docs, not rsms-api-fastapi.

CORS for EPARSMS FastAPI

React SPAs run on a different origin than the API (for example SWA hostname vs eparsms.azurewebsites.net). The browser enforces CORS, so the API must allow the SPA origin.

EPARSMS FastAPI handles CORS in application code with FastAPI's CORSMiddleware (app/config.py). Built-in defaults include local dev ports such as localhost:3000, localhost:3001, localhost:3002, and some deployed SWA URLs.

Add production origins with CORS_ORIGINS_EXTRA

For each new frontend origin, add it to the Azure App Service application setting:

Setting name: CORS_ORIGINS_EXTRA

Example value:

https://your-app.azurestaticapps.net,https://app.yourdomain.com
  • Comma-separated list, no spaces required (trimmed automatically).
  • These origins are added to the built-in defaults. They do not replace them.
  • After changing the setting, restart the Web App if origins do not take effect immediately.

Do not add SPA origins through the Azure Portal CORS blade on the Web App (see troubleshooting below).

Local development

Defaults in config.py already allow common local origins. If your dev server uses a different port, add it to CORS_ORIGINS_EXTRA in your local .env file.

Use 127.0.0.1 instead of localhost when proxying to Django or FastAPI locally. Some stacks behave differently with localhost (see common React issues).

CORS troubleshooting: Azure Portal CORS vs app middleware

Symptom

After adding an origin in Azure Portal → Web App → API → CORS, other frontends stop working. Origins configured in CORS_ORIGINS_EXTRA or the app's default list no longer receive correct Access-Control-Allow-Origin headers.

Cause

Azure App Service has platform-level CORS. When any origin is saved in the Portal CORS blade, Azure handles CORS before the request reaches FastAPI. The app's CORSMiddleware and CORS_ORIGINS_EXTRA are effectively bypassed. Only the Portal list applies.

Fix

  1. Open Azure Portal → your EPARSMS Web App → API → CORS.
  2. Remove all origins from the Portal CORS list (leave it empty).
  3. Save and restart the app if needed.
  4. Add every required SPA origin to CORS_ORIGINS_EXTRA in Application settings instead.

Rule of thumb

  • Use CORS_ORIGINS_EXTRA in Azure App Service Application settings for EPARSMS FastAPI.
  • Do not use the Azure Portal CORS blade for EPARSMS FastAPI because it overrides the app middleware.