Rename all project references from quicproquo/qpq to quicprochat/qpc across documentation, Docker configuration, CI workflows, packaging scripts, operational configs, and build tooling. - Docker: crate paths, binary names, user/group, data dirs, env vars - CI: workflow crate references, binary names, artifact names - Docs: all markdown files under docs/, SDK READMEs, book.toml - Packaging: OpenWrt Makefile, init script, UCI config (file renames) - Scripts: justfile, dev-shell, screenshot, cross-compile, ai_team - Operations: Prometheus config, alert rules, Grafana dashboard - Config: .env.example (QPQ_* → QPC_*), CODEOWNERS paths - Top-level: README, CONTRIBUTING, ROADMAP, CLAUDE.md
9.2 KiB
Auth, Devices, and Tokens
This page describes the authentication, device management, and authorisation design for quicprochat. It introduces account and device identities, gates server operations by authenticated identity, enforces rate and size limits, and binds MLS identity keys to accounts.
This design cuts across milestones M4 through M6. For the broader production readiness plan, see Production Readiness WBS.
Goals
- Introduce accounts and devices with authenticated access to
NodeService. - Gate operations by identity: enqueue/fetch/fetchWait require a valid token bound to the caller's account and device.
- Enforce rate and size limits per account, per device, and per IP.
- Bind MLS identity keys to accounts: a KeyPackage upload must be associated with the uploading account, preventing impersonation.
- Keep wire changes minimal and versioned: the
Authstruct is additive and uses a version field for backward compatibility.
Data Model (Server)
Accounts
| Field | Type | Description |
|---|---|---|
account_id |
UUID | Unique account identifier |
created_at |
Timestamp | Account creation time |
status |
Enum | active, suspended, deleted |
Devices
| Field | Type | Description |
|---|---|---|
device_id |
UUID | Unique device identifier |
account_id |
UUID | Owning account (foreign key) |
device_pubkey |
Ed25519 public key (32 bytes) | Device signing key |
created_at |
Timestamp | Device registration time |
status |
Enum | active, revoked |
Sessions / Tokens
| Field | Type | Description |
|---|---|---|
session_id |
UUID | Unique session identifier |
account_id |
UUID | Owning account |
device_id |
UUID | Originating device |
access_token |
Opaque bytes | Short-lived bearer token |
refresh_token |
Opaque bytes | Long-lived token for renewal |
expires_at |
Timestamp | Access token expiry |
created_at |
Timestamp | Session creation time |
Identity Binding
| Field | Type | Description |
|---|---|---|
account_id |
UUID | Owning account |
mls_identity_key |
Ed25519 public key (32 bytes) | MLS credential public key |
verified_fp |
SHA-256 fingerprint (32 bytes) | Fingerprint of the bound key |
The identity binding table ensures that only the account that registered an Ed25519 public key can upload KeyPackages for that key. This prevents a compromised or malicious client from uploading KeyPackages under another account's identity.
Wire / API Changes
Auth Struct
A new Auth struct is added to all NodeService RPC methods:
struct Auth {
version @0 :UInt16; # 0 = legacy (no auth), 1 = token-based
accessToken @1 :Data; # opaque bearer token
deviceId @2 :Data; # optional UUID (16 bytes) for audit/rate limit
}
The Auth struct is included as a parameter in enqueue, fetch, fetchWait,
uploadKeyPackage, and fetchKeyPackage.
Versioning
| Version | Meaning |
|---|---|
| 0 | Legacy mode: no authentication. Server can allow-list in development but defaults to rejecting in production. |
| 1 | Token-based authentication. accessToken is required and validated. |
The server rejects any version value higher than its current maximum. This
ensures that a newer client connecting to an older server fails cleanly rather
than silently skipping auth.
Optional Device ID
The deviceId field is optional. When present, the server uses it for:
- Per-device rate limiting (in addition to per-account limits).
- Audit logging (which device performed which operation).
- Future: device revocation without revoking the entire account.
Server Enforcement
Token Validation
- Extract
Authstruct from the incoming RPC. - If
version == 0and server is in production mode, reject withAUTHENTICATION_REQUIRED. - If
version == 1, validateaccessToken:- Token must exist in the session store.
- Token must not be expired (
expires_at > now). - Associated account must have
status == active. - Associated device (if
deviceIdpresent) must havestatus == active.
- Map validated token to
(account_id, device_id)for downstream authorisation.
Identity Matching
- uploadKeyPackage: The
identityKeyin the RPC must match an identity binding for the authenticated account. Reject withIDENTITY_MISMATCHif the key is not bound to the caller's account. - fetchKeyPackage: No identity restriction (any authenticated client can fetch any identity's KeyPackage -- this is required for the MLS add-member flow).
- enqueue: If
channelIdis present, the caller's identity must be in the channel membership. IfchannelIdis absent (legacy mode), the operation is allowed for any authenticated client. - fetch / fetchWait: The
recipientKeymust correspond to an identity bound to the caller's account.
Rate Limits
| Limit | Scope | Default |
|---|---|---|
| Request rate | Per IP | 50 requests/second |
| Request rate | Per account | 50 requests/second |
| Request rate | Per device | 50 requests/second |
| Payload size | Per RPC call | 5 MB |
| KeyPackage TTL | Per package | 24 hours |
| KeyPackage uploads | Per account | Configurable (prevents store exhaustion) |
Rate limit counters use a sliding window. When a limit is exceeded, the server
responds with RATE_LIMITED and includes a Retry-After hint.
Audit Logging
The following events are logged at audit level:
- Authentication success (account, device, IP).
- Authentication failure (reason, IP).
- Token issuance and refresh (account, device).
- KeyPackage upload (account, identity key fingerprint).
- Enqueue (account, channel, recipient).
- Fetch / fetchWait (account, recipient).
- Rate limit exceeded (scope, account/IP, current rate).
All audit log entries include a timestamp and correlation ID. Sensitive fields (token values, ciphertext, private keys) are never logged.
Client Changes
Login / Register Flow
- Register: Client generates an Ed25519 identity keypair, sends the public
key to the server. Server creates an account, binds the identity key, and
returns an
(access_token, refresh_token)pair. - Login: Client presents credentials (initially: signed challenge from device key). Server validates and issues tokens.
- Token storage: Access and refresh tokens stored in the client state file
(same location as identity keypair). The state file should be
permission-restricted (
0600). - Token refresh: Client detects
TOKEN_EXPIREDerrors and uses the refresh token to obtain a new access token without re-authenticating.
RPC Integration
Every RPC call includes the Auth struct:
// Pseudocode for client RPC calls
let auth = Auth {
version: 1,
access_token: state.access_token.clone(),
device_id: Some(state.device_id),
};
node_service.enqueue(auth, recipient_key, channel_id, payload).await?;
Identity Binding
At registration, the client's Ed25519 public key is bound to the new account. The client must refuse to upload KeyPackages if the local identity key does not match the bound key -- this prevents accidental identity confusion after key rotation.
Compatibility
Wire Version Field
The Auth struct includes its own version field, independent of the delivery
message version. This allows auth changes to evolve separately from the delivery
protocol.
Legacy Support
version == 0: No auth. Server behaviour is configurable:- Development: Allow legacy calls (default for
cargo run). - Production: Reject legacy calls (default for Docker deployment).
- Development: Allow legacy calls (default for
version == 1: Full auth. This is the target for M4+.
N-1 Integration Tests
Compatibility testing covers:
- New client (v1 auth) against new server -- expected: full auth flow works.
- Old client (v0 legacy) against new server in dev mode -- expected: legacy calls succeed.
- Old client (v0 legacy) against new server in prod mode -- expected: clean
rejection with
AUTHENTICATION_REQUIRED. - New client (v1 auth) against old server -- expected: server ignores unknown
Authstruct fields; operations succeed if server does not enforce auth.
Implementation Sequence
- Extend Cap'n Proto schemas with the
Authstruct and add it to allNodeServicemethods. - Implement token validation middleware in server RPC handlers; add an in-memory token store (upgradeable to SQLite at M6).
- Bind
identityKeyto account on upload; enforce on fetch/enqueue. - Add tests: unit tests for token validation; integration tests for auth success and failure paths.
- Add rate limiting middleware with configurable thresholds.
- Add audit logging for all auth-related events.
Cross-references
- Milestones -- M4 and M6 deliverables
- Production Readiness WBS -- Phase 3 (Auth/Device/Server Hardening)
- 1:1 Channel Design -- channel-level authz
- Wire Format: NodeService Schema -- RPC schema
- Coding Standards -- security-by-design requirements