From ec70060ae5fa665953741da921e28031b56bc5e0 Mon Sep 17 00:00:00 2001 From: tv Date: Sat, 7 Nov 2026 22:35:09 +0100 Subject: auth: logging in, and tokens as bearers POST /auth/login (by htpasswd or the proxy), /auth/refresh, /auth/logout, GET /auth/whoami; every request may bring Authorization: Bearer instead of a password. --token-lifetime and --session-lifetime. Auth plan M2. --- docs/access.md | 32 ++++++++++++++++ docs/api.md | 5 ++- docs/example.toml | 9 +++++ docs/openapi.yaml | 112 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ docs/trust.md | 3 +- 5 files changed, 158 insertions(+), 3 deletions(-) (limited to 'docs') diff --git a/docs/access.md b/docs/access.md index db86e21..e8c5455 100644 --- a/docs/access.md +++ b/docs/access.md @@ -73,6 +73,38 @@ A user the node lets in but the registry doesn't have may do nothing (`403`). whole swarm (its members, its remakes, its users), so it is given apart, never by accident with the data. A user who may do both has both. +## Tokens + +A user logs in on any node, by what that node checks (its `--htpasswd`, or a proxy's +`--user-header`), and gets a token every node of the swarm takes, sent as +`Authorization: Bearer ` instead of a password: + +```sh +curl -u alice -X POST http://localhost:3000/auth/login # {"token": "ct1.…", …} +curl -H "Authorization: Bearer ct1.…" http://other-node:3000/store +``` + +* **Who signs it**: the node it logged in on, with its node key. Every node takes a + token signed by a member of the swarm (the member list gives the keys: a node ID is + its key), and none signed by another or since forgotten. +* **What it says**: who, how it proved it, when it ends, and optionally less than the + user may do (`limit`, capabilities as `POST /admin/users/{name}` takes them): never + what the user may do. Every node judges a request by the registry it holds then, so + a capability granted or revoked counts on a node as soon as the change reaches it. +* **How long**: `--token-lifetime` (12 hours), refreshed (`POST /auth/refresh`, on any + node, which signs the new one) until `--session-lifetime` (7 days) after the login; + then the user logs in again. A refreshed token may say less (`limit`), never more. +* **Revoking**: a user disabled or removed is refused at once; all of a user's tokens, + by the user (`POST /auth/logout`) or by one who may manage users (`DELETE + /admin/users/{name}/tokens`), which sets `tokensNotBefore` in its entry: tokens issued + before aren't taken any more. A token of a node forgotten by the swarm isn't taken + either. In a swarm without a registry, a token is taken until it expires. +* **What it means to trust**: any member could sign a token for any user, as it could + write anything as anyone ([`trust.md`](trust.md)). A token is a bearer's: whoever holds it is + the user until it ends, so it goes over TLS only, as passwords do. + +`GET /auth/whoami` says who a request's user is, how it proved it, and what it may do. + ## Managing users Through `/admin/users` ([`openapi.yaml`](openapi.yaml), Users): looking needs `read` on diff --git a/docs/api.md b/docs/api.md index 5486378..8c4a534 100644 --- a/docs/api.md +++ b/docs/api.md @@ -31,9 +31,10 @@ curl -u alice:secret -X POST 'localhost:3000/store/stats/visits?increment=1' # * Bodies are JSON (`application/json`, the default), text (`text/plain`, stored as a string), or a value of a type of its own (Types); answers are JSON, or as `Accept` asks. Numbers in JSON are stored as integers where they fit, as floats otherwise. -* Every request needs a user from the node's `--htpasswd` (basic authentication), or +* Every request needs a user from the node's `--htpasswd` (basic authentication), named by a reverse proxy in front that authenticated it (`--user-header`; - [`operations.md`](operations.md), Behind a reverse proxy). What each may do, the swarm's user + [`operations.md`](operations.md), Behind a reverse proxy), or a token from logging in on any node + of the swarm (`Authorization: Bearer`, [`access.md`](access.md), Tokens). What each may do, the swarm's user registry says, by path ([`access.md`](access.md)): `403` where it doesn't let the user, and of a part it may only partly read, a read shows the keys on the way to what it may. In a swarm without a registry, users are all alike: each can read and write diff --git a/docs/example.toml b/docs/example.toml index b98752d..ab8237c 100644 --- a/docs/example.toml +++ b/docs/example.toml @@ -73,6 +73,15 @@ htpasswd = "/etc/continuum/htpasswd" # 0: never. # stream-heartbeat = "30s" +# How long a token this node issues (POST /auth/login, /auth/refresh) is taken, by every +# node of the swarm (docs/access.md, Tokens). Refreshed before it ends, it lasts until +# the login's end (session-lifetime). +# token-lifetime = "12h" + +# How long after a login (POST /auth/login) its tokens may be refreshed; then the user +# logs in again. +# session-lifetime = "7d" + # Where the reverse proxies whose user-header is believed connect from: IP addresses (or # ranges, as ADDRESS/BITS), and unix for the API's Unix sockets (bind-addr). On a host # with other users, leave out the addresses and have the proxy use a Unix socket: others diff --git a/docs/openapi.yaml b/docs/openapi.yaml index 5bf5a50..64c5b9b 100644 --- a/docs/openapi.yaml +++ b/docs/openapi.yaml @@ -26,6 +26,7 @@ servers: security: - basic: [] + - bearer: [] tags: - name: Store @@ -38,6 +39,10 @@ tags: description: | Replacing the swarm's document with a new epoch (compaction, replacement, import), and what happened to local writes that a remake couldn't place. + - name: Auth + description: | + Logging in on this node for a token every node of the swarm takes, and using it + (docs/access.md, Tokens). - name: Users description: | The swarm's users and what each may do (docs/access.md): reading needs `read` on @@ -733,6 +738,91 @@ paths: '401': $ref: '#/components/responses/Unauthorized' + /auth/login: + post: + tags: [Auth] + summary: Log in for a token + description: | + As a user this node lets in (htpasswd, or a trusted proxy's user-header): a token + signed by this node, taken by every node of the swarm until `expiresAt` + (`token-lifetime`), refreshable until `sessionExpiresAt` (`session-lifetime`). + With `limit`, the token gives only what it allows too. + security: + - basic: [] + requestBody: + required: false + content: + application/json: + schema: + $ref: '#/components/schemas/Limit' + responses: + '200': + description: The token. + content: + application/json: + schema: { $ref: '#/components/schemas/TokenAnswer' } + '400': { description: A token instead of a password, or a limit that isn't one. } + '401': + $ref: '#/components/responses/Unauthorized' + '403': { description: Not a registered, enabled user. } + '503': { description: Not ready yet. } + + /auth/refresh: + post: + tags: [Auth] + summary: Refresh a token + description: A new token for the same login, signed by this node; with `limit`, narrower still (within the token's). + security: + - bearer: [] + requestBody: + required: false + content: + application/json: + schema: + $ref: '#/components/schemas/Limit' + responses: + '200': + description: The token. + content: + application/json: + schema: { $ref: '#/components/schemas/TokenAnswer' } + '400': { description: No token, or a limit wider than the token's. } + '401': { description: The token isn't taken, or its login ended. } + + /auth/logout: + post: + tags: [Auth] + summary: End all of your logins + description: Every token of the user, of every login, no longer taken (`tokensNotBefore`). + responses: + '204': { description: Done. } + '409': { description: No user registry; tokens are taken until they expire. } + + /auth/whoami: + get: + tags: [Auth] + summary: Who you are here + description: The user, how it proved it (`htpasswd`, `header`, `token`), its registry entry's capabilities, and its token's claims if it came with one. + responses: + '200': + description: Who. + content: + application/json: + schema: + type: object + + /admin/users/{name}/tokens: + parameters: + - { name: name, in: path, required: true, schema: { type: string } } + delete: + tags: [Users] + summary: Revoke every token of a user + responses: + '204': { description: Revoked. } + '403': { description: May not manage users. } + '404': { description: No such user. } + '409': { description: No user registry. } + /admin/users: get: tags: [Users] @@ -1468,6 +1558,10 @@ components: basic: type: http scheme: basic + bearer: + type: http + scheme: bearer + description: A token from `POST /auth/login` on any node of the swarm (docs/access.md, Tokens). parameters: key: @@ -1637,6 +1731,24 @@ components: $ref: '#/components/schemas/Error' schemas: + Limit: + type: object + properties: + limit: + type: array + items: { $ref: '#/components/schemas/CapabilityRequest' } + TokenAnswer: + type: object + properties: + token: { type: string, description: '`ct1.…`: send it as `Authorization: Bearer `.' } + user: { type: string } + issuer: { type: string, description: The node that signed it. } + expiresAt: { type: integer, description: Unix ms. } + sessionExpiresAt: { type: integer, description: Unix ms. } + limit: + type: array + nullable: true + items: { $ref: '#/components/schemas/CapabilityRequest' } CapabilityRequest: type: object required: [path, rights] diff --git a/docs/trust.md b/docs/trust.md index fd37638..8b0f72a 100644 --- a/docs/trust.md +++ b/docs/trust.md @@ -34,7 +34,8 @@ secret like the swarm key: readable by its owner only (a node warns otherwise; a it creates is). **The HTTP API.** Every endpoint but the API description needs a user from -`--htpasswd`, or one a reverse proxy names (`--user-header`): the node believes that +`--htpasswd`, one a reverse proxy names (`--user-header`), or a token a node of the +swarm issued to one of those ([`access.md`](access.md), Tokens): the node believes that header from `--trusted-proxies` alone, but from those blindly, so whoever can send requests from there can be any user. With the default loopback addresses, that is every user of the host; with only the API's Unix socket (`--trusted-proxies unix`), those its -- cgit v1.3.1