Docs menu

Auth

dotwire issues nothing and stores no credentials. Your host application holds an RS256 private key and mints JWTs. dotwire holds only the matching public keys and verifies signatures, so it can never mint a token itself.

The token's sub is your user id. dotwire treats it as an opaque string and uses it to look up roles and room memberships in its own tables. It must be stable and unique per user. User ids are 1 to 256 characters with no control characters.

Token contract

Contract (AUTH.md, "Token contract (normative)")

Every JWT presented to dotwire must satisfy the following. This table is the contract host integrators build against; nothing beyond it is required, and nothing on it is optional except where marked.

ClaimRequiredMeaning
issyesHost identifier; must match dotwire's configured issuer.
subyesThe host's user id — an opaque string, never interpreted by dotwire beyond identity/lookup.
audyesMust match dotwire's configured audience.
exp / iatyesStandard expiry/issued-at claims. Short TTLs are recommended (guidance: 5–60 minutes) — see "Threat notes" for why.
dw:roleyesNamespaced custom claim; one of member | auditor | admin. Cross-checked against the service-side user_roles table (see "Authorization pipeline") — the JWT's claim is treated as a hint, not a grant.
dw:namenoOptional display name.

Contract (AUTH.md, "Token contract (normative)")

Header: kid (key id) is required in every token's header — it is how dotwire selects which public key to verify against, whether the key came from a JWKS endpoint or inline config.

Signing: RS256 only. dotwire's JwtBearer configuration does not accept HS256 or any symmetric algorithm — see "Threat notes" for why that restriction is load-bearing rather than incidental.

A token with no sub, or with a dw:role other than the three role names, is rejected at authentication.

Send the token as Authorization: Bearer <token>. The hub (/hub/rooms) and the SSE endpoint (/rooms/{roomId}/events) also accept it as an access_token query parameter, because browser WebSocket and EventSource APIs can't set headers.

Minting tokens

Both host SDKs mint tokens that satisfy the contract.

Node.js (@dotwire/host)

const token = await host.mintToken('alice', {
  role: 'member',
  displayName: 'Alice Smith',
  ttl: '1h'
});

.NET (Dotwire.Host)

string token = client.MintToken("user-42", role: "member");

You can also mint tokens with any JWT library, as long as the result follows the contract above. AUTH.md in the dotwire repository has host-side examples for System.IdentityModel.Tokens.Jwt and jsonwebtoken.

Verification keys

Configure at least one key source. dotwire refuses to start without one, and without Auth:Issuer and Auth:Audience.

  • Inline PEM keys. Set Auth:Keys:<kid> to a PEM-encoded RSA public key, for example Auth__Keys__dev as an environment variable. Use this for simple or airgapped deployments.
  • JWKS URL. Set Auth:JwksUrl to a standard JSON Web Key Set endpoint. dotwire caches it, refreshes it every Auth:JwksRefreshMinutes (5 by default), and also refreshes, rate-limited, when it sees an unknown kid. If a refresh fails, it keeps the previous keys.

Rotating keys

  • JWKS: publish the new key under a new kid, and keep the old key published until every token signed with it has expired. Allow for the refresh interval as well as the token TTL.
  • Inline: add the new key under a new kid, keep the old entry until its tokens expire, and restart dotwire. There is no live polling.

Roles

There are three roles, stored in dotwire's user_roles table. They are independent, not a hierarchy.

  • member is the role for ordinary participants. It grants no rooms by itself. Room access comes from room membership.
  • auditor grants read access to the audit log and nothing else.
  • admin manages the service: roles, room membership, message injection, redaction, retention, and DSAR export. It grants no room access and no audit access.

An admin who needs to read a room must also be a room member. An admin who needs the audit log needs the auditor role, which means a separate user id, because each user has one role.

The migrations seed two identities so a host can use the admin and audit APIs right away: host-admin with admin, and host-auditor with auditor. Both host SDKs sign admin and audit calls as these users by default.

Authorization pipeline

Contract (AUTH.md, "Authorization pipeline")

Every authenticated request or connection goes through this pipeline, in this exact order. The order matters: each stage is only meaningful once the stage before it has passed.

  1. Signature verification. The token's kid selects a public key (from JWKS cache or inline config); the signature is verified against it. Fails closed — an unrecognized kid or bad signature is rejected before anything else is inspected.
  2. Standard claims. exp, aud, and iss are checked. An expired token, wrong audience, or wrong issuer is rejected here, before any dotwire-specific claim is consulted.
  3. Role cross-check. The token's dw:role claim is compared against the service-side user_roles table for that sub. The table is authoritative, not the claim — if they disagree, the request is denied. This is what makes a stolen or stale token claiming admin harmless if the table says otherwise: revoking or demoting a user in user_roles takes effect immediately, without waiting for outstanding tokens to expire.
  4. Room membership check. For room-scoped operations, membership is checked against the room_members table (host-managed via the admin API). This check happens at CONNECT, not per message — the result is cached on the connection and invalidated only by membership-change events, per ARCHITECTURE.md's "Concurrency and fanout disciplines." A per-message membership query would put the database on the fanout hot path; this contract is what keeps it off.

In practice:

  • A failed signature or standard-claim check returns 401.
  • A user with no row in user_roles, or a row that doesn't match the token's dw:role, gets 403.
  • /admin routes require admin in both the token and the table. /audit routes require auditor in both.
  • REST room routes check membership on every request. The hub checks it when a connection calls Subscribe.

Token lifetime

dotwire has no revocation list for individual tokens. The role cross-check handles demotions immediately, but a still-valid token remains a credential until it expires, so keep TTLs short and refresh them.

When a token expires, dotwire closes the hub connection and ends any SSE stream that used it. When you configure tokenUrl, @dotwire/client fetches a fresh token from your token endpoint 30 seconds before the cached one expires, by default.

Two key systems

JWT signing keys authenticate people. dotwire's encryption keys protect data at rest. They are unrelated, and losing each has a different consequence. See Encryption and retention.

Related