Skip to main content

πŸ” CONDOR Authentication & Authorization

Last updated: March 2026 Applies to: Server v2.x with Entra ID SSO support


Table of Contents​


Overview​

CONDOR supports three authentication methods, resolved in priority order on every request:

PriorityMethodCredentialUse Case
1Entra ID (SSO)JWT via ?token= URL param β†’ session cookieUsers arriving from the Original App
2Session Cookiesession httpOnly cookieBrowser sessions (login or SSO)
3API Key BearerAuthorization: Bearer <key> headerMachine-to-machine / scripts

All three methods ultimately resolve to either a Session or EntraUser object that routes consume via Depends(SessionService.require_session).


Architecture Diagram​


Authentication Methods​

1. Entra ID Single Sign-On (SSO)​

The primary authentication method for users accessing CONDOR from the Original App.

How It Works​

  1. User clicks "CONDOR" in the Original App
  2. Original App redirects to https://condor.example.com/?token=<JWT>
  3. CONDOR validates the JWT once (signature, expiry, issuer, audience, azp, roles)
  4. Validated EntraUser + raw JWT cached in Redis with TTL matching JWT exp
  5. Session cookie set on a 302 redirect to / (clean URL)
  6. All subsequent requests use the session cookie β†’ cached EntraUser loaded from Redis
  7. No JWT re-validation on API calls β€” Redis TTL handles expiry

JWT Claims Used​

ClaimPurposeExample
audMust match ENTRA_CLIENT_ID6999a97c-c1cf-4809-b14c-05bc596bb0c5
issMust match tenant issuer URLhttps://{tenant}.ciamlogin.com/{tenant}/v2.0
azpMust be in ENTRA_ALLOWED_CLIENT_IDS64abed69-d2d8-402e-9a04-e83c2cc8bde2
expToken expiry β†’ session cookie TTLUnix timestamp
rolesRBAC β€” must include Condor.Write["Condor.Write", "Camera.Operate"]
oidUser's Object ID in Azure ADUUID
nameDisplay nameSharath Gaddameedi
preferred_usernameEmailuser@live.com
tidTenant IDUUID

Role Enforcement​

  • Condor.Write β€” Required at bootstrap time to create a session. Without this role, the user sees an error on the login page.
  • Route-level roles β€” Individual routes can enforce additional roles using EntraAuthService.require_roles("RoleName").

2. Username / Password Login​

Traditional form-based login for direct access without SSO.

PropertyValueReason
httpOnlytruePrevents JavaScript access (XSS protection)
securetrueHTTPS only (localhost exempt in browsers)
samesitelaxCSRF protection for cross-origin requests
max_ageNot set (session cookie)Dies with browser tab

3. API Key Bearer Token​

For machine-to-machine access (scripts, automated exports, health checks).

  • Stateless β€” No Redis session; a new Session object is created per request
  • No CSRF β€” Bearer tokens are not vulnerable to CSRF
  • No TTL refresh β€” No session to extend
  • Key format: SESSION_API_KEYS='{"api-key-value": "username"}'

Auth Resolution Order​

Every protected route uses Depends(SessionService.require_session), which calls get_session() internally:

Key insight: Entra-bootstrapped sessions (via /?token=JWT) resolve through the cookie path on subsequent requests. The cookie is looked up in Redis, which returns the cached EntraUser β€” no JWT re-validation occurs. Redis TTL auto-expires the cached user when the JWT would have expired.


CSRF Protection​

CONDOR uses the Signed Double-Submit Cookie pattern:

CSRF Token = random_value | HMAC-SHA256(SESSION_CSRF_SECRET, session_id:random_value)

When enforced: POST, PUT, DELETE, PATCH requests using cookie-based auth only.
Not enforced: Bearer token requests (API keys, direct Entra JWT) β€” not vulnerable to CSRF.


Session Lifecycle & Redis Storage​

Redis Key Layout​

For a session with ID abc123:

KeyValueTTL
session:abc123Interaction count (int)SESSION_LIFETIME_SECONDS or JWT exp
session:abc123:entra_userSerialized EntraUser JSONJWT exp
session:abc123:entra_tokenRaw JWT stringJWT exp

Expiry Behavior​

Session TypeTTL SourceRefreshed on Activity?
Username/PasswordSESSION_LIFETIME_SECONDS (default: 7 days)Yes β€” each interaction refreshes TTL
Entra SSOJWT exp claim (typically 60-90 min)No β€” fixed lifetime, expires with JWT
API KeyN/A (stateless)N/A

Token Bootstrap Flow (Entra SSO)​

Detailed step-by-step of what happens when a user arrives at /?token=JWT:


JWT Validation Pipeline​

What EntraAuthService.validate_token() checks, in order:

Security: Fail-closed on missing ENTRA_ALLOWED_CLIENT_IDS
If ENTRA_ALLOWED_CLIENT_IDS is empty or not set, all tokens are rejected with a 403 error. This prevents misconfiguration from accidentally allowing any client app's token.


Error Handling​

When token bootstrap fails, users are redirected to /?error=<code> which displays a human-readable message:

Error CodeMessageCause
not_enabledSingle sign-on is not configured on this server.ENTRA_ENABLED=false or missing tenant/client config
expiredYour session has expired. Please sign in again from the application.JWT exp is in the past
invalid_audienceThis token was not issued for this application.JWT aud β‰  ENTRA_CLIENT_ID
invalid_issuerThis token was issued by an untrusted authority.JWT iss β‰  expected tenant issuer
invalid_signatureThe token signature could not be verified.JWKS key mismatch or tampered token
invalid_tokenThe authentication token is invalid.Malformed JWT or non-JWT string
unauthorized_appAccess is only allowed from an authorized application.JWT azp βˆ‰ ENTRA_ALLOWED_CLIENT_IDS
missing_roleYour account does not have the required 'Condor.Write' role. Contact your administrator.roles claim missing Condor.Write

The error message is injected into the login page's existing #login-error element, and the ?error= param is cleaned from the URL via history.replaceState.


API Endpoints Reference​

EndpointMethodAuth RequiredDescription
/GETOptionalServes login page (public) or SPA (authenticated)
/?token=<JWT>GETJWT in URLEntra SSO token bootstrap
/api/loginPOSTNone (form data)Username/password authentication
/api/logoutPOSTSession cookieClear session and cookies
/api/meGETAny authReturns current user info
/api/cameraGETrequire_sessionList cameras
/api/camera/eventGETrequire_sessionList camera events
/api/event/{id}GETrequire_sessionGet event details
/api/event/{id}/annotatePOSTrequire_session + CSRFAnnotate an event

/api/me Response Shapes​

Entra-authenticated user:

{
"auth_method": "entra",
"oid": "1bd08d5e-dd0f-4cda-b2be-36f1cf1e91f2",
"name": "Sharath Gaddameedi",
"email": "sharathgoud@live.com",
"roles": ["Condor.Write", "Camera.Operate"]
}

Session-authenticated user:

{
"auth_method": "session",
"interaction_count": 42
}

Configuration Reference​

Entra ID Settings (ENTRA_ prefix)​

VariableRequiredDefaultDescription
ENTRA_ENABLEDYesfalseEnable Entra ID authentication
ENTRA_TENANT_IDIf enabled""Azure AD tenant ID
ENTRA_CLIENT_IDIf enabled""CONDOR's App Registration client ID (JWT aud)
ENTRA_ALLOWED_CLIENT_IDSIf enabled""Comma-separated client IDs allowed to call CONDOR (JWT azp). Empty = reject all (fail-closed)
ENTRA_CIAMNofalseUse CIAM tenant ({tenant}.ciamlogin.com instead of login.microsoftonline.com)

Session Settings (SESSION_ prefix)​

VariableRequiredDescription
SESSION_USERNAMEYesLogin username for password auth
SESSION_PASSWORDYesLogin password for password auth
SESSION_API_KEYSYesJSON map of API keys to usernames: {"key": "username"}
SESSION_CSRF_SECRETYesHMAC secret for CSRF token signing
SESSION_LIFETIME_SECONDSYesSession TTL in seconds (default: 604800 = 7 days)

Infrastructure & Secrets Management​

Secrets Flow​

Key Vault Secrets (ai4gl-condor-uat-kv)​

These secrets are written at deploy time for backup, audit, and rotation:

Secret NameSource Parameter
session-usernameSERVER_SESSION_USERNAME
session-passwordSERVER_SESSION_PASSWORD
session-api-keysSERVER_SESSION_API_KEYS
session-csrf-secretSERVER_SESSION_CSRF_SECRET
entra-allowed-client-idsSERVER_ENTRA_ALLOWED_CLIENT_IDS
postgres-passwordPOSTGRES_PASSWORD
redis-passwordREDIS_PASSWORD
vm-usernamevmUsername
vm-passwordvmPassword

VM Bootstrap: write_env_file​

The vm-post-setup.sh script strips the SERVER_ prefix when writing to server/.env:

SERVER_ENTRA_ENABLED=true    β†’    ENTRA_ENABLED=true
SERVER_ENTRA_TENANT_ID=... β†’ ENTRA_TENANT_ID=...
SERVER_SESSION_USERNAME=... β†’ SESSION_USERNAME=...

Security Design Decisions​

Why store the JWT server-side instead of in the browser?​

ApproachRiskCONDOR's Choice
JWT in sessionStorageXSS can steal it; lost on tab close; visible in DevToolsβœ— Removed
JWT in localStorageXSS can steal it; persists indefinitelyβœ— Never used
JWT in httpOnly cookieCookie too large (>4KB); sent on every requestβœ— Not used
JWT in Redis, session cookie as handleCookie is opaque, small, httpOnly; JWT never in browserβœ“ Current design

Why validate the JWT only once (at bootstrap)?​

  • Performance: JWKS fetch + RSA signature verification on every request adds ~50-200ms latency
  • Reliability: JWKS endpoint downtime would break all requests
  • Correctness: Redis TTL mirrors JWT exp β€” the cached EntraUser expires at exactly the same time the JWT would
  • Trade-off: A revoked role takes effect only after the JWT (and thus the cached session) expires (typically 60-90 min)

Why fail-closed on empty ENTRA_ALLOWED_CLIENT_IDS?​

If ENTRA_ALLOWED_CLIENT_IDS is not configured:

  • Old behavior: The azp check was skipped, meaning any Entra app's token was accepted
  • New behavior: All tokens are rejected with 403 Server misconfiguration
  • Rationale: Defense-in-depth β€” misconfiguration should deny access, not grant it

Why redirect instead of inline serving after bootstrap?​

After successful /?token=JWT validation:

  • Old approach: Serve the private SPA HTML directly with cookies set on the same response
  • Problem: Browsers handle Set-Cookie on HTML responses inconsistently; the JWT token remained visible in the URL bar
  • New approach: 302 Redirect to / with cookies on the redirect response β†’ browser stores cookies, follows redirect, sends cookies on the second request β†’ clean URL, reliable cookie storage

File Reference​

FilePurpose
server/services/entra_auth_service.pyJWT validation, JWKS client, RBAC dependency factories
server/services/session_service.pyAuth resolution (get_session, require_session), login/logout, CSRF
server/routers/files_router.pyToken bootstrap (/?token=), error display, SPA serving
server/routers/session_router.py/api/login, /api/logout, /api/me endpoints
server/database/redis_session_database.pyRedis session storage, Entra user caching
server/models/entra_user.pyEntraUser Pydantic model with role helpers
server/settings/entra_settings.pyEntraSettings β€” tenant, client, CIAM, JWKS URI
server/settings/session_settings.pySessionSettings β€” username, password, API keys, CSRF secret
server/tests/test_entra_auth.pyUnit tests for JWT validation, RBAC, token extraction
azure/infrastructure/main.bicepInfrastructure β€” passes Entra params to modules
azure/infrastructure/modules/keyvault.bicepStores secrets in Key Vault at deploy time
azure/infrastructure/scripts/vm-post-setup.shWrites server/.env from deploy params