Solution Overview
| Aspect | Technology |
|---|---|
| Frontend | React 18 + TypeScript + Vite, MSAL (@azure/msal-browser, @azure/msal-react). Built to static assets and served by the backend from wwwroot. |
| Backend | .NET 10 ASP.NET Core Minimal API, plus a domain library and an xUnit test suite. A hand-rolled Graph HTTP client is used — no Graph SDK. |
| Authentication (users) | Microsoft Entra ID, multi-tenant OIDC + PKCE via MSAL; authority …/organizations. The API validates JWTs and accepts a token only if its tenant is the home tenant or an onboarded customer tenant. |
| Authentication (Graph) | App-only, per-tenant, certificate-based credentials. The certificate is sourced from Azure Key Vault via Managed Identity (Azure hosting) or a local PFX file (self-hosted). |
| Persistence | A single SQLite metadata file holds only RBAC assignments, the tenant registry, and audit logs — never customer device or user data. |
| RBAC | Roles are code-defined; assignments persist in SQLite. Enforcement is deny-by-default and scoped per tenant, checked on every API call. |
| Tenant management | A SQLite tenant registry. Onboarding a tenant = admin-consenting the app-only "Data" application in that tenant + registering its tenant ID. |
The two-application model
CET2 is built on two separate Entra app registrations, each with a distinct purpose:
- Portal application — what users sign in to. Multi-tenant, delegated permissions only
(
openid,profile,User.Read), exposes a custom API scope (access_as_user) that the SPA requests to call CET2's own backend. - Data application — what CET2 uses to read/write Microsoft Graph on a customer's behalf.
App-only (client credentials), certificate-authenticated, holds the actual
DeviceManagement*/User.ReadWrite.All/ etc. Graph application permissions. Consented separately, per customer tenant.
This separation means a compromised user session can never escalate into Graph-level access — the Portal app has no Graph permissions of its own, and the Data app never authenticates an end user.
Request flow
Write actions — the Action Broker
Every write (device sync/retire/wipe, user disable/enable, revoke sessions, reset access) passes through a single choke point that enforces, in order:
- RBAC + tenant scope — the caller must hold the specific permission for that tenant.
- Reason capture — a business justification is mandatory and recorded.
- Confirmation — the UI must have explicitly confirmed; destructive actions require typing the target's name.
- Step-up authentication — Elevated actions require a sign-in within 15 minutes; Destructive actions within 5 minutes. A stale session triggers a fresh interactive sign-in.
- Rate limiting — per-user, per-tenant, per-action-class limits.
- Execution — the live Graph write.
- Audit — every phase (Requested → Started → Succeeded/Failed) is written to a durable audit trail, visible as Action history on the target's detail page.
See RBAC for how permissions and roles work, and the Deployment Guide for how to set all of this up for a new customer.