Skip to content

Authentication

Consystence does not run a single login mechanism. A request arriving at a Consystence server is authenticated by whichever of several schemes fits its credentials, chosen per request by a policy-scheme selector named MultiAuth. This page describes that model, the three schemes it routes to, and — just as importantly — the flows that are deliberately not implemented here because they belong to the external identity provider.

If you are integrating against the platform, the practical summary is: browsers and interactive humans send a CIAM Bearer token; machines send an X-Api-Key; air-gapped site terminals use a locally-issued site-PIN token. Everything below explains how the server tells them apart and what each one proves.

MultiAuth is registered as the default authentication scheme. It is a policy scheme — it holds no validation logic of its own. On each request it inspects the presented credentials and forwards to the concrete scheme that should validate them. This lets every endpoint use a bare [Authorize] / RequireAuthorization() without enumerating schemes.

flowchart TD
    R[Incoming request] --> Q{X-Api-Key header?}
    Q -- yes --> AK[ApiKey scheme]
    Q -- no --> B{Authorization: Bearer?}
    B -- "no" --> NR[Bearer -> NoResult -> 401]
    B -- "yes" --> ISS{iss = consystence-local?}
    ISS -- yes --> L[LocallyIssuedJwt scheme]
    ISS -- no --> E[Entra CIAM Bearer scheme]

The selection rules are exact:

Headers presentRouted toNotes
X-Api-Key onlyApiKeyTenant or control-plane key
Authorization: Bearer … onlyBearer (Entra CIAM) or LocallyIssuedJwtRouted by the token’s iss claim
Both X-Api-Key and AuthorizationApiKeyPrefer the API key, emit a warning log — never reject with 400 (HTTP clients commonly set both)
NeitherBearerReturns NoResult, the policy then fails closed to 401

When a Bearer token is present, the selector reads the token’s iss claim without validating it purely to decide which scheme should validate it: tokens whose issuer is consystence-local go to the site-PIN scheme, everything else to the Entra CIAM scheme. The chosen scheme then performs full cryptographic validation (signature, issuer, audience, lifetime), so a forged issuer is rejected downstream — the unverified read only routes.

One transport exception to the header-based rules above: a SignalR WebSocket handshake cannot carry an Authorization header, so on /hubs paths the selector also reads the bearer token from the ?access_token= query string and applies the same iss-based routing to it — this is how an offline site-PIN token reaches a scheme-agnostic hub. A query-string token on a non-/hubs path is ignored.

Across every scheme, the stable identity and membership key for a human is the Entra object id (oid) — the tenant-immutable object id that resolves one person to one identity at the account and at every cell. Email is a verified attribute and an invitation channel, never the membership key. A @gmail.com account can hold different roles in two different organisations simultaneously, keyed on the same oid.

Roles are not asserted by the identity provider. An Entra token carries the oid but no Consystence role; the server recomputes the canonical org_slug (tenant) and org_role (role within the tenant) from the oid and the request subdomain at authentication time. See Roles & membership for the role model (owner / admin / member).

The canonical claims after authentication are:

ClaimCIAM BearerAPI keySite-PIN
subjectoid / sub (mapped to NameIdentifier)the key’s key_idthe site user id
org_slugrecomputed from oid + subdomainthe key’s tenant_idabsent (site-PIN proves site presence, not org membership)
org_rolerecomputed from membershipthe key’s roleabsent
site_rolethe user’s site role, stamped into the local token
key_id / key_prefixemitted for audit correlation

Scheme 1 — CIAM Bearer (Entra External ID)

Section titled “Scheme 1 — CIAM Bearer (Entra External ID)”

The primary scheme for browsers and interactive users. Identity is provided by Entra External ID (CIAM) — the id.consystence.com identity provider. The browser obtains a token through an OpenID Connect authorization-code flow with PKCE, which the server drives against the provider’s published discovery document:

  • GET /auth/login — builds the authorize URL (resolved from the provider’s /.well-known/openid-configuration) and redirects the browser to the IdP.
  • GET /auth/callback — validates state, exchanges the code (with the PKCE verifier) for tokens, and establishes a server-side session.
  • POST /auth/token — returns the access token for the SignalR connection (POST so it never lands in logs or history).
  • POST /auth/logout — clears the session.

The CLI obtains the same CIAM Bearer token through its own PKCE flow with a localhost loopback redirect — see the CLI client.

Tokens are validated against the configured authority with clock-skew tightened to one minute. Entra External ID may issue either the v2.0 issuer or the legacy v1.0 issuer depending on endpoint and token type, so both forms are accepted on issuer validation. Each cell validates CIAM tokens directly — token validation is local to the server handling the request and does not call back to the account control plane on the request path.

Scheme 2 — X-Api-Key tenant keys (csk_…)

Section titled “Scheme 2 — X-Api-Key tenant keys (csk_…)”

Programmatic and partner integrations authenticate with a tenant API key sent in the X-Api-Key header. Keys are:

  • Prefixed csk_ — a high-entropy random value generated by a CSPRNG (the exact length and encoding are an implementation detail).
  • Stored hashed, never in plaintext. Only the hash and a short display prefix (csk_ + the first 4 characters) are persisted.
  • Tenant-scoped — the principal’s org_slug and org_role come from the key record, so a key cannot act outside the tenant that minted it.

On validation, the handler hashes the presented key and looks it up by its unique hash index (constant-time-equivalent — no string comparison). Any miss returns a uniform 401.

Tenant keys are managed under /api/admin/api-keys (an admin operation — see Roles & membership):

MethodPathBehaviour
POST/api/admin/api-keysMint a key. Plaintext is returned exactly once in the 201 body — capture it then; no later call re-emits it.
GET/api/admin/api-keysList keys for the caller’s tenant — prefix and metadata only, never plaintext or hash.
DELETE/api/admin/api-keys/{keyId}Revoke a key. Cross-tenant or unknown ids both return a uniform 404.

Keys carry a required expiry: expiresAt must be supplied when minting — there is no server-side default (one year is only a UI convention) — and the service layer validates it to lie between one hour and two years from now, rejecting anything outside that window with a 400. An out-of-band reminder service emails owners ahead of expiry. The tenant_id is always derived from the authenticated caller, never from the request body — a caller cannot widen scope by editing the payload.

Scheme 3 — LocallyIssuedJwt site-PIN (offline-capable)

Section titled “Scheme 3 — LocallyIssuedJwt site-PIN (offline-capable)”

Industrial sites — mining and CHPP plants — must keep operating when the upstream link is down. The LocallyIssuedJwt scheme exists for exactly this: a site-resident, offline-capable sign-in that does not depend on reaching Entra.

A site user signs in with email plus a numeric site PIN (POST /auth/site/login). The PIN is hashed with the ASP.NET Core password hasher and gated by lockout after repeated failures. On success the server issues a short-lived (one-hour) access JWT signed by a local key, with issuer consystence-local and audience consystence-api, validated against the server’s local JWKS endpoint — plus a seven-day rotating refresh token. The local issuer is what the MultiAuth selector routes on.

The scheme carries its own full token lifecycle, none of it depending on cloud reachability:

  • POST /auth/site/refresh — exchanges the refresh token for a new access + refresh pair; the old refresh token is revoked on rotation. Presenting an already-revoked refresh token is treated as theft and revokes all of that user’s site sessions.
  • POST /auth/site/logout — revokes the presented refresh token.
  • POST /auth/site/verify-email / POST /auth/site/verify-otp — first-login email verification: a six-digit OTP is emailed to the address and, once confirmed, marks the site user’s email verified.
  • POST /auth/site/create-pin — sets or changes the caller’s PIN (minimum six digits; requires an authenticated session, OIDC or local JWT).
  • GET /auth/site/me — returns the current user as asserted by the token.

A site-PIN principal proves site presence, not org membership — it deliberately carries no oid and no org_role; its authority comes from the site role stamped into the token (see Roles & membership). It cannot stand in for an organisation-level identity; it authorises the site-local surface only.

Site users are administered under /api/site/users. On the cloud the surface is gated by the OrgAdmin policy (see Authorisation, in brief); on a site server it is a site function — a site-PIN principal whose site role carries admin authority can manage site users, API keys, and configuration, fully offline:

MethodPathBehaviour
GET/api/site/usersList site users with role, verification, lock, and enabled status.
POST/api/site/users/provisionPre-authorise a user before they arrive on-site: email + display name + site role, optionally with a temporary PIN (minimum six digits). A user provisioned with a PIN can sign in immediately — including during a cloud outage.
PUT/api/site/users/{userId}/roleUpdate the user’s site role.
POST/api/site/users/{userId}/unlockClear a PIN lockout (resets failed attempts).
DELETE/api/site/users/{userId}Disable the user (soft delete — revokes site access).

Because the CIAM identity is Entra’s responsibility, the platform does not implement — and does not expose — the usual hand-rolled account-management flows for that identity:

  • No email/password login for the cloud identity, and no /register. Account creation and credentials live in Entra External ID.
  • No /refresh, MFA, password reset, phone verification, or “switch organisation” flows for the CIAM identity. Token lifecycle, second factors, and credential recovery are the IdP’s job.
  • No OIDC provider surface — Consystence consumes OIDC discovery, it does not serve it.

platform_admin is a narrow, audited elevation — not the licensing authority (licensing is an account/cell concern; see Licensing). It is granted by a claims transformer when a principal presents a verified email whose domain is on the configured platform-admin domain list (in practice @consystence.com), narrowed at the account by an approved seed list rather than a domain wildcard. The grant honours the identity provider’s verification assertion: a token that explicitly asserts an unverified email never auto-grants, regardless of domain. (Entra External ID verifies local-account emails at signup and does not emit the assertion on every flow, so a token carrying no assertion is accepted as verified.)

The grant is treated as an explicit, logged bypass — it satisfies the platform-admin policy and the route-agnostic org-role checks where a policy includes the override. Org-scoped admin policies (admin of the specific org in the route) intentionally have no platform-admin override, so platform admins cannot silently act inside an arbitrary tenant through those doors.

Authentication establishes who; named authorisation policies decide what. The policies compose the canonical role claims — for example, OrgAdmin is satisfied by an org_role of owner/admin, and OrgMember by any of owner/admin/member. Both are route-agnostic claim checks, and both include the platform-admin override: a platform_admin claim alone satisfies them. The policy without that override is OrgScopedAdmin, which binds the caller’s owner/admin authority to the specific org in the route — the org-scoped policy the Platform administration section above refers to. The full role model and its rationale live in Roles & membership.

  • Roles & membership — the owner / admin / member model and named policies
  • Organisation & site model — tenants, slugs, sites, and how org_slug is derived
  • Cell topology — where token validation and the account control plane sit
  • Licensing — the separate account-issued, cell-mediated licensing authority
  • API reference — sending Authorization and X-Api-Key to the cell-direct, CLI-scoped /api surface
  • Core concepts — accounts, organisations, identity