Skip to content

LCO DMA Authentication Implementation (Dec 19, 2025)

Overview

This page documents the authentication work delivered on December 19, 2025. The implementation adds Microsoft Entra ID login, frontend auth UX, and API key support as an alternate backend authentication path. It also locks down API routes and adds role-based authorization.

Scope (Today's Commits)

  • f4efc09 - Microsoft Entra ID authentication (backend + frontend)
  • 6bd1def - Auth UX (overlay, inline prompts) + protected pages
  • fad4c08 - API key authentication + management endpoints

Architecture Summary

  • Primary auth: Microsoft Entra ID via MSAL (frontend) and fastapi-azure-auth (backend)
  • Alternate auth: API keys via X-API-Key header (backend)
  • Role model: Reader (read access), Contributor (write + key management)
  • Protection model: All v1 routers are gated by a shared auth dependency when auth is enabled

Backend Implementation

1) Entra ID (JWT) Authentication

Core module: backend/app/core/auth.py

  • Uses SingleTenantAzureAuthorizationCodeBearer from fastapi-azure-auth.
  • Scope is generated as api://{AZURE_CLIENT_ID}/user_impersonation.
  • On request, auth dependency validates the token and stores the user in request.state.user.
  • Swagger UI OAuth config is provided when AZURE_OPENAPI_CLIENT_ID is set.
  • OpenID config is loaded during application startup when auth is enabled.

Key objects and helpers: - AuthenticatedUser model (includes roles and auth_type) - require_auth (base dependency) - require_reader_role and require_contributor_role

2) Router Protection

Main app: backend/app/main.py

  • Auth dependency is applied to all v1 routers when AZURE_AUTH_ENABLED=true (this is the master toggle for enforcement, including API keys).
  • When auth is disabled, dependencies are not applied and a mock user is used.

Protected routers include: - Clients, Projects, Services, Crews, Equipment, Materials, WBS, COA, Service Crews, MTO, etc. - API Keys router (/api/v1/api-keys)

3) API Key Authentication (Alternate Path)

Core module: backend/app/core/auth.py

  • If API_KEYS_ENABLED=true, the auth dependency checks X-API-Key first.
  • Valid API keys create an AuthenticatedUser with auth_type="api_key" and role derived from the key.
  • If no key is present or invalid, it falls back to Entra ID JWT.

Repository: backend/app/repositories/api_key.py

  • Keys are generated as lco_<32 chars>
  • Stored as bcrypt hashes; plaintext is returned only once on creation
  • keyPrefix (first 8 characters) is used for partition key and lookup
  • lastUsedAt is updated on successful verification

Cosmos container: - New container: ApiKeys (partition key: /keyPrefix)

4) API Key Endpoints

Router: backend/app/api/v1/routers/api_keys.py

  • POST /api/v1/api-keys - create key (Contributor only)
  • GET /api/v1/api-keys - list keys (Contributor only)
  • DELETE /api/v1/api-keys/{key_id} - revoke key (Contributor only)

5) Roles & Authorization

Role handling: - Reader OR Contributor -> read access - Contributor only -> write access and API key management

Roles are sourced from: - Entra ID token claims (roles claim) - API key role in Cosmos (Reader or Contributor)

6) Dev Mode / Auth Disabled

When AZURE_AUTH_ENABLED=false: - A mock user is returned with Contributor role - Router-level dependencies are not applied - Intended for local development only

7) CORS Security Fix

CORS is now explicitly configured with allowed origins (no wildcard).


Frontend Implementation

1) MSAL Integration

Files: - frontend/src/auth/msalConfig.ts - frontend/src/auth/msalInstance.ts - frontend/src/main.tsx

Key behavior: - MsalProvider wraps the app - loginPopup() used for interactive sign-in - Tokens are acquired silently for API calls

2) Auth State Management

Store + hook: - frontend/src/stores/authStore.ts (Zustand) - frontend/src/hooks/auth/useAuth.ts

State tracked: - status: initializing | authenticated | unauthenticated | logging_in | logging_out - user and roles - overlayDismissed

3) API Client Token Handling

File: frontend/src/services/api/config.ts

  • Axios request interceptor acquires access tokens via MSAL
  • Authorization: Bearer <token> added automatically
  • AuthenticationError thrown on 401 so UI can suppress generic error toasts

4) Auth UI/UX

Components: - AuthOverlay (dismissible sign-in modal on app load) - AuthRequired (inline "Sign in to view ..." guard) - LoginButton (sidebar footer)

Behavior: - Overlay appears for unauthenticated users until dismissed - Pages wrap sensitive content with AuthRequired - Data fetching is skipped when unauthenticated

5) Protected Pages (UI)

Wrapped with AuthRequired and guarded against API calls: - Projects (list, detail, create, edit) - Universal Tables (crews, crew members, crew trades, equipment, WBS, materials, COA) - API Keys settings page


Configuration

Backend (.env)

Set these when auth is enabled: - AZURE_AUTH_ENABLED=true|false - AZURE_CLIENT_ID=<app registration client id> - AZURE_TENANT_ID=<tenant id> - AZURE_OPENAPI_CLIENT_ID=<optional swagger client id> - API_KEYS_ENABLED=true|false - API_KEYS_CONTAINER=ApiKeys

Frontend (.env)

  • VITE_AZURE_CLIENT_ID=<app registration client id>
  • VITE_AZURE_TENANT_ID=<tenant id>
  • VITE_AZURE_API_SCOPE=api://<client_id>/user_impersonation
  • VITE_API_URL=<backend base url>

Operational Notes

Calling the API with an API Key

Send the API key in the X-API-Key header:

curl -H "X-API-Key: lco_..." https://<host>/api/v1/projects

Token-Based Calls

Frontend automatically attaches the bearer token. If calling manually:

curl -H "Authorization: Bearer <access_token>" https://<host>/api/v1/projects

Key Rotation / Revocation

  • Keys are only shown once on creation
  • Revoke via DELETE /api/v1/api-keys/{key_id}
  • Re-create to rotate

Files Touched (Key References)

Backend: - backend/app/core/auth.py - backend/app/main.py - backend/app/api/v1/routers/api_keys.py - backend/app/repositories/api_key.py - backend/app/schemas/api_key.py - backend/app/core/config.py - backend/app/db/cosmos.py

Frontend: - frontend/src/auth/msalConfig.ts - frontend/src/auth/msalInstance.ts - frontend/src/hooks/auth/useAuth.ts - frontend/src/stores/authStore.ts - frontend/src/services/api/config.ts - frontend/src/components/auth/AuthOverlay.tsx - frontend/src/components/auth/AuthRequired.tsx - frontend/src/components/auth/LoginButton.tsx - frontend/src/App.tsx - frontend/src/main.tsx


Follow-Up Ideas (Optional)

  • Add .env.example entries for the new auth-related env vars
  • Add a small integration test that validates API key auth flow
  • Add role-based UI badges (Reader vs Contributor) in the sidebar