Skip to content

Authentication ​

This page tells how a client proves who it is to a kinedb server, and what that proof allows. It covers the four ways to log in, the guest, the bearer token and the settings that control them. The SQL statements that make users, roles and credentials are in the SQL reference.

The ways to log in ​

MethodYou giveYou getWhere it works
PasswordA user name and a passwordA session, or a bearer tokenHTTP and WebSocket
API secretA user name and a secret kdsk_…The same as a password loginHTTP and WebSocket
Key pairA user name and a signature of a server challengeA session, the same as a password sessionWebSocket only
Bearer tokenThe token of an earlier loginThe rights of the user of the tokenHTTP and WebSocket

By default, a client must log in (KINEDB_CLIENT_AUTH=required). A client that does not log in is the guest.

An API secret and a public key are credentials of a user. A user can hold many credentials. Add and drop them with ALTER USER (Credentials).

Log in with a password ​

The first boot creates the user root. See Log in for its password. Make other users with CREATE USER (SQL reference).

Over HTTP, POST /login gives a token, and every other request carries it:

sh
curl -s -X POST http://localhost:4820/login \
  -d '{"username": "root", "password": "choose-a-password"}'
# the answer is a JSON object with a "token" and "expires_at_ms"

curl -s -X POST http://localhost:4820/sql \
  -H "Authorization: Bearer <token>" \
  --data-binary 'SHOW DATABASES'

The SQL shell reads the password from KINEDB_ROOT_PASSWORD and logs in as root. Use --user and --password-env for another user. There is no password prompt.

Each client library has a login call. See the JavaScript client and the Python client.

Switch off the password login of the web ​

The cluster setting cluster.auth.web_password_login (default true) switches the password login of the web on and off. With false, POST /login with a password and the password form of the login page (POST /auth/password) refuse the password of every user except root. The answer is the answer of a wrong password, so it does not tell which user exists. The login page shows only the provider buttons, or goes on to the provider at once when there is only one (the console login page); /auth/login?form=root shows a password form for root. A user then signs in through an identity provider and gets its token from POST /auth/session.

These logins do not change: the WebSocket login with a password (the connection string of an app), an API secret, and a key. root always keeps its password on the web, so root can always sign in and switch the setting back on.

sql
SET CLUSTER cluster.auth.web_password_login = false CONFIRM;
SET CLUSTER cluster.auth.web_password_login = true;

The server refuses false when no enabled provider is on the login page (LOGIN PAGE ON and a redirect URI), because then only root can sign in on the web. End the statement with CONFIRM to switch it off anyway. The console page "Identity providers" shows the setting as a switch. Each change writes an audit row with the actor and the new value (WebPasswordLoginOn or WebPasswordLoginOff). A refused password writes a Login row with the refusal PasswordLoginOff.

A user with no password ​

CREATE USER '<u>' NO PASSWORD makes a user that has no password (SQL reference). No password logs in as that user. A password login of the user gets the same answer, the same cost and the same audit event as a wrong password, so the answer does not tell that the user has no password. The server writes no log line for it.

A federation link connects a user to an account at an identity provider: ALTER USER '<u>' ADD FEDERATION PROVIDER <p> SUBJECT '<s>' (SQL reference). The subject is the stable id of the account at the provider, never an email address. One account at one provider belongs to one user. A user with links and no password is legal. A user signs in through a link with an identity provider: see Log in with an identity provider.

Settings for passwords ​

Three cluster settings control how a user changes the own password. The password of another user needs a password right (the password of another user). Each setting is true or false and has no environment variable. Set it with SET CLUSTER. The value of the user (ALTER USER … SET <OPTION>) overrides it. See change a password.

SettingDefaultWhat it allows
cluster.auth.default_reset_passwordtrueA user changes the own password with the old password.
cluster.auth.default_set_passwordfalseA user sets the own password with no old password.
cluster.auth.keep_sessions_on_password_changefalseA user keeps the open sessions on a password change.

The old names cluster.auth.change_own_password and cluster.auth.set_own_password are aliases of cluster.auth.default_reset_password and cluster.auth.default_set_password for one release. SET CLUSTER and SHOW SETTINGS take both names, and the two names hold one value. Use the new names: the old names go in a later release.

The upgrade of old users ​

A user that an older version created is an old user. After the update, the metadata leader upgrades each old user one time, on every node:

  • the user gets CONTROL on himself, so he manages his own API keys and key pairs;
  • each password option that the user has no value for gets the cluster value of that time: CHANGE_OWN_PASSWORD from cluster.auth.default_reset_password and SET_OWN_PASSWORD from cluster.auth.default_set_password. A value of the user (ON or OFF) stays.

A later SET CLUSTER of the two settings does not change an upgraded user: the user holds his values. A user that had his CONTROL on himself revoked after the upgrade does not get it again. The home database of an old user is default, and AUDIT and QUERY AS stay.

The leader upgrades the old users only when every voter of the metadata group runs the same build. While a node runs another build or does not answer, the leader logs [B4032_UPGRADE_WAITS_FOR_BUILD] <node> <revision> and waits. A single server upgrades its old users when it starts.

The setting cluster.auth.web_password_login (default true) controls the password login of the web. A user has no value for it. See switch off the password login of the web.

The setting cluster.auth.auto_create_users_per_hour (default 50, 0 to 10000) limits the users that the first logins through one provider with AUTO CREATE USERS ON make in one hour. See auto create users.

The setting cluster.auth.max_role_depth (default 3, minimum 1, maximum 64) limits the number of roles that a user passes through on a chain of roles inside roles. It has no environment variable. Set it with SET CLUSTER, on a single server or on a cluster. A value below the longest chain that exists is refused. See a role inside a role.

Log in with an API secret ​

An API secret is a long random text for a program, not for a person. A client logs in with the user name and the secret in place of the password. POST /login takes {"username": "carol", "api_secret": "kdsk_…"} and gives the same token as a password login.

  1. Make the secret: ALTER USER 'carol' CREATE API SECRET NAME 'ci'. The reply holds the secret one time only. Copy it at once.
  2. Give the secret to the program, for example in an environment variable.
  3. The program logs in with it. In Rust, the call is WsClient::authenticate_with_api_secret. In kd-tool, use --api-secret-env <VAR> or --api-secret-file <path>.

A wrong, unknown, dropped or expired secret, or the secret of another user, gets the one error invalid username or password. A secret with an EXPIRE AT that has passed cannot log in, and the token of a secret login ends at the latest at its EXPIRE AT. For the statements that make, list and drop a secret, see Credentials.

Log in with a key pair ​

With a key pair, the private key never leaves the client. The server holds only the public key.

  1. Make a P-256 key pair (the commands are in Credentials).
  2. Add the public key to the user: ALTER USER 'carol' ADD PUBLIC KEY '<key>' NAME 'laptop'.
  3. The client signs a challenge of the server with the private key.

How the client logs in. A client that holds the private key of a public key on the user logs in on the WebSocket. It asks the server for a challenge: 32 random bytes, for this connection only, used once and valid for 30 seconds. Then it signs one fixed byte string with its private key, in this order: the label kinedb-key-login-v1, one 0x00 byte, the protocol version (4 bytes, big-endian), the role (one byte, 0 for a client), its own 32-byte nonce, the 32-byte server nonce, the 32-byte key fingerprint (the key id), and the user name (a 2-byte big-endian length, then the UTF-8 bytes). crates/kinedb-protocol/src/key_login.rs builds these bytes. The signature is ECDSA P-256 with SHA-256, in the raw 64-byte r||s form. The server checks the signature with the public key on the user, and the session is the same as a password session. kinedb never receives or stores the private key. In Rust the call is WsClient::authenticate_with_key (the caller signs); the kinedb crate (native builds) has Remote::login_key; kd-tool takes --key-file <path>, a PKCS#8 file (DER or PEM PRIVATE KEY). In @kinedb/client the call is authenticateWithKey, with a signer from keySignerFromPkcs8; it reads the .pk8.pem file of the web console (see the JavaScript client). In the Python client the call is authenticate_with_key: the caller signs and returns 64 raw bytes (see the Python client). The embedded SDKs take a {user, key} credential in JavaScript and key_id= with sign= in Python (see the JavaScript embedded SDK and the Python embedded SDK). The wasm module holds no key and signs nothing: it asks the host for the signature. A wrong, unknown, dropped or expired key, the key of another user, a reused or old challenge, or a signature over other bytes, gets the one error invalid username or password. A DROP CREDENTIAL of the key closes a connection that logged in with it at its next request. A DROP CREDENTIAL followed by an ADD of the same key (or of the same API secret) makes a new credential. It ends the old connections and the old bearer tokens of the dropped credential. Over HTTP, see log in with a key pair over HTTP. The signature binds no server, no cluster and no TLS channel: a node that receives a login can relay it to another cluster that holds the same public key for the same user name. Until a later label adds a cluster id (B2814), give each cluster its own key.

Log in with a key pair over HTTP ​

Over HTTP, a key login takes two requests. Neither request needs a login.

  1. POST /login/challenge with an empty body. The answer is {"challenge": "kdc1_…", "server_nonce": "<64 hex>", "valid_ms": 30000}.
  2. The client makes its own 32-byte nonce and signs the same byte string as on the WebSocket, with the server_nonce of the answer.
  3. POST /login with this body:
json
{"username": "carol", "key_id": "<64 hex>", "client_nonce": "<64 hex>",
 "challenge": "kdc1_…", "signature": "<128 hex>", "protocol_version": 6}

The answer is the same as for a password login: a token and expires_at_ms. The protocol_version is the version of the signed bytes; a version below the minimum gets the upgrade text. The key id, the nonce and the signature are hex. The challenge is the text of step 1, unchanged.

Any node of the cluster can answer step 1 and step 3: the cluster signs the challenge with its token key, so a load balancer needs no sticky routing. A token of a key login ends at a DROP CREDENTIAL of the key, at a DROP and ADD of the same key, at the EXPIRE AT of the key, and in the other cases of Tokens. A token refresh keeps the key. Every failure of the check (a wrong key, a wrong signature, an old, changed or used challenge) gets the one 401 with invalid username or password. The other answers are separate: an old protocol_version gets 401 with the upgrade text, the limit of failed checks gets 429, and a node that cannot complete the login gets 503. Each signed body works one time only: after any refusal, 401 or 503, ask for a new challenge before the next try. A correct signature spends its challenge before the token is made, so a body that got a refusal after the check gets 401 again. Known limits:

  • Each node keeps the server nonces that it used, in memory, until their window ends. A signed login that someone captures can give one more token on EACH other node of the cluster, inside the window of its challenge. A restart of a node empties its set.
  • Each node accepts a challenge in a window of 35 seconds on its OWN clock: from 5 seconds before the issue time of the challenge to 30 seconds after it. Each node accepts each server nonce at most once. A clock skew between two nodes moves the window in real time: for example, a node 20 seconds behind the node that made the challenge refuses it for the first 15 seconds and then accepts it until 50 seconds after the issue. Keep the clocks of the nodes in sync (NTP).
  • Each node holds at most 65 536 used nonces. When the set is full, a key login on that node gets 503 with retry shortly, until the oldest nonces are 30 seconds old. Only a client with a registered key can fill the set: an anonymous request adds nothing.
  • The first challenge of a new cluster, or after a change of the cluster key, makes the token key record. While one node makes it, another challenge on that node gets 503.

Credential expiry and live sessions ​

A live session ends at the EXPIRE AT of its credential. A WebSocket session that logged in with an API secret or a public key ends at the EXPIRE AT of that credential: the server refuses the next request, closes the connection and sends no more WATCH rows. This is also true for a session that logged in with the token of a secret login. A session of a credential with no EXPIRE AT, or of a password, does not end by time. A change of the server clock does not extend the session: the server ends it at the earlier of the wall clock time and the time that remained at the login. The client logs in again with a live credential.

The guest ​

A client that does not log in is the guest. The guest can run SELECT, INSERT, UPSERT, UPDATE, DELETE and the EXPLAIN of each. The guest gets only the rights of GRANT … TO ANYONE. A collection without such a grant gives the guest nothing. The guest can also CALL a function with GRANT INVOKE ON FUNCTION … TO ANYONE; the body runs as the guest, or as the user of its RUN AS clause. Such a function is a small API: the site needs no server route and no password for one safe write (a small API with a function). Every other statement from the guest gets the "log in first" refusal (HTTP 401). A statement that does not parse gets its parse error, the same error that a logged-in client gets. A request with a bad token also gets HTTP 401; it never becomes the guest. To refuse the guest completely, set cluster.auth.allow_guest to false (SET CLUSTER cluster.auth.allow_guest = false, or KINEDB_ALLOW_GUEST=false on a new cluster).

The run-as rule (B3842): the user of a RUN AS clause of a function or a ticker must be eligible. Its home is default, and it is not a member of global_admins, so root is never a run-as user. The creator of the function or the ticker needs CONTROL on that user; a new user holds CONTROL on himself. A ticker with no RUN AS runs as its creator, who must be eligible too. The server judges the rule again at each call and each tick. When the user moves to another home or becomes a global admin, the old functions and tickers of that user stop: a call fails with function '<f>' may not run as '<u>': its home is not 'default' or it is a global admin (who may call a function).

Before the login, a compressed frame can unpack to 64 MiB at most when the guest is on. This is the same size that an uncompressed frame can have. With the guest off, the limit is 1 MiB: only the login frames are allowed then, and they are small. The 1 MiB limit applies to an uncompressed frame too (B2838). After the login, the limit applies to the compressed size only (B2835).

KINEDB_CLIENT_AUTH=permissive turns the login off: then anyone who reaches the port can read and write. Use it only on a network that you trust. A request that sends an expired or unknown bearer token still gets 401, so that the client logs in again.

What a login allows: the CONTROL right ​

After the login, the rights of the user decide each statement. root is a superuser and may run every statement. Every other user gets the rights of its grants: SELECT, INSERT, UPDATE, DELETE, INVOKE on a function, and CONTROL (SQL reference).

CONTROL is the right to change what protects a row or an object:

  • On the rows that a grant selects (MATCH or WHERE), it lets the user change a protected field: a field that a grant of the collection reads. A write permission alone never changes such a field.
  • On every row of a collection (a grant with no MATCH and no WHERE), it also lets the user run ALTER TABLE, DROP TABLE or DROP COLLECTION, GRANT, REVOKE and ALTER COLLECTION … SET POLICY on that collection only.
  • On a function, it lets the user drop the function and grant or revoke rights on it.
  • On a branch, it lets the user drop the branch and grant or revoke rights on it (GRANT … ON BRANCH, REVOKE … ON BRANCH). A branch also takes SELECT, INSERT, UPDATE and DELETE. A user with no right on a branch does not see it (Rights on a branch).

GRANT ALL does not give CONTROL. The guest never gets the object rights of CONTROL (ALTER, DROP, GRANT and REVOKE on a collection or a function), also from a grant to ANYONE.

What a login allows: the CREATE grants ​

A CREATE grant lets a user create a table, a function or a ticker (who may create). By default every logged-in user may create a table in every database (CREATE TABLE ON DATABASE <db> TO AUTHENTICATED), and the creator gets SELECT, INSERT, UPDATE, DELETE, CONTROL on the new table. A superuser revokes the default of one database to close it. CREATE FUNCTION and CREATE TICKER have no default: a superuser grants them. The guest never creates. The other statements of a schema (TRUNCATE, CREATE INDEX) stay for a superuser. CREATE ROLE needs a CREATE ROLE grant.

A right of a creator names the user itself, not its name. When a superuser drops a user and creates a new user with the same name, the new user gets no right on the objects of the old user, and a ticker of the old user does not run.

What a login allows: roles and users ​

A role has the rights CONTROL and SELECT, and a user has the right SELECT (rights on a role and on a user). CONTROL on a role lets the user add and remove members, drop the role and share the role. To add a member, the user also needs SELECT on the member. A member of a role counts as SELECT on it, and a user counts as SELECT on himself. Rights on a role do not nest.

SHOW ROLES and SHOW USERS show a user that is not a superuser only the roles and the users that the user may see. A role that the user cannot see answers as a role that does not exist. GRANT AUDIT and GRANT QUERY AS stay for a superuser.

What a login allows: the admin roles ​

Two kinds of role give admin rights by membership. A member gets the rights at once; no grant names them.

global_admins exists from the first start, and root is a member. No user, also not root, drops the role or removes root from it. Only a member adds or removes other members (GRANT ROLE global_admins TO 'ann'). A member holds:

  • CREATE TABLE, CREATE FUNCTION, CREATE TICKER, CREATE ROLE, CREATE BRANCH and CREATE DATABASE, in every database;
  • CONTROL on every table, function, ticker and role;
  • CREATE PROVIDER and DROP PROVIDER, and ALTER USER … SET DEFAULT DATABASE.

A role with the name global_admins that an older version created gives no admin right. The upgrade renames such a role to global_admins_old (or global_admins_old_2 and so on, when the name is in use). The renamed role keeps its members, its grants and its rules, and each role that held it as a member holds the renamed role. Then global_admins is the built-in role, with root as its one member. The log line [B4032_GLOBAL_ADMINS_RENAMED] gives the new name.

A login with MAP ROLES BY NAME ON never grants a built-in role: a value global_admins or admins_<db> in the roles claim of the token gives no role. A role mapping of a provider names the principal id of its role, so a mapping that names the renamed role grants global_admins_old, never the built-in global_admins.

admins_<db> is made by CREATE DATABASE <db>. The creator is its member and holds CONTROL on it (rights on a role). A member holds CREATE TABLE and CREATE BRANCH in <db>, CONTROL on the tables of <db>, and DROP DATABASE <db>. It has no right in another database. DROP DATABASE <db> drops the role too; before that, no user removes it. The database default has no admin role. A database that an older version created has no admin role either.

What a login allows: the password of another user ​

Two rights on a user change the password of that user: SET PASSWORD (with or without the old password) and RESET PASSWORD (only with REPLACE '<old>'). A user holds a right on user u through a rule on u, a rule on the users of the home of u (ON USERS IN DATABASE <home>), membership of admins_<home>, or membership of global_admins (the password of another user).

  • Only a member of global_admins changes the password of a member of global_admins. root is a member.
  • KEEP SESSIONS on another user needs a member of global_admins. Every other change of the password of another user ends all the sessions of that user.
  • CONTROL on a user gives no password right.
  • A wrong old password counts in the limit of the user that runs the statement (the limit of failed password checks).
  • The own password follows the three settings of settings for passwords, not these rights.

The cluster judges the right again when the change applies, in the order of the log.

Tokens ​

A bearer token of POST /login works on every node of the cluster, also after a restart of the nodes, for 24 hours. A login writes nothing. POST /logout ends that one token on every node; the other sessions of the user stay. ALTER USER … REVOKE SESSIONS, a password change, DROP CREDENTIAL and DROP USER end the token on every node. A node that cannot reach a metadata leader to check a new token answers 503 with retry shortly, never 401. Known limits:

  • After the upgrade to this version, each client logs in again one time: a token of an older version gets 401.

  • A node that is behind accepts a token that a logout, a password change, REVOKE SESSIONS, DROP CREDENTIAL or DROP USER ended, until that node applies the change. The statement answers after the commit, and each node applies the change a short time later. The metadata barrier does not make this time shorter: a node runs the barrier only when its own check refuses a token, so that it never answers 401 for a token that it has not seen yet.

  • A WebSocket connection that logged in with a token stays open after POST /logout of that token. The logout ends the token for the next request and the next login. To close the connection too, run ALTER USER … REVOKE SESSIONS, or close it in the client.

  • A restore of a backup brings back the logouts of the backup time. A token that was logged out after that time works again until its expiry. Run ALTER USER … REVOKE SESSIONS for its user, or change the password.

  • The list of logged-out tokens holds at most 1 024 ids of one user and 65 536 ids in total, each until its token expires. A logout past the limit of its user ends all the sessions of that user. Thus one user fills only its own part. To fill the total list, at least 64 accounts must log out 1 024 tokens each. Then a logout of any user ends all the sessions of that user, until the ids expire (at most 24 hours).

  • Each node limits the logouts of one user: 20 at once, then 1 each 6 seconds. Past the limit, POST /logout answers 429 Too Many Requests with Retry-After and AUTH_THROTTLED: too many logouts of this user; retry in <N> s. The logout did not happen, and the token stays valid. Each logout writes one entry to the replicated metadata, so the limit keeps one user from filling the metadata log.

  • A logout that fails answers 503 with "retry": true. The text tells the two cases apart. "The logout did not happen" means that the token stays valid. "The outcome of the logout is unknown" means that the entry was sent and can still apply, so the token can be dead. In both cases, send the logout again: a second logout of a dead token answers 200.

  • The metadata leader checks the token of each logout that another node sends to it. It also accepts a new stored key only for the cluster key of the link to that node.

  • After a change of the cluster key (the upgrade from the cluster secret of an older version), the metadata leader removes the stored keys of the old key material. It keeps the key of its own material and the key of "no material".

  • The stored key is a random key in the replicated data of the cluster. The signing key is made from the stored key and the token key material of the cluster key pair (The cluster key), and it signs the tokens. A copy of the stored key alone signs nothing. A copy of the stored key together with cluster.key and the lock key of its node (lock.key on a node with no TPM) can sign a token for any user, root too. Keep the data directory and its backups as private as the root password. Each such token lives at most 24 hours and 10 minutes. A node refuses a later expiry. But the holder can sign a new token at any time. There are two repairs:

    • ALTER CLUSTER ROTATE TOKEN KEY (only root can run it) makes a new stored key. The tokens of the previous key work until their expiry. After a key theft, run the statement TWO times to end the tokens of the stolen key. Run it THREE times to also end every token that was minted between the first two runs. Such a token has the key of the middle generation. It stays valid until its expiry, or until a third rotation. A node that is behind, or cut off from the cluster, accepts a dropped key until it applies the rotation. An open WebSocket connection of a dropped key ends at its next request. A live WATCH of that connection ends at its next push or frame. A WebSocket connection that logged in with a password is not affected. Logins keep working: a new login gets a token of the new key.

      The cost: after the second rotation, each client of an old token gets a 401 and logs in again. Each rotation makes every open WebSocket connection read the users lock once.

      A rotation does not help when the thief can also read the data directory, the WAL or the backups. The new key is stored and replicated in the same way as the old key.

    A node with no cluster key and no peers (a node with no data directory) makes the signing key from the stored key alone. Then a copy of the data alone can sign a token. A node with peers and no cluster key yet answers 503 to a login and to a token check. A user record from before this version holds no random incarnation. An example is the root user of an older cluster. The signing key alone signs a token for such a user.

  • The nodes need clocks that agree within 10 minutes. Keep the clocks in sync (NTP).

    • A node whose clock is more than 10 minutes slow refuses a fresh valid token with 401. The reason: the expiry of the token is later than the clock of the node + 24 h + 10 min. Behind a load balancer the client logs in again in a loop.
    • A logout sent to such a node answers 200 and ends nothing. That node finds the token not valid, so it writes no logout. The other nodes still accept the token. This failure is silent. It occurs on every node whose clock is more than 10 minutes slow, also on the metadata leader.
    • A metadata leader whose clock is more than 10 minutes slow refuses each logout that another node sends to it. The client gets the retryable 503 on every retry. The token stays live.
    • A metadata leader whose clock is more than 10 minutes fast removes a logged-out token from the list before the token expires. The token stays refused on every node: each node refuses a token whose expiry is not later than the latest expiry that a removal took (B2743). But each node also refuses a valid token that expires before that point, a little before its expiry. The log of each node with a correct clock shows [TOKEN_REVOKE_SWEEP_EARLY].

Refresh a token ​

POST /token/refresh with Authorization: Bearer <token> gives a new token without the password or the API secret. The answer has the same JSON form as POST /login: token and expires_at_ms.

sh
curl -s -X POST http://localhost:4820/token/refresh \
  -H "Authorization: Bearer $TOKEN"
  • The new token ends at the earliest of these three times: 24 hours from now, the EXPIRE AT of the credential, and 30 days after the login that started the chain. A refresh never makes a session longer than 30 days. After 30 days, log in again with the credential.
  • The old token ends at once, on every node. Each refresh writes one entry to the replicated metadata, the same as a logout. A login still writes nothing.
  • A second refresh of the same token ends the chain: the tokens that later refreshes made are refused at their next HTTP request and at the next WebSocket login. The server cannot tell a retry from a stolen copy of the token. Then the user logs in again with the credential.
  • Never send a refresh again. Log in again. After a timeout, a lost answer or the "outcome unknown" error (status 500), the refresh can have happened: a second refresh with the old token ends the chain. Only a 503 with "retry": true tells that the refresh did not happen; then you can send it again.
  • You can send the new token to any node. A node that has not yet applied the refresh that made the token waits for it. If the wait fails, the node answers the 503 with "retry": true. It does not end the chain (B2920).
  • Refresh at about half the lifetime of the token. Two browser tabs that share one token and refresh at the same time end the chain: let one tab refresh and share the new token.
  • Every refused refresh gets the same 401 invalid_token, for an expired, logged-out, refreshed or unknown token and for a chain older than 30 days. The answer time can differ: the refresh of a refreshed token waits for the metadata write that ends its chain.
  • A refresh uses the logout limit of the user (20 at once, then 1 each 6 seconds). Past the limit, it answers 429 Too Many Requests with Retry-After and AUTH_THROTTLED. The old token stays valid. A thief with a live copy of a token can keep this limit empty with refreshes, so the user gets 429 on a refresh or a logout. The end of a chain by a replay does not use the limit.
  • ALTER USER … REVOKE SESSIONS, a password change, DROP CREDENTIAL, DROP USER and two ALTER CLUSTER ROTATE TOKEN KEY end a refreshed token, the same as any other token. The token that made a change to its own user's password stays valid, but it cannot refresh: log in again.
  • An open WebSocket connection keeps its login. A refresh, the end of a chain and the 30-day cap do not close it, the same as POST /logout. To close it, run ALTER USER … REVOKE SESSIONS. Use the new token at the next connect.
  • Only POST /token/refresh finds a replay. A refreshed token that a client sends to POST /sql, to a WebSocket login or to a QueryAs gets the plain 401 and does not end the chain. So if a thief refreshes a stolen token first, the chain of the thief stays live (at most 30 days) until the user sends the old token to POST /token/refresh, or until REVOKE SESSIONS or a password change.
  • The list of revoked tokens has a limit: 1 024 rows of one user and 65 536 rows in total. A refreshed session holds ONE row, whatever the number of its refreshes (B2878). The row names the newest token of the session, and every older token of the session is refused. The row stays until the newest token would expire (at most 24 h after the last refresh). So the limit counts the refreshed sessions, not the refreshes. Past the limit, a refresh or a logout of that user ends all the sessions of that user (REVOKE SESSIONS).
  • The node that took the refresh keeps the result of the last 4 096 refreshes. A node that is more than 4 096 refreshes behind when the refresh applies answers "outcome unknown", also when the refresh happened. Log in again.
  • A refused refresh of a dead token writes the audit row of a refused token login (TokenLogin, actor unknown), not a TokenRefresh row: the server does not trust the user name in a dead token.
  • A token from a version before the refresh also refreshes. Its chain starts 24 hours before its expiry.

Run a query as a user of a token (QueryAs) ​

A web server can keep one WebSocket connection for all requests. Each request has the bearer token of its own user. The frame QueryAs (B2843) runs ONE statement as the user of that token. The statement runs with the rights of that user only. It never has the rights of the connection.

  1. A user of the connection logs in. Give this user the right: GRANT QUERY AS TO 'webapp' (GRANT QUERY AS). The user, or one of its roles, must hold the grant. No user has it by default, and root needs it too.
  2. The client sends the token and the statement in each QueryAs frame.
rust
use kinedb_core::Value;
use kinedb_protocol::client::WsClient;
use kinedb_protocol::params::Params;

// `client` is a WsClient, logged in as 'webapp', which holds GRANT QUERY AS.
// `token` is the `kdt1_…` token that the user of the request got from POST /login.
let rows = client.query_as(&token, "SELECT id, total FROM orders")?;

// With a database, a branch and parameters:
let params = Params::new(vec![Value::Int(42)])?;
let rows = client.query_as_in(
    &token,
    "SELECT * FROM orders WHERE id = $1",
    Some("shop"),
    None,
    Some(params),
)?;

The rules:

  • The server checks the token at EVERY frame. The check is the same as for a token login: the signature, the expiry, the user, the incarnation, the token epoch and the credential. A logout, a revoke, a dropped user or a dropped credential applies to the next frame.
  • Each failure gives the one error session expired or revoked: log in again, and the client must not retry it. The error never tells which check failed. A connection with no login, under KINEDB_CLIENT_AUTH=required, first gets the usual refusal that asks for a login.
  • A connection past its own EXPIRE AT is closed. The fence closes the connection before the handler runs, so the frame gets no session expired reply. A QueryAs on the peer plane is refused.
  • QueryAs needs a client-plane connection. A node with both KINEDB_PEER_AUTH and KINEDB_CLIENT_AUTH set to permissive puts a client without the cluster header on the peer plane. There the node refuses QueryAs.
  • WATCH, UNWATCH, USE, BEGIN, COMMIT and ROLLBACK are refused through QueryAs. A frame carries one statement of one user.
  • A query that the node forwards to another node carries the user of the token as the caller. The token never leaves the node that received it.
  • The audit trail has ONE event for each pair of a connection and a token user, with the action query_as (GRANT QUERY AS). The actor is the user of the connection, and the target is the user of the token. It also records each refusal. The token is never in a log, in SHOW STATEMENTS, in the slow-query list, in an audit row or in an error. SHOW STATEMENTS names the user of the token.
  • The server needs protocol version 6 (breaking changes).
  • The JavaScript client sends the frame with socket.queryAs(token, sql, opts) (JavaScript client). The Python client sends it with sock.query_as(token, sql, params) (Python client) (B2886).
  • A token of a revoked chain (B2846) is refused at the next QueryAs frame, the same as every other dead token. A QueryAs with a refreshed token does not end the chain: only POST /token/refresh finds a replay (Refresh a token).

kinedb as an identity provider: the signing keys ​

kinedb can sign ID tokens with its own key. An app checks such a token with the public key of kinedb. This release adds the key and the two public documents that publish it. The route that starts a sign-in for an app is in the next section. No route signs an ID token yet.

The issuer ​

The issuer is the URL at which the apps reach the cluster. Set it with SET CLUSTER:

sql
SET CLUSTER cluster.auth.issuer = 'https://auth.example.com';

The default is empty, and an empty issuer turns the two documents off. The URL follows these rules:

  • The scheme is https. The scheme http is allowed only for the host localhost, 127.0.0.1 or [::1].
  • The URL has no query, no fragment and no user information.
  • The URL has no / at the end. A path is allowed, for example https://example.com/kinedb.
  • The URL has at most 2048 bytes and no white space.

The issuer is a cluster setting. kinedb never reads it from the Host header of a request. A reverse proxy that serves the issuer path must map that path to the node: /kinedb/… becomes /….

The two public documents ​

Two GET routes answer with no login. Both answer 404 until the issuer is set.

RouteWhat it holdsCache-Control
/.well-known/openid-configurationThe discovery document: the issuer, the endpoint addresses (also userinfo_endpoint and end_session_endpoint), the jwks_uri, the algorithm ES256.public, max-age=3600
/.well-known/jwks.jsonThe key document: the public keys, the active key first, then the retired keys.public, max-age=300

The endpoints /authorize and /token in the discovery document exist (sign-in for apps). Each key in the key document is an EC key of the curve P-256, with kid, use = sig and alg = ES256. The key document never holds a private key. A cluster with no key yet gives {"keys":[]}. Both documents allow any origin (Access-Control-Allow-Origin: *).

Each source address may send 60 requests in a burst, and then 1 request each second. More requests get 429 Too Many Requests with a Retry-After header.

The first key ​

kinedb makes the first key at the first use, never at boot. The first use is the first signature of an ID token, or the first ALTER CLUSTER ROTATE ID TOKEN KEY. A request for a public document never makes a key.

The metadata leader makes the key. It seals the private key with the seal key of the cluster key pair (The cluster key), and the cluster replicates only the sealed bytes. The private key never crosses a link between nodes. No view, no log, no audit row and no dump holds it.

Every node needs the cluster key, also a single node. Without it, a key statement is refused: an ID token key needs the cluster key on this node. A key that an older version sealed under the cluster secret does not open: the next signature fails, and the message tells you to run ALTER CLUSTER ROTATE ID TOKEN KEY. The old public key stays in the key document, so the old tokens still verify.

Rotate the key and the overlap ​

Only a superuser may run these statements (SQL reference):

text
SHOW ID TOKEN KEYS;
ALTER CLUSTER ROTATE ID TOKEN KEY;

A rotation makes a new active key. The old key becomes retired and stays in the key document. An ID token lives at most 1 hour. So a retired key stays for at least 1 hour and 10 minutes after its retirement. A later rotation removes a retired key that is past that time. A rotation that comes too early keeps it. No background job removes a key. The cluster holds at most 16 keys.

kinedb as an identity provider: sign-in for apps ​

An app can send a user to kinedb to sign in. The route is GET /authorize. It uses the code flow with PKCE. kinedb gives the app a code at its return address. The app redeems the code at POST /token and gets a bearer token of the user and an ID token (Use kinedb as the identity provider of your app). An admin must register each app and its return addresses (Apps and return addresses).

The flow ​

  1. The app sends the browser to /authorize with the parameters in the table below.
  2. kinedb checks the app, the address and the parameters.
  3. If the browser has no live session, kinedb sends it to the login page. The login page shows the password form and the buttons of the providers (Log in with an identity provider).
  4. After the login, the browser comes back to /authorize. kinedb signs a code and sends the browser to the return address of the app, with code, state and iss.
  5. The app redeems the code at POST /token. kinedb answers with the access token and the ID token.

/authorize answers 404 until cluster.auth.issuer is set (The issuer). It works with --no-web. It answers only GET. A POST gets 405 with Allow: GET.

The parameters ​

Send each parameter one time. A parameter that appears two times is refused.

ParameterRule
response_typeRequired. Must be code.
client_idRequired. The name of a registered app that is enabled. The built-in app kinedb-admin is refused.
redirect_uriRequired. Must match a return address of the app. It has at most 512 bytes. A relative address is refused.
scopeMust hold the word openid. A space separates the words. kinedb ignores a word that it does not know.
stateOptional. At most 512 printable bytes (0x20 to 0x7E). kinedb sends it back as it is.
nonceOptional. At most 255 printable bytes.
code_challengeRequired. 43 characters of base64url (the SHA-256 of the verifier).
code_challenge_methodRequired. Must be S256. plain is refused. This holds for every app.
promptOptional. One of none, login, consent or select_account. none must be alone. login and select_account go on to the provider.
response_modeOptional. Must be query.

request and request_uri are refused (request_not_supported and request_uri_not_supported). kinedb ignores every other parameter, also max_age, login_hint, ui_locales and display.

The fixed answers ​

The answer depends on the first check that fails.

  • A bad app (not known, not enabled, kinedb-admin, missing or two client_id) gets an error page with 403. The page has no redirect.
  • A bad address (missing, two redirect_uri, or no match with a return address of the app) gets an error page with 400. The page has no redirect.
  • Any other failure gets a redirect to the return address with an OAuth error: invalid_request, unsupported_response_type, invalid_scope, login_required or temporarily_unavailable. The redirect has the state when the state is valid. It has no error_description.
  • Each redirect (code or error) has the parameter iss. Its value is the issuer (RFC 9207). The discovery document says authorization_response_iss_parameter_supported: true. The app must compare iss with the issuer that it knows.
  • The redirect keeps the query of the stored address. It adds code or error after it.

Every answer has Cache-Control: no-store and Referrer-Policy: no-referrer.

The prompt parameter and no session ​

  • With no prompt, a browser with no live session goes to the login page.
  • With prompt=login, the browser goes to the login page also when it has a session. The return to /authorize has no prompt, so the login does not repeat.
  • With prompt=none and no live session, kinedb sends error=login_required to the address. It shows no page. An app uses this to renew its sign-in with no user action.
  • prompt=login and prompt=select_account go on to the provider. The login page address carries them as its own prompt parameter, and the provider login sends them to the provider. With prompt=login, the provider asks for the password again. With prompt=select_account, the provider shows its account chooser. An explicit prompt wins over the account chooser after a sign-out.
  • prompt=select_account with a live session acts as no prompt: one browser has one session at kinedb.
  • prompt=consent acts as no prompt and does not go to the provider. kinedb has no consent page: an admin registers each app.

kinedb does not issue a refresh token in this version. An app that wants a new sign-in sends the browser to /authorize with prompt=none.

A login on the login page of kinedb sets the SSO cookie. The password form and the provider callback set it. POST /login (the JSON API) does not set it.

AttributeValue
Name__Host-kinedb_sso behind a trusted https proxy, else kinedb_sso.
HttpOnlyYes. A script cannot read it.
SameSiteLax.
Path/.
SecureYes in the __Host- mode only.
Life8 hours from the login. The life does not slide. The end of the credential can make it shorter.

The cookie is not a bearer token. A token cannot work as an SSO cookie, and an SSO cookie cannot work as a token. The session ends when one of these happens:

  • The 8 hours end.
  • The user changes the password.
  • An admin runs ALTER USER … REVOKE SESSIONS.
  • An admin drops the user, the federation link or the provider.

A node refuses a request with two SSO cookies. The user then has no session and signs in again.

SameSite=Lax has an effect on a hidden iframe. A browser does not send a Lax cookie in a cross-site iframe. So an app that renews with prompt=none in a hidden iframe gets login_required. A top-level redirect to /authorize works.

The code ​

The code is a signed text that starts with kda1_. It holds the session, the app, the return address, the challenge, the nonce and the scope. The cluster key signs it.

  • The code is valid for 60 seconds.
  • The code is for one use on one node. Each node keeps a list of the codes that it has seen. The list holds at most 65,536 entries. A full list refuses the code. A restart empties the list.
  • A code does not work for another app, another address or another verifier.

A replay on another node needs the code and the PKCE verifier inside 60 seconds. A restart of a node inside 60 seconds also allows one more use of a code on that node.

The rate limit ​

Each source address may send 60 requests to /authorize in a burst, and then 1 request each second. A request over the limit gets the error page with 429 and no redirect. kinedb does not give a token back. The limit is on each node.

Audit ​

SHOW AUDIT records these actions. A row never holds a code, a cookie, the state, the nonce, the challenge or the return address.

ActionCodeWhen
AuthorizeCode94kinedb sent a code to an app.
AuthorizeRefused95kinedb refused a request. The row has a refusal code.
AuthorizeByAnyPattern96The return address matched only the pattern *. kinedb writes it as a second row with each answer that goes to the address.

The refusal codes of the row 95 are:

CodeNameMeaning
8ThrottledThe rate limit.
25ReturnRefusedThe return address does not match.
31AppRefusedThe app is not known or is not enabled.
32RequestInvalidA parameter is missing, wrong or twice, or the request uses request or request_uri.
33ResponseTypeRefusedresponse_type is not code.
34PkceRequiredThe challenge or its method is missing or wrong.
35ScopeRefusedThe scope has no openid.
36LoginRequiredprompt=none and no live session.

A row names the app as its object. An app name with a - gives no object (known issue B3882).