Guarding the Doors
Not every caller is a signed-in human. A background function invoking an endpoint, a browser starting an OAuth login, camera data sitting at rest — each is a different boundary with a different notion of "prove you're allowed." The Infrastructure package ships three security services, one per boundary, and the useful mental model is which door each one guards.
API keys — the door for machines
ApiKeyValidator is for service-to-service calls that have no user and no OAuth token — an
Azure Function trigger, say. It checks an x-api-key header against the configured
FunctionApiKey, with overloads for both the Functions isolated-worker HttpRequestData and
ASP.NET Core HttpRequest, plus CreateUnauthorizedResponse for the Functions 401.
A valid API key proves the caller knows the key — nothing about who they are. It's appropriate for trusted internal callers and wrong for anything user-facing, where you want the OAuth path below. Rotating the key is a config change that every holder must pick up.
OAuth tokens — the door for people
JWTService runs the OAuth2/OIDC flows against Azure AD B2C CIAM
(alertcalifornia.ciamlogin.com) for both the frontend and admin apps. It builds authorize
URLs, exchanges authorization codes for tokens, and — the non-obvious part — authenticates
the app itself with a certificate-based client assertion rather than a client secret:
PEM certificates are loaded from configuration, and each token request carries an RSA-signed
JWT as its client_assertion. It's registered as a singleton (and pulls in an
HttpClient), so the certificate is loaded once and reused.
Every method comes in a frontend and an admin flavor (GetFrontendTokensAsync /
GetAdminTokensAsync, GetFrontend… / GetAdmin…), each with its own client id, redirect
URI, and certificate thumbprint. Using the admin assertion for a frontend exchange fails at
the token endpoint, not at compile time. Match the flavor to the app end to end.
Encryption — the door for data at rest
EncryptionService protects sensitive camera data with AES-CBC symmetric encryption,
where the symmetric key itself is guarded by Azure Key Vault. At startup it reads the RSA
key from Key Vault and uses it to unwrap (RSA-OAEP) the stored symmetric key, holding the
decrypted AES key in memory for the singleton's lifetime.
Each encryption generates a fresh 16-byte IV and prepends it to the ciphertext, so every value is self-describing — decryption just reads the IV back off the front:
Because a fresh IV is generated every time, encrypting the same plaintext twice yields two different Base64 strings. That's correct cryptographically — but it means you can never match encrypted values by comparing ciphertext, and you can't index or dedupe on an encrypted column. Decrypt to compare, or store a separate non-reversible key if you need equality.
Wrapping up
Three services, three doors: API keys authenticate trusted machines (a shared secret, not an
identity), JWTService runs the certificate-backed CIAM OAuth flows for users (frontend and
admin kept strictly separate), and EncryptionService encrypts data at rest with AES-CBC
under a Key-Vault-guarded key (with a random per-call IV that makes ciphertext incomparable).
Register each one explicitly.
Next, the tool that lets you exercise every repository and service by hand: The Diagnostic Cockpit.