Appearance
Auth for humans and devices
A mobile browser kept showing an HTTP Basic Auth prompt. Sometimes it remembered the password. Sometimes an installed app opened a new prompt after a restart. Logging out reliably was worse.
The credentials were valid. The mechanism was a poor fit for a persistent browser application.
A login page and signed cookie fixed the prompt. They did not create multi-user authorization or turn a browser ID into a human identity.
Auth discussions fail when five different concepts collapse into one word.
Five questions, five answers
text
device identity -> which installation produced this write?
user authentication -> did this person prove an account claim?
authorization -> may this actor perform this operation?
session -> how is prior authentication carried across requests?
capability link -> what may the holder of this secret access?They can interact, but they are not interchangeable.
Device identity
A device identity is usually a random value in IndexedDB, a secure platform store, or a device-bound key pair. It can break merge ties, name a counter shard, hold per-device preferences, and label an installation in a session list.
It does not prove who holds the device. A script can copy or replace a local random ID. Clearing site data creates a new identity. Several people may use one browser profile.
Call it deviceId, not userId. That naming choice prevents later code from treating an unverified string as an account.
A registered device key can prove possession more strongly. It still does not prove that the original person is present.
User authentication
Authentication verifies a claim such as "I control the account for sam@example.com." Passwords, passkeys, enterprise identity providers, and one-time links are authentication methods.
At the authentication boundary, validate credentials, rate-limit attempts, and avoid responses that reveal whether an account exists.
A shared username and password can authenticate everyone into one shared account. That may be enough for a private household deployment. It is not multi-user identity because the server cannot distinguish members.
Authorization
Authorization decides whether an authenticated user, device, service, or capability may perform a specific operation on a specific resource.
Examples:
- This account may read space A.
- This capability may read but not write room B.
- This service may append events but not read documents.
Check authorization on the server for every transport. Hiding a button does not remove permission. REST and WebSocket writes must call the same policy.
Authentication without authorization is only a verified name. Authorization can also exist without user authentication, as capability links demonstrate.
A worked multi-user policy
Suppose a membership row contains tenantId, workspaceId, userId, and an owner, editor, or viewer role. Each resource carries the same tenant and workspace IDs plus ownerUserId. Viewers read workspace resources. Editors may change them. Workspace owners manage membership. A sensitive delete or export can additionally require the resource owner or workspace owner.
Put that scope in every query. A lookup by resource ID must also constrain tenant and workspace and verify membership. Lists use the same predicates. A guessed cross-tenant ID returns no row.
sql
SELECT r.*
FROM resources r
JOIN memberships m
ON m.tenant_id = r.tenant_id
AND m.workspace_id = r.workspace_id
WHERE r.id = $1
AND r.tenant_id = $2
AND r.workspace_id = $3
AND m.user_id = $4
AND m.role IN ('owner', 'editor', 'viewer');Workers use a service actor containing serviceId, tenant, workspace, and job ID, with an optional delegated user. Grant only the job's required operations and recheck membership when revocation must take effect. Cache keys include tenant, workspace, authorization scope, and policy version. Export jobs, files, and downloads carry and recheck the same scope.
One shared account, cookie, API key, or other shared principal is not this model. It cannot represent individual membership, ownership, revocation, or attribution.
Sessions
A session carries the result of authentication across requests. It avoids sending a password on every request.
A signed session can carry an account and session ID, expiry, device nonce, and credential version under an HMAC. The server verifies the signature and expiry. Changing the credential version can invalidate sessions after a password change.
For a browser session cookie:
- Use
Secureso it travels only over HTTPS. - Use
HttpOnlyso application JavaScript cannot read it. - Use
SameSite=Laxor a stricter policy when the flow allows it. - Prefer a host-only cookie with no broad domain scope.
- Keep login redirects same-origin.
- Clear the cookie on logout.
Browsers attach cookies automatically, so protect every mutation from CSRF. Do not mutate with GET. Use SameSite, verify an unpredictable CSRF token, and require an exact allowed Origin. A strict Referer check can cover older clients.
Cookie-authenticated WebSocket upgrades also need an Origin allowlist because normal CORS checks do not protect them. Validate before upgrade, bind the socket to the session and workspace, then authorize every message.
Stateful server sessions make selective revocation and session lists easier. Signed stateless sessions reduce server storage but need an expiry or revocation mechanism. Neither choice decides authorization by itself.
An offline app may open from a service-worker cache without checking the session. The local replica remains readable. If that is unacceptable, encrypt local data with a device secret or do not promise offline access.
Capability links
A capability link authorizes its holder for one resource or operation. It need not authenticate a person.
This is useful for low-friction sharing. A read-only room link can grant less authority than a logged-in account cookie. A write link can let a guest contribute without account creation.
Keep actor kinds explicit in policy: user, device, capability, and service. A capability actor includes its space and read-only or read-write role. Do not turn it into a fake user. Audit records can state "write capability for space B" when no human identity is known.
Compose the layers
A robust request path may work like this:
- Parse a session cookie or capability credential.
- Verify its signature, hash, expiry, and intended resource.
- Build a typed actor.
- Ask the authorization policy about the requested operation.
- Apply rate limits and business validation.
- Record the actor type and stable identifier in the audit event.
A user session may register a device, while a capability may join a space without one. These are compositions, not substitutions.
Rejected shortcuts
Use one cookie as the authorization policy. A valid session does not imply access to every resource.
Put account credentials in local storage. Page scripts and injected code can read them. Prefer HttpOnly cookies for browser sessions.
Assume logout deletes local data. Cookie removal leaves IndexedDB, caches, service workers, and queued writes untouched.
Failure modes
- Changing a password leaves old long-lived sessions valid.
- An open redirect sends a newly authenticated browser to an attacker-controlled URL.
- An API request receives a login HTML page instead of a 401 JSON response.
- A WebSocket accepts a read-only key for writes.
- A session cookie is scoped to sibling applications that do not need it.
- Logout fails offline and the UI claims local data was cleared.
- A device ID rotates after storage eviction and breaks counter or tie-break assumptions.
- Audit logs attribute capability writes to the space owner.
- Login lockout trusts an easily forged client header.
Field checklist
Chapter 19, Presence without persistence covers data that should disappear with a connection. Capability links are developed in Chapter 17.