Docs Guides
Authentication
For connecting Microsoft admin accounts to several tenants in the Local Workbench, see Add tenants with Microsoft sign-in. Those encrypted, per-operator Graph connections are separate from the workbench login described below.
The operator plane -- who is allowed to use the bridge, and against which tenants -- is independent of the Microsoft plane (direct tenant admin connections or the optional partner SAM connection; 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. Controllers share the same authentication plumbing, but
access rules differ by mode: Local accounts use explicit tenant grants, while OIDC retains
the all-access operator model described below.
| Mode | Who can sign in | When to use it |
|---|---|---|
| Oidc (server 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. |
Auth:Mode=Local (it refuses to start with
Dev) and generates its own Auth:Local:SigningKey on first launch,
stored encrypted with the same Data Protection key ring as the SAM token. The first account
registered becomes the instance Administrator. Everything below about Local accounts, sharing,
TOTP and passkeys applies exactly the same way in that mode. On a machine that is already
secured you can instead choose to use it without an account, through single-use launch links
minted by the running exe. Each expires after two minutes; run the exe again for a new one --
self-registration, password login, passkeys and TOTP are all blocked for that account until then,
though MCP access tokens still work. That option is Local Workbench only, gets instance
Administrator and no tenant access until it adds a tenant, and first run refuses to offer it with
a non-loopback --listen address. See
Using it without an account.
Local accounts: open signup, scoped power
After setup, registration (POST /api/auth/register) is deliberately open: no invite
code, no admin approval queue. On a Local Workbench's first run, the exe's single-use setup
ticket is required before creating the first account. 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.
On the Local Workbench, the relying-party id is fixed to
localhost and the one allowed origin is http://localhost:<port>
-- WebAuthn ties a passkey to the RP id it was registered under, so this only works because
every request is normalized onto that single canonical origin first (a request addressed to
127.0.0.1 or [::1] is redirected there before auth runs at all; see
the canonical origin). A server
deployment instead sets Auth:Local:Passkey:RelyingPartyId to its real domain and
Origins to its HTTPS origin, per the configuration reference below.
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. |