Appearance
Log in with an identity provider
This page tells how a user signs in to the web interface of kinedb through an external identity provider, for example Microsoft Entra ID, Google or Keycloak. The password login stays. root keeps its password. The provider handles the second factor of the user.
How the login works
- The browser opens
/auth/login. The built-in login page shows one button for each provider and the password form. - The user selects a provider. The browser goes to
/auth/start. kinedb sends it on to the provider. - The user signs in at the provider. The provider sends the browser to
/auth/callback. - kinedb checks the answer, finds the user, sets the roles and makes a bearer token.
- kinedb sends the browser to the
returnaddress. The token is in an HttpOnly cookie.
A login on this page, by password or by provider, also sets the SSO cookie. An app that uses kinedb as its identity provider needs this cookie (sign-in for apps). When the return address is a return to /authorize, kinedb sets no kinedb_session cookie.
No node of a cluster keeps the state of a login. The login can start on one node and end on another node.
The routes
The routes are in the binary. They are not under /ui, so they also work with --no-web.
| Route | What it does |
|---|---|
GET /auth/providers | Lists the providers for the host of the request, as JSON. It shows only name and display_name of each provider, and password_login: false when the cluster setting cluster.auth.web_password_login is false. |
GET /auth/login?return=<address> | Shows the login page. return is optional. The default is /ui/. With the password login of the web off, the page has no password form; form=root adds a password form for root. An optional prompt (login, select_account, or both with a space) goes on to the provider links; another value is ignored. |
GET /auth/start?provider=<name>&return=<address>&app=<name>&prompt=<prompt> | Starts a login at a provider. app is optional. The default is kinedb-admin. An optional prompt (login, select_account, or both) goes to the provider; another value is ignored. |
GET /auth/callback | Receives the answer of the provider. |
POST /auth/password | Receives the password form of the login page. |
POST /auth/session | Turns the session cookie into a bearer token. |
POST /auth/link/start | Starts the link of a second provider for the signed-in user. It needs the bearer token. See Link a second provider. |
GET /auth/link | Shows the prompt of a link. |
GET /auth/link/prompt | Gives the values of the prompt as JSON, for a custom link page. |
POST /auth/link/accept | Adds the link that the prompt shows. |
POST /auth/link/refuse | Closes the prompt and adds no link. |
A provider is in the list of /auth/providers when all of these are true:
- The provider is enabled and
LOGIN PAGEisON. - The provider lists the host of the request in
HOSTNAMES, or has no host list. - One
REDIRECT URISentry of the provider fits the host.
/auth/start does not need LOGIN PAGE ON. The flag only controls the button.
A proxy in front of kinedb must keep the Host header. kinedb does not read X-Forwarded-Host.
Limit the host names of a provider
A provider with an empty Host names list accepts a login on any host name that reaches the node. The page Identity providers of the web interface shows a warning on each such provider that is enabled: on its row in the list, and in its edit form next to the field Host names:
"This provider has no host names, so it accepts a login on any host name that reaches this node. Enter db.example.com in Host names."
The warning gives the host name of the page as the value to enter.
To limit the host names in the web interface:
- Open the page Identity providers.
- Click the name of the provider. Its edit form opens.
- In Host names, enter the host name of the web interface, for example
db.example.com. Write one host name on each line. The hint under the field shows the host name of the page: "Example for this page: db.example.com". - Click Save.
A new provider gets the host name of the page as the default value of Host names, and https://<host>/auth/callback as the default value of Redirect URIs.
After Save, the page reads the list of the providers again, and the warning of the provider goes away. You do not reload the page. A disabled provider gets no warning.
The Return addresses list of the app kinedb-admin is not the fix. The built-in app takes relative patterns only, for example /ui/*, so a host name cannot go in it (see Apps and return addresses).
Register the redirect address at the provider
Register this address as the redirect address (also called the callback address) of the client at the provider:
https://<host>/auth/callbackUse the same address in REDIRECT URIS of the provider in kinedb. The path is always /auth/callback. kinedb uses the stored address text. It never builds an address from the Host header. A login that starts on a host uses only a provider that lists this host, or lists no host.
sql
CREATE PROVIDER corp DISCOVERY 'https://login.example.com'
CLIENT ID 'kinedb'
CLIENT SECRET 'the-secret-of-the-provider'
REDIRECT URIS ('https://db.example.com/auth/callback')
DISPLAY NAME 'Corp SSO'
LOGIN PAGE ON HOSTNAMES ('db.example.com');The statements for providers are in the SQL reference. kinedb asks for the scopes openid email profile. It uses the code flow with PKCE.
The claims that kinedb reads
kinedb checks the ID token of the provider before it uses a claim.
| Claim | What kinedb does with it |
|---|---|
iss | Must equal the issuer of the provider. |
aud and azp | Must fit the client id of the provider. |
exp, nbf, iat | Must be valid now. |
nonce | Must be the nonce of this login. |
sub | The stable id of the account. With SUBJECT CLAIM 'oid', kinedb reads oid. |
email | Used only at the first login of a user (see the match rule). |
email_verified | Not checked. kinedb trusts the provider: the provider must validate the emails. |
the ROLES CLAIM (for example groups) | The values go to the role mappings. It is a list of strings, a string, or absent. |
kinedb refuses a token with a group overflow. This is a token that says the groups are too many and sends a link instead (_claim_names or hasgroups). kinedb does not follow the link. Reduce the groups that the provider sends, or send only the groups that map to roles.
The signature algorithm must be one of the ALGORITHMS of the provider. none and HS256 are refused. If the provider rotates its keys, kinedb refreshes them at once for a new kid.
The match rule
kinedb finds the user in this order:
- kinedb looks for the pair (provider, subject) in the federation links. If it finds the pair, the login uses that user. A changed email does not matter.
- If it finds no pair, this is a first login. The ID token must have an
emailclaim, else kinedb denies the login (EmailClaimMissing). The user name is the email in ASCII lower case. kinedb does not checkemail_verifiedor the domain of the email: you add a provider because you trust it, so the provider must validate the emails. - If a user of that name exists and has no federation link yet, kinedb adds the link for (provider, subject) and signs the user in. A user that has a link already is refused (
UserAlreadyLinked). - If no user of that name exists and the provider has
AUTO CREATE USERS ON, kinedb makes the user with no password and with the link, in one write. Then the role mappings apply as for every login. WithAUTO CREATE USERS OFF(the default), kinedb denies the login (NoUser).
A user name that you match by email is in lower case. A user Alice@Corp.com does not match. An email Alice@Corp.com makes the user alice@corp.com.
Auto create users
With AUTO CREATE USERS ON, every account of the provider that signs in gets a kinedb user. Use it only with a provider that validates the emails of its accounts, and that holds only the accounts that you want in kinedb. The new user has no password and no role. The role mappings of the provider give the roles.
The cluster setting cluster.auth.auto_create_users_per_hour (default 50, 0 to 10000) limits the users that one provider makes in one hour. A login over the limit is refused (AutoCreateLimit) and makes no user. 0 makes no user. The node that runs the writes (the metadata leader) counts, so a new leader starts a new count.
sql
ALTER PROVIDER corp AUTO CREATE USERS ON;
SET CLUSTER cluster.auth.auto_create_users_per_hour = 20;To add a link by hand, use ALTER USER '<u>' ADD FEDERATION PROVIDER <p> SUBJECT '<s>' (Federation links).
Roles
kinedb applies the role mappings of the provider before it makes the token. The new session has the right roles. A login that changes no role writes no role grant. The audit rows of these changes name the provider as the actor.
The token and the cookie
A token from this login has the credential flag 3. The flag points to the stored link. The token ends when an admin drops the link or disables the provider. kinedb refuses to drop a provider that still has links, so drop the links first.
kinedb does not put a token in an address. An address goes into the history of the browser and into the logs of proxies. Instead, the callback sets the cookie kinedb_session:
- The cookie is
HttpOnly,SameSite=Strict, and lives 120 seconds. - When the request came over https through a trusted proxy that sends
X-Forwarded-Proto: https, the cookie isSecureand its name is__Host-kinedb_session. A browser accepts a__Host-cookie only withSecure,Path=/and noDomain, so a host next to yours (a sibling subdomain) cannot set it. - Over http the name is
kinedb_sessionand the cookie goes only toPOST /auth/session.
The console sends POST /auth/session from the same origin. The node refuses the request with 403 when the Origin header does not name the host of the request; the cookie then stays. It refuses a request with two session cookies with 400. The answer is JSON with token, expires_at_ms and username. The cookie ends at once. The browser keeps the token as it does for a password login.
A second cookie binds a login to the browser that started it: __Host-kinedb_login behind https, else kinedb_login (path /auth/). It ends when the login ends. A callback whose Cookie header holds this cookie two times is refused: the node does not pick one.
The console login page
The login page of the web console (/ui/) shows a button for each provider that GET /auth/providers returns for the host of the request. The buttons are beside the password form. The page shows only the providers of the host. After a provider login, the console loads and sends POST /auth/session to take the session. A signed-in user can select Link an account in the user menu: the console sends POST /auth/link/start (Link a second provider).
When the cluster setting cluster.auth.web_password_login is false, the console login page shows only the provider buttons and a Sign in as root button. That button shows a password field for root. The server refuses the password of every other user on the web. The Identity providers page shows the setting as a switch. See Switch off the password login of the web.
When the login page has exactly one provider and no password form, the page has no choice to offer. GET /auth/login then answers a 302 to /auth/start for that provider, with the same return and app. A disabled provider does not count. The page stays in these cases: two or more providers, a password form, no provider, or a custom login.html (the login page of an app).
Admins manage the providers and the apps on the Identity providers page and the Apps page of the console. Only a superuser sees these pages. The create form and the edit form of a provider show AUTO CREATE USERS as a switch, with the warning that the provider must validate the emails (Auto create users). See Identity providers and Apps and return addresses.
The return address and the app
The return address must match a pattern of the app. Name the app with the parameter app=<name> on /auth/start. The default is the built-in app kinedb-admin. See Apps and return addresses. A refused address ends at an error page that shows no redirect. return has at most 512 bytes. A return to /authorize (the form that kinedb makes itself) may have up to 960 bytes. The return address of an app stays at 512 bytes.
Link a second provider
A signed-in user can add the account of a second provider to the user. After the link, the user can log in with either provider. The user starts the link from a session, and accepts it on a prompt page. An admin can also add a link by hand with ALTER USER … ADD FEDERATION PROVIDER (Federation links).
Who can link
Each user with a live bearer token can start a link: a token of a password login or of a provider login. The link is always for the user of that token. A logout, REVOKE SESSIONS or a drop of the user before the accept refuses the accept.
Start the link
The console sends this request with the bearer token of the user:
http
POST /auth/link/start
Authorization: Bearer <token>
Content-Type: application/json
{"provider": "partner", "return": "/ui/"}The body can also be a form (application/x-www-form-urlencoded) with the same fields. return is optional; the default is /ui/. app is optional, as on /auth/start. The provider must be enabled and must fit the host of the request, as for a login.
The answer is 200 with the JSON {"location": "<address>"} and the cookie kinedb_login. The console then sends the browser to location, the login page of the provider. The answer is not a redirect, because a fetch cannot follow a redirect to another site. A request with no bearer, or with a token that ended, gets 401 and no cookie.
The token never goes into a cookie or an address. The cookie kinedb_login holds only a reference to the session, and the node checks that session again at each step.
The prompt
The user signs in at the provider. The provider sends the browser to /auth/callback. Then:
- The account at the provider is already linked to this user: the browser goes to the
returnaddress. kinedb shows no prompt and writes nothing. - The account is linked to another user: the node shows the "in use" page with the status
403. The page shows a fixed text: "This account is linked to another user. Ask an administrator." It never names the other user. - The user already has a link of this provider (with another account): the error page with the code
UserAlreadyLinked. One user has at most one link of each provider in this flow. - Else the node sends the browser to
GET /auth/link. That page asks: "Add the account of <display name> to the user <name>?". It has two buttons: "Add the account" and "Do not add".
The prompt lives 5 minutes. The node keeps the prompt in the cookie kinedb_link (HttpOnly, SameSite=Lax, Max-Age=300; behind https __Host-kinedb_link). No page holds the account id at the provider (the subject), the token or this cookie. A reload of /auth/link shows the same prompt again.
Accept or refuse
- "Add the account" sends
POST /auth/link/accept. The node adds the link (provider, subject) for the signed-in user and sends the browser to thereturnaddress. The user keeps the current session. The accept grants no role and makes no new token. The next login with the second provider applies its role mappings. - "Do not add" sends
POST /auth/link/refuse. The node writes nothing and sends the browser to thereturnaddress. The user stays signed in.
Each form sends the field form_token. The form token is valid only with the cookie of the same prompt. The node also checks that the Origin header names the host of the request, so another site cannot send the form. The answer clears the cookie kinedb_link.
An accept after 5 minutes gets the error page with the code StateExpired: start the link again. A refuse after 5 minutes still works, because it writes nothing. If another user takes the account between the prompt and the accept, the accept shows the "in use" page.
When the user is not signed in
The link needs the start from a session. A user who is not signed in and logs in with a second provider gets the rules of a normal login (The match rule). If that user already has a link of another provider, the login by email is refused with the code UserAlreadyLinked. kinedb never adds a second link by email. The user signs in first, then starts the link.
A custom link page
A file link.html in the directory of the custom login pages replaces the built-in page of GET /auth/link (Custom login pages). The node checks the cookie kinedb_link first. If the check fails, the node shows the error page (or error.html), never link.html. link.html gets the fixed headers of a custom page.
The file is static: the node fills in no value. The page reads the values from GET /auth/link/prompt on the same host. The browser sends the cookie kinedb_link with the request. The answer is JSON:
| Field | What it holds |
|---|---|
provider | The name of the provider, for example partner. |
display_name | The display name of the provider. |
username | The name of the signed-in user. |
form_token | The value of the form field form_token. |
expires_at_ms | The end of the prompt, in milliseconds since 1970-01-01 UTC. |
The JSON holds no subject, no token and no link state. The answer has no CORS header, so another site cannot read it. When the cookie is missing, not valid or old, the answer is the error page with an error status (for example 400) instead of JSON.
The page then posts form_token to /auth/link/accept or to /auth/link/refuse. A small example: link.html and its script files/link.js (the CSP of a custom page allows only a script of the same host, no inline script).
html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Link an account</title>
<script src="files/link.js" defer></script>
</head>
<body>
<p id="question">Loading…</p>
<form method="post" action="/auth/link/accept">
<input type="hidden" name="form_token">
<button type="submit">Add the account</button>
</form>
<form method="post" action="/auth/link/refuse">
<input type="hidden" name="form_token">
<button type="submit">Do not add</button>
</form>
</body>
</html>js
// files/link.js
fetch('/auth/link/prompt', { credentials: 'same-origin' })
.then((r) => {
if (!r.ok) throw new Error('the prompt ended');
return r.json();
})
.then((p) => {
document.getElementById('question').textContent =
`Add the account of ${p.display_name} to the user ${p.username}?`;
for (const f of document.querySelectorAll('input[name="form_token"]')) {
f.value = p.form_token;
}
})
.catch(() => {
document.getElementById('question').textContent =
'This link request ended. Start the link again.';
});Use textContent, not innerHTML, for the values: the display name and the user name are text.
Settings for common providers
kinedb tests do not cover these settings. The tests use the test provider of kinedb only. Check each setting in your own tenant.
Microsoft Entra ID
- Register an application. Add the redirect address as a Web redirect.
- Create a client secret. Use it as
CLIENT SECRET. - Use
DISCOVERY 'https://login.microsoftonline.com/<tenant>/v2.0'. - Use
SUBJECT CLAIM 'oid'if you want the object id of the user as the subject. - Entra ID can send no
email_verifiedclaim. kinedb does not check it, so the first login by email matches. Entra ID can send noemailclaim for some accounts: then an admin adds the link withALTER USER … ADD FEDERATION PROVIDER, and the login uses the link. - Entra ID can send an overflow instead of many groups. kinedb refuses that token. Limit the groups that the application receives.
Google
- Create an OAuth client of the type Web application in the Google Cloud console. Add the redirect address.
- Use
DISCOVERY 'https://accounts.google.com'. - Google validates the emails of its accounts. Every Google account can sign in at the provider, so use
AUTO CREATE USERS ONonly when the role mappings and the grants of your cluster are safe for any account. - Google sends no groups. Set no
ROLES CLAIM, or give roles by hand. - Google has no
end_session_endpoint. kinedb cannot end the Google session of the browser. After a sign-out at/end_session, the next login sendsprompt=select_account, so Google shows its account chooser (Sign out).
Keycloak
- Create a client of the type OpenID Connect. Turn on client authentication, and add the redirect address.
- Use
DISCOVERY 'https://<host>/realms/<realm>'. - Add a mapper that puts the groups in a claim, for example
groups. SetROLES CLAIMto that claim name.
Limits and audit
kinedb limits the rate of /auth/start and /auth/callback for each source address. A caller over the limit gets the error page with the status 429 and a Retry-After header. The limit is on each node.
SHOW AUDIT records these actions. No row holds a token, an ID token, a code or a subject.
| Action | When |
|---|---|
FederatedLogin | A login succeeded, or was refused. The row has a reason code. |
FederatedLinkAdded | A first login added the link. |
FederatedUserCreated | A first login made the user (AUTO CREATE USERS ON). The actor and the object are the provider. |
FederatedRoleGranted | A login granted a role. |
FederatedRoleRevoked | A login revoked a role. |
FederationLinkPrompt | The callback of a link start showed the prompt, or refused it. |
FederationLinkAccepted | A user accepted a prompt, or the accept was refused. |
FederationLinkRefused | A user refused a prompt, or the refuse was refused. |
WebPasswordLoginOn | An admin switched the password login of the web on. The object is the setting. |
WebPasswordLoginOff | An admin switched the password login of the web off. The object is the setting. |
A password that the web login refuses because cluster.auth.web_password_login is false writes a Login row with the reason code PasswordLoginOff and an unknown actor.
The reason codes of a refused login are: StateInvalid, StateExpired, CodeExchangeFailed, IdTokenSignature, IdTokenAlgorithm, IdTokenIssuer, IdTokenAudience, IdTokenExpired, IdTokenNonce, GroupOverflow, EmailClaimMissing, NoUser, AutoCreateLimit, ProviderDisabled, HostRefused, ReturnRefused, SubjectInvalid, UserAlreadyLinked, RoleSyncFailed and Throttled. Rows of an older release can also hold EmailNotVerified and EmailDomainRefused; no login makes these codes now. For a role change by a login, SHOW AUDIT shows the actor provider:<name>.
The three link rows have the signed-in user as the actor and the provider as the object. A row before the node knows the user (for example a link cookie that is not valid) has an unknown actor. A link row can have these reason codes:
LinkInUse: the account at the provider is linked to another user.FormTokenInvalid: the form has no form token, or the form token of another prompt.UserAlreadyLinked: the user already has a link of this provider.TokenRejected: the session of the user ended before this step.StateInvalid,StateExpired,ProviderDisabled: as for a login.
An accepted link also writes the row AddFederationLink, with the user as the target. No link row holds a subject, a token, a link state or a form token.
Troubleshooting
If a user gets "log in again" every time, check GRANT QUERY AS and SHOW AUDIT WHERE action = 'query_as'.
A login that ends at the error page has a reason code in the audit trail. The page shows a fixed text only. Find the code with SHOW AUDIT and look it up in the table above.