Skip to main content

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.

Gotcha: this is a shared secret, not an identity

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.

Gotcha: two apps, two certificates — don't cross the wires

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:

Gotcha: a random IV per call means ciphertext isn't comparable

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.