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.

Login screen with a primary Sign in with a passkey button above a password fallback form and a Register link.

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.

ModeWho can sign inWhen 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.

There is no instance role that bypasses tenant access. The first account becomes Administrator, but every tenant permission for every account -- including an Administrator -- still comes from an explicit, visible Viewer/Operator/Owner grant.

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 roleCan do
AdministratorDelegate instance roles and onboard/sync tenants, plus every delegated permission below.
Catalog managerAuthor app templates and packages, contracts and desired apps, and provisioning defaults.
Credential managerView and rotate the shared Secure Application Model credential.
Automation policy managerChange 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:

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:

RoleCan do
ViewerRead dashboard, history, and diagnosis output for that tenant. Cannot remediate or deploy.
OperatorEverything a Viewer can, plus run known-fix workflows and Win32 deployments.
OwnerEverything 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.

Security screen listing a registered passkey, TOTP enabled, and an MCP access token named Claude Desktop.

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.

A PAT is confined to the MCP transport (/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

KeyApplies toNotes
Auth:ModeAllOidc (default) / Local / Dev. Auth:Enabled=true|false is still read as a fallback for config predating this key.
Auth:Authority, Auth:Audience, Auth:NameClaimOidcStandard OIDC discovery + JWT bearer validation settings.
Auth:Local:SigningKeyLocalSecret. 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, AccessTokenLifetimeHoursLocalDefaults: 12 chars, 10 attempts, 15-minute lockout, 12-hour tokens.
Auth:Local:Passkey:RelyingPartyIdLocalBare domain the SPA is served from (no scheme/port), e.g. pcbridge.example.com.
Auth:Local:Passkey:RelyingPartyNameLocalDisplay name shown by the OS passkey prompt.
Auth:Local:Passkey:OriginsLocalExact origin(s) (scheme+host+port) the browser sends, e.g. https://pcbridge.example.com. A mismatch fails every ceremony.