Authentication
The operator plane -- who is allowed to use the bridge, and against which tenants -- is independent of the Microsoft plane (the SAM app that gives the bridge access to customer tenants; see Architecture). This page covers the operator plane's three modes, and the self-service account model that ships with the local one.
Three modes
Set with Auth:Mode. Nothing else in the app changes based on the mode --
controllers use the same [Authorize] attribute and the same
User.Identity.Name regardless of which one issued the token.
| Mode | Who can sign in | When to use it |
|---|---|---|
| Oidc (default) | Anyone your OIDC provider (Authentik, or any standard OIDC IdP) authenticates. Every signed-in user has full, unrestricted access to every tenant -- this mode predates the tenant-sharing model below and keeps that original all-access behavior. | You already run an IdP and want every technician to go through it. |
| Local | Anyone who registers an account directly in the app. No external IdP required. See below for the full account/sharing/2FA/passkey model. | Solo technicians, small teams, or anyone self-hosting without wanting to stand up Authentik first. This is the friction-free default for adoption. |
Dev (Auth:Enabled=false) |
Everyone, as a fixed dev-operator identity. No credentials at all. |
docker-compose evaluation only. Never on a deployed instance -- see the warning on Getting started. |
Local accounts: open signup, scoped power
Registration (POST /api/auth/register) is deliberately open: no invite code, no
admin approval queue. The safety story isn't the signup gate, it's that a fresh account starts
with zero tenant access. It can log in and see the app shell, but can't run a
single workflow or deployment against a customer tenant until someone shares that tenant with
it.
Delegating instance administration
Instance roles govern shared bridge configuration, not customer data. An Administrator can use the Security screen's Instance access card to combine the narrow delegated roles below. Role changes take effect on the next request even for an already-issued login token or MCP PAT; roles are resolved from the database rather than trusted from stale token claims.
| Instance role | Can do |
|---|---|
| Administrator | Delegate instance roles and onboard/sync tenants, plus every delegated permission below. |
| Catalog manager | Author app templates and packages, contracts and desired apps, and provisioning defaults. |
| Credential manager | View and rotate the shared Secure Application Model credential. |
| Automation policy manager | Change a tenant's MCP approval policy between Queue and ClientTrust without gaining access to that tenant's data. |
Owning a tenant
An Administrator becomes a tenant's Owner automatically when bringing it into the system, two ways:
- Add it directly --
POST /api/tenants(the "Add a tenant" form on the Tenants screen) if you already have the GDAP relationship and just want to start automating against it. - Sync from Partner Center -- running an Administrator-only sync grants you Owner on any newly discovered tenant. A tenant the system already knew about just gets its name and domain refreshed; ownership is untouched.
Sharing (the collab model)
An Owner can grant another registered user access to that tenant, self-service, from the Tenants screen -- no central admin has to be the bottleneck for every hand-off:
| Role | Can do |
|---|---|
| Viewer | Read dashboard, history, and diagnosis output for that tenant. Cannot remediate or deploy. |
| Operator | Everything a Viewer can, plus run known-fix workflows and Win32 deployments. |
| Owner | Everything an Operator can, plus grant or revoke other users' access to that tenant, and reassign its contract. |
Grants can carry an optional expiry -- useful for a contractor or a temporary engagement.
Two-factor authentication (TOTP)
Any local account can enable RFC 6238 TOTP from the Security screen. Enrollment is a
two-step confirm: the app shows a secret (and an otpauth:// URI you can paste into
most authenticator apps -- there's no QR code rendered client-side, deliberately: a live TOTP
secret isn't something to hand to a third-party QR image service), and enabling only completes
once you prove you captured it by entering a live code. Ten single-use recovery codes are shown
exactly once at that point.
Once enabled, a password login returns an MFA challenge instead of a token; the account isn't considered logged in until the second factor clears. A wrong code doesn't burn the challenge -- it's retryable (bounded) within its short server-side lifetime, so a typo doesn't force you back through your password. A successful passkey login skips this step entirely: WebAuthn possession-plus-verification is already MFA-equivalent.
Passkeys (primary sign-in)
Passkeys are the primary way to sign in, not a bolt-on: registered as discoverable (resident) credentials, so the login screen's "Sign in with a passkey" button needs no email typed first -- the browser/authenticator presents whichever passkeys it holds for the site and the user picks one. Password stays the permanent fallback credential; it's never removable from an account, so a lost device or a browser without WebAuthn support can't lock anyone out.
Passkey ceremonies (/api/auth/passkey/*) are standard WebAuthn/FIDO2 via
Fido2NetLib
-- no proprietary protocol, works with platform authenticators (Windows Hello, Touch ID) and
roaming ones (security keys) alike.
MCP personal access tokens
A fourth credential type, layered on top of Local mode rather than a fourth Auth:Mode:
a personal access token (PAT) is a long-lived, individually revocable JWT for
machine callers -- specifically, an MCP client that can't do an interactive browser login. Mint
one from the Security screen's "MCP access tokens" panel; it carries your identity while current
roles, tenant grants, account status, and token revocation are checked from the database on every
request. Revoke it immediately if the client using it is decommissioned or compromised.
/mcp) only -- dedicated middleware rejects
it everywhere else with 403, so a leaked PAT cannot be replayed against the REST API. See
MCP Server for the full model, including the Queue/ClientTrust
approval modes that gate what an MCP client is actually allowed to change.
Audit trail
Every security-relevant event is recorded to an append-only AuditEvents table:
registration, login success/failure, logout, TOTP enable/disable, a recovery code being spent,
passkeys added/removed, and every tenant-access grant or revoke -- who, when, and (for access
changes) what role. Mutations to sensitive entities (accounts, tenants, contracts, templates,
deployments, tenant grants) are captured automatically by an EF Core save-changes interceptor,
so logging isn't something a future controller can forget to do. This is in addition to, not
instead of, the workflow-run history described in Architecture.
Configuration reference
| Key | Applies to | Notes |
|---|---|---|
Auth:Mode | All | Oidc (default) / Local / Dev. Auth:Enabled=true|false is still read as a fallback for config predating this key. |
Auth:Authority, Auth:Audience, Auth:NameClaim | Oidc | Standard OIDC discovery + JWT bearer validation settings. |
Auth:Local:SigningKey | Local | Secret. Base64, 32+ bytes (openssl rand -base64 32). Signs locally issued JWTs; required to start in Local mode. Rotating it invalidates every outstanding token. |
Auth:Local:MinPasswordLength, MaxFailedLogins, LockoutMinutes, AccessTokenLifetimeHours | Local | Defaults: 12 chars, 10 attempts, 15-minute lockout, 12-hour tokens. |
Auth:Local:Passkey:RelyingPartyId | Local | Bare domain the SPA is served from (no scheme/port), e.g. pcbridge.example.com. |
Auth:Local:Passkey:RelyingPartyName | Local | Display name shown by the OS passkey prompt. |
Auth:Local:Passkey:Origins | Local | Exact origin(s) (scheme+host+port) the browser sends, e.g. https://pcbridge.example.com. A mismatch fails every ceremony. |