User authentication
User authentication in Kittox is optional. If an application needs to authenticate users, an authenticator among a selection of predefined ones can be enabled, or a custom authentication can be developed.
In order to enable an authenticator you need to:
- Specify its name and settings in the config file.
- Include/use the relevant unit in your project.
Standard authenticators (Auth: parameter values):
| Name | Unit | Description |
|---|---|---|
| DB | Kitto.Auth.DB | Uses a database table of users and optionally hashed passwords |
| DBCrypt | Kitto.Auth.DB | Same user table as DB, but passwords are bcrypt-hashed in the PASSWORD_B_HASH column. |
| DBServer | Kitto.Auth.DBServer | Authenticates users as DB server users. Requires a Connection block that reads the typed credentials through the %Auth:...% macros — see Auth: DBServer. |
| OSDB | Kitto.Auth.OSDB | Uses Operating System authentication |
| TextFile | Kitto.Auth.TextFile | Uses a text file of users and optionally hashed passwords. See Auth: TextFile. |
| LDAP | Kitto.Auth.LDAP | Validates users with a simple bind against an LDAP directory (Active Directory or generic LDAP); no local user table. See Auth: LDAP. |
The JWT envelope is not an authenticator of its own: it is an optional JWT: sub-block you add under any of the authenticators above (unit Kitto.Auth.JWT), which issues a self-contained signed JWT in an HttpOnly cookie that replaces the opaque session id. See JWT envelope for the full reference. |
Other authentication methods will be added. Plus, it's easy to tweak existing authenticators and create inherited modified versions.
By default, the Null authenticator is used, which does not require user authentication. It accepts every request as an authenticated anonymous session, and refuses password change, password reset and PIN/QR enrolment: there is no account for it to write to.
Startup checks
Two configuration mistakes stop the application at startup, with the reason in the log:
| Configuration | What happens |
|---|---|
Auth: names a class that nobody registered — a typo, or the unit missing from UseKitto.pas | The application refuses to start, and the message lists the class ids actually registered. Only Null is registered by the core: DB, DBCrypt, TextFile, DBServer, OSDB and LDAP each need their unit in the project's UseKitto.pas. (JWT is not an authenticator class — it is an optional envelope added under one of these.) |
Auth: DBServer whose Connection block has fixed credentials | The application refuses to start. This authenticator validates a login by opening the connection with the supplied credentials, so a Connection with its own user name and password checks nothing and accepts everyone. See Auth: DBServer. |
The Login dialog
When authentication is enabled, a Kittox application displays a Login dialog at startup, unless all required credentials are specified by other means.
Credentials can vary depending on the authenticator, but usually they are UserName and Password, both of type String.
The other available means of specifying credentials, needed in special cases, are basically two:
- In the Config file, inside a Auth/Defaults subnode.
- In the URL, as standard URL parameters.
What credentials in the URL do, and do not, do
?UserName=…&Password=… on the application root authenticates the user — the identity is resolved and the home renders. But it does not go through the kx/login endpoint, so no token is issued: with a JWT envelope no token cookie is set, and under AccessControl: JWT there is no kx_acl claim, which is closed-world. The result is a home page where every view is denied and only Logout is visible — an apparently broken session rather than a working login. F5 also repeats the credentials, so the login page never returns.
The credentials are also exposed where a URL goes: the address bar, the browser history, and the Referer header sent to any external link on the page. They do not reach the log, which records the path without the query string.
Use it for what it is meant for — a deliberate deep link, a kiosk, an integration — and prefer Auth/Defaults when the goal is simply to pre-fill or preset credentials. The application itself never produces such a URL: see How the form is submitted.
Default credentials can be specified also partially, in which case the Login dialog is still displayed with pre-filled fields.
Example (Config.yaml file):
Auth: DB
Defaults:
UserName: guest
Password:The passepartout (master password)
IsPassepartoutEnabled: True plus a PassepartoutPassword gives you a master password: it authenticates as any user name that exists, whatever that user's own password is. It is meant for technical intervention — entering as a specific user to reproduce what they see — and it is supported deliberately.
Auth: DB
IsPassepartoutEnabled: True
PassepartoutPassword: <a long, high-entropy secret>| Passepartout | |
|---|---|
| user exists, with a password | accepted |
| user exists, password column empty or NULL | accepted — the master password is the credential, not a blank one |
| user not in the table / not in the user list file | refused: it impersonates, it does not create |
user disabled (IS_ACTIVE = False, or a # in the user list file) | refused |
PassepartoutPassword left empty | ignored (see below) |
It is compared with the password as typed, never with its hash, on both DB and TextFile — so the value in Config.yaml is the master password in clear, not its MD5.
An enabled passepartout with no password configured is ignored
IsPassepartoutEnabled: True with an empty PassepartoutPassword is ignored — otherwise a blank password would log in as any existing user, the opposite of a master password. The misconfiguration is logged the first time somebody hits it; configuring a real value restores the full behaviour.
Two things the framework does not do for you
The value is read with Config.GetString, so macros are not expanded: today it can only be a literal in the configuration file. And a passepartout login sets an IsPassepartoutAuthentication flag in the auth data but nothing writes it to the log, so in the log an intervention is indistinguishable from the user's own login.
Database environment choice
When the application is configured with multiple databases under the Databases node, an Auth/DatabaseChoices setting lets the user pick which database to authenticate against from a drop-down on the login page. The chosen database becomes the active one for the entire session.
Auth: DB
DatabaseChoices: FireDAC_MSSQL, FireDAC_PostgreSQL, FireDAC_Firebird
...A listed name is shown only if that database is actually usable — its Databases/<Name> block is defined and the DB adapter it references (FD, ODAC, DBX, ADO, …) is compiled into the build (its EF.DB.* unit is in UseKitto.pas). Choices that fail either check are silently skipped, so you can permanently list an optional back-end (e.g. ODAC_Oracle) and it appears only once enabled. See the Login docs for details.
See the Environment / database choice section of the Login controller documentation for the full description (combo placement, default selection rules, persistence cookie, %Auth:DatabaseName% macro).
Default SQL queries
When Auth: DB is enabled, the framework uses a built-in set of SQL queries against the KITTO_USERS table. They are dialect-agnostic by design: each IS_ACTIVE and MUST_CHANGE_PASSWORD literal is written as the %DB.TRUE% / %DB.FALSE% macro, so the same query works whether those columns are declared as BIT (SQL Server), smallint (Firebird ≤ 2.5, Oracle), or native boolean (PostgreSQL, Firebird 3+).
Default queries (from Source/Kitto.Auth.DB.pas):
-- ReadUserCommandText
select USER_NAME, PASSWORD_HASH, EMAIL_ADDRESS, MUST_CHANGE_PASSWORD
from KITTO_USERS
where IS_ACTIVE = %DB.TRUE% and USER_NAME = :USER_NAME
-- SetPasswordCommandText
update KITTO_USERS
set PASSWORD_HASH = :PASSWORD_HASH, MUST_CHANGE_PASSWORD = %DB.FALSE%
where IS_ACTIVE = %DB.TRUE% and USER_NAME = :USER_NAME
-- ResetPasswordCommandText
update KITTO_USERS
set PASSWORD_HASH = :PASSWORD_HASH, MUST_CHANGE_PASSWORD = %DB.TRUE%
where IS_ACTIVE = %DB.TRUE% and EMAIL_ADDRESS = :EMAIL_ADDRESS AND USER_NAME = :USER_NAME
-- RegisterNewUserCommandText
insert into KITTO_USERS (USER_NAME, PASSWORD_HASH, IS_ACTIVE, MUST_CHANGE_PASSWORD, EMAIL_ADDRESS)
VALUES (:USER_NAME, :PASSWORD_HASH, %DB.TRUE%, %DB.TRUE%, :EMAIL_ADDRESS)The bcrypt-aware variants use the PASSWORD_B_HASH column and are enabled by choosing the dedicated DBCrypt authenticator (Auth: DBCrypt, optionally with a BCryptCostValue node), not by a flag on the DB authenticator.
DBCrypt requires PASSWORD_B_HASH in the read query
The authenticator recognises a bcrypt password by finding a non-empty PASSWORD_B_HASH in the user record, so an overridden ReadUserCommandText must select that column. When it is absent or empty the user is treated as a legacy one and validated against PASSWORD_HASH, and the first successful login re-hashes the password into PASSWORD_B_HASH (cost from BCryptCostValue).
You can override any of these queries explicitly in Config.yaml under the Auth node — useful when you have a legacy user table with different column names. See Custom User Table and Custom User Columns for examples.
Changing a password
The ChangePassword view writes through the authenticator, so what it can do depends on which one is configured:
| Authenticator | Change from the application |
|---|---|
DB | yes — SetPasswordCommandText |
DBCrypt | yes — the new password is bcrypt-hashed before being stored |
OSDB | yes — it keeps no hash of its own, and the "must differ" check is skipped when no password is stored |
TextFile | no — the user list file is never rewritten |
LDAP | no — passwords live in the directory (see above) |
DBServer | no — the accounts are database-server users; changing one is a DBA job |
Null | no — there is no account |
A JWT envelope does not change this column: password change depends on the underlying authenticator (the row above for DB, LDAP, …), and the token layer is orthogonal to it.
Which row applies is not a documentation convention: it is the authenticator's own answer to SupportsPasswordChange, and both the ChangePassword view and the kx/changepassword endpoint ask before writing. Where the answer is no the user gets an explicit refusal, and no password is accepted and silently discarded.
Writing your own authenticator
SupportsPasswordChange defaults to True, so an authenticator that overrides SetPassword needs no change. Override it to False when the credential lives somewhere you do not write to — an identity provider, a directory, the operating system.
Note that MUST_CHANGE_PASSWORD is read by ReadUserCommandText and cleared by SetPasswordCommandText: a user whose flag is set is sent to the change-password screen as the home page, and gets in only once the password has been changed. ResetPasswordCommandText sets the flag again, which is what makes a reset-by-mail temporary password temporary.
Imposed steps are enforced server-side
A user who must still change the password, or accept the privacy terms, is authenticated. The authorization filter enforces the two flags on every request, so a client that skips the home page does not skip the step: while MustChangePassword or MustConfirmAccess is pending, any endpoint not belonging to the required view answers 404, and the reason is logged.
| Pending flag | Required view name |
|---|---|
MustChangePassword | ChangePassword |
MustConfirmAccess | ConfirmAccess |
The names are matched literally against the view the request resolved to, so the view that renders the dialog must be named exactly like that. Exempt from the gate: the application root (it is what renders the dialog), endpoints marked as anonymous, views declared public, and every endpoint scoped to the required view itself — so the dialog can load and save. If the flag is raised and the application declares no view with that name, the request fails with a message that says which view is missing, instead of an empty page.
Password strength: the ValidatePassword node
Strength rules are enforced by the authenticator, inside SetPassword. DBCrypt reads them from a ValidatePassword node of its own configuration, and an application authenticator can do the same:
Auth: DBCrypt
ValidatePassword:
RegEx: '^(?=.*[A-Za-z])(?=.*\d)[ -~]{8,63}$'
Message: At least 8 characters, with letters and digitsRegEx defaults to ^[ -~]{8,63}$ — any printable character, from 8 to 63 — and Message is what the user sees when the password is rejected. The same expression also constrains the passwords produced by GenerateRandomPassword, so a temporary password sent by mail always satisfies the rules the user will be held to.
Reading the node from code
ValidatePassword belongs to the authenticator and sits directly under Auth/ (a JWT envelope does not move it — only the token keys go under Auth/JWT/). Application code can read it by absolute path or, more robustly, through TKConfig.Instance.Authenticator.EffectiveConfigNode.FindNode('ValidatePassword').
PIN login (LoginType: PIN)
Auth: DB (and DBCrypt) can replace the password with a six-digit time-based one-time code (TOTP, the kind produced by Google Authenticator, Microsoft Authenticator, FreeOTP…):
Auth: DB
LoginType: PINThe login form still asks for a user name and a second field, but the value typed there is validated as a numeric TOTP code instead of the password: with LoginType: PIN the password is not checked at all. The code is verified against a shared secret stored in the user record, in a SECRET_CODE column, which your ReadUserCommandText must select:
select USER_NAME, PASSWORD_HASH, EMAIL_ADDRESS, MUST_CHANGE_PASSWORD, SECRET_CODE
from KITTO_USERS
where IS_ACTIVE = %DB.TRUE% and USER_NAME = :USER_NAMEFill SECRET_CODE with a random per-user value (base32, the alphabet authenticator apps expect), generated at enrolment and never derived from anything public. QRGenerate produces the enrolment QR code (otpauth://totp/<AppTitle>?secret=<SECRET_CODE>) from that same column, so the app and the server share exactly one secret per user.
Both login and enrolment refuse without a per-user secret
If the query does not select SECRET_CODE, or the column is empty for that user, the PIN login is refused and the QR code is not generated, with the reason in the log at high level.
There is no fallback secret derived from the user name: the user name is public, and since this branch replaces the password check, such a fallback would let anyone who knows a user name compute a valid code and log in with no credential at all. Enrol every user with a real random secret.
ValidateTOPT accepts a ±window around the current 30-second step (default 4 → about ±2 minutes), which tolerates clock drift between the server and the phone.
Auth: DBServer
Auth: DBServer has no user table of its own: it authenticates a login by opening a database connection with the credentials the user typed. The accounts are the DBMS's own accounts. It follows that the Connection block must read those credentials, through the %Auth:UserName% / %Auth:Password% macros:
Auth: DBServer
DatabaseName: MyDB # optional; defaults to the default database
Databases:
MyDB:
Connection:
DriverID: MSSQL
Server: localhost
Database: MyDatabase
User_Name: '%Auth:UserName%' # required
Password: '%Auth:Password%' # requiredA Connection with fixed credentials authenticates everyone
If the Connection block carries its own user name and password, opening it succeeds no matter what the user typed — so every login is accepted, including one with a wrong password. The authenticator checks the block at startup and refuses to start unless at least one of its values contains %Auth:, logging the reason.
Password change, password reset and PIN/QR enrolment are not supported: altering a database-server account is a DBA operation, not something the application does.
Returning to the home page
Loading the application root ends the current session: the user is logged out and gets the login page. This is deliberate, and it is what Kitto1 did — a full page load starts a session from scratch, so pressing F5 on the root takes the user back to the login rather than silently resuming.
This matters when the application itself wants to bring an already authenticated user back to the home page: after login, after a language change, or after an operation such as a privacy confirmation. Do not redirect to the root by hand — call:
TKWebApplication.Current.ReloadOrDisplayHomeView;It marks the redirect as an application-issued reload (the ReloadingHome session flag), so the root serves the home page instead of logging the user out, and it applies a pending language change on the way. A rule that redirects to the root any other way will log the user out.
Reloading from the client
The same rule applies to a reload started in the browser, and there the flag cannot be raised in advance: by the time the reload reaches the root there is no earlier request to raise it in. A bare window.location.reload() on the home is therefore indistinguishable from a fresh page load, and signs the user out.
Client code that reloads to recover — not to end the session — must announce itself first:
kxReloadHome(); // defined in kxgrid.jsIt POSTs to kx/reloadhome, which raises the flag, and only then reloads. The framework uses it for the [Reset] and [Retry] buttons of the standard error dialog on both channels; use it in application JavaScript for the same reason. The endpoint raises the flag only for an already authenticated session, so it grants nothing by itself, and the flag is one-shot: a user pressing F5 afterwards still signs out, JWT cookie included.
| You want | Use |
|---|---|
| Bring an authenticated user back to the home (server side) | TKWebApplication.Current.ReloadOrDisplayHomeView |
| Reload the page from JavaScript, keeping the session | kxReloadHome() |
| End the session | TKWebApplication.Current.Logout, or just let the reload hit the root |
Full page loads, not partial updates
Both the login and this reload go through a complete page load, because the home page is rebuilt for the authenticated user — menus, theme and language included. Inside the application, ordinary navigation swaps content via HTMX and never touches the root.
No login with an empty password
A password that is empty on either side of the comparison is refused, and this is enforced wherever the password is the credential being checked:
| Authenticator | An empty typed password | A stored password that is empty or NULL |
|---|---|---|
DB, DBCrypt | refused | refused — the account cannot be entered at all |
TextFile | refused | refused (see Auth: TextFile) |
DBServer | refused before the connection is opened | not applicable — the account lives in the DBMS |
LDAP | refused before the bind | not applicable — the account lives in the directory |
OSDB, authenticating the OS user | accepted — see below | irrelevant: the password is not the credential |
OSDB, when the OS user is not an application user | refused | refused |
Null | there is no authentication to speak of | — |
The rule is about the password, not about the account: a passepartout login is unaffected, because what the operator types is the master password, not a blank one. See The passepartout.
No blank-password login for a passwordless account
An active DB / DBCrypt user row whose password column is empty or NULL cannot be matched by an empty typed password. Rows like that are not exotic — an application form that creates users without a password field produces them, and this documentation itself describes the state ("when no password is stored"). Such an account cannot be logged into: its owner gets in through the password reset instead.
Why OSDB is different, and how your own authenticator declares it
Auth: OSDB authenticates the operating-system user: the OS has already verified the identity, the password field carries nothing by design, and refusing an empty password there would break a correct configuration. The distinction is therefore not "is the password empty" but "is the password the credential this authenticator verifies", and it is a method:
/// True when the credential this authenticator verifies IS the password stored
/// in the user record. Default True.
function IsPasswordTheStoredCredential: Boolean; virtual;TKOSDBAuthenticator returns not IsUsingSystemUserName, so the exemption applies only while it is authenticating the OS user; as soon as the OS user is not an application user it behaves like Auth: DB and the refusal applies again. An authenticator of your own that delegates the check elsewhere — an identity provider, a token, a device — overrides it to False; one that compares a stored password leaves it alone.
The password-reset flow
This is how a user with no usable password gets back in, and it is unaffected by the rules above: it does not go through authentication at all.
- The user asks for a reset from the login page, giving user name + e-mail address.
ResetPasswordCommandTextwrites a freshly generated temporary password on the row that matches both, and raisesMUST_CHANGE_PASSWORD. A wrong e-mail address fails with user name and email address not found.- The application mails the temporary password to the user, by overriding
AfterResetPassword— the framework writes the password, it does not send mail. - The user logs in with it and, the flag being set, lands on the imposed change-password screen and cannot go anywhere else until the password has been changed.
The temporary password comes from GenerateRandomPassword: 8 characters over [2-9A-Z] for DB (GetRandomString(8, '01') — the second argument excludes 0 and 1, it is not the alphabet), and 8 alphanumerics plus a special character for DBCrypt, re-generated until it satisfies the ValidatePassword expression, so a temporary password always satisfies the rules the user will then be held to.
Auth: TextFile
Auth: TextFile (unit Kitto.Auth.TextFile) reads a plain text file of user name=password lines. The file is re-read at every authentication, so a change takes effect without a restart.
Auth: TextFile
FileName: '%HOME_PATH%FileAuthenticator.txt' # this is the default
IsClearPassword: False # False = the file holds MD5 hashes
IsPassepartoutEnabled: False
PassepartoutPassword: ''# a line starting with # is ignored, which is how a user is disabled
alice=e52d98c459819a11775936d8dfbb7929
bob=e54cfb3714f76cedd4b27889e1f6a174
#carol=ee41750a2cd87a6fddd89d8760a5345eWith IsClearPassword: False (the default) the file holds the MD5 hash of each password; with True it holds the password itself. The user name is matched case-insensitively; the password is not.
Rules the authenticator enforces (the same ones Auth: DB applies to its user table):
| Situation | Result |
|---|---|
| Empty password | refused before any comparison |
| User name not in the file | refused |
Line with nothing after the = | no password can authenticate it, and the broken user list is logged at high level (a passepartout still can — the user is listed) |
User disabled with a leading # | refused (the line is not loaded at all) |
PassepartoutPassword matches | accepted for any user listed in the file, including one whose line carries no password — see The passepartout |
IsPassepartoutEnabled: True with an empty PassepartoutPassword | the passepartout is ignored, not matched |
No login with an empty password
GetStringHash('') returns '' (not the MD5 of an empty string), and TStrings.Values[] returns '' for a name that is not in the list — so a naive comparison of an empty typed password against an empty stored one would compare two empty strings and succeed, in both IsClearPassword modes, for any user name at all: one absent from the file, one made of spaces, one whose line had nothing after the =, and — worst — one disabled with a leading #, since the comment line is stripped and the user becomes "absent". Kittoˣ therefore refuses an empty password on both sides of the comparison. (The required attribute on the password box is client-side only and does not stop a scripted request.)
With IsClearPassword: False the passepartout is compared with the password as typed — which is what Auth: DB does — so put the master password in PassepartoutPassword in clear.
Password change, password reset and PIN/QR enrolment are not supported: the user list is read-only for the application (see Changing a password).
Auth: LDAP
Auth: LDAP (unit Kitto.Auth.LDAP) authenticates a user with a simple bind against an LDAP directory using the native Windows LDAP API (wldap32.dll). It needs no local user table: the directory itself confirms that the user exists and that the password is valid. After a successful bind, when a SearchBase is configured, the account's display attributes (name, e-mail, optionally group membership) are read and stored into the auth data, so they are available through the %Auth:...% macros and to the access controller.
It requires only UserName + Password (like the other classic authenticators). Add Kitto.Auth.LDAP to your project's UseKitto.pas.
Active Directory
Users log in with the familiar DOMAIN\user form (or a UPN, user@domain). Set DefaultDomain so they can type just the user name:
Auth: LDAP
Host: dc01.corp.local # domain controller (host name or IP)
Port: 389 # 389 plain, 636 with UseSSL
UseSSL: False # True = LDAPS (recommended in production)
DefaultDomain: CORP # prepended to a bare user name -> CORP\user
SearchBase: DC=corp,DC=local # base DN for reading the user attributes
# SearchFilter: (sAMAccountName=%s) # this is the default
Attributes: # override only if your schema differs (AD defaults shown)
Email: mail
FirstName: givenName
LastName: sn
FullName: displayName
Groups: memberOfGeneric (non-AD) LDAP
For a directory that binds by distinguished name (OpenLDAP, public test servers, …), set BindDNTemplate so the user types only a short name; the full DN is built from it:
Auth: LDAP
Host: ldap.example.com
Port: 389
BindDNTemplate: uid=%s,dc=example,dc=com # the user types just "jsmith"
SearchBase: dc=example,dc=com
SearchFilter: (uid=%s)
Attributes:
Email: mail
FullName: cn
LastName: snThe KEmployee live demo uses exactly this setup against the public ldap.forumsys.com test directory.
Parameters
| Parameter | Description |
|---|---|
Host | LDAP server host name or IP (required). |
Port | TCP port. Default 389, or 636 when UseSSL: True. |
UseSSL | True opens an LDAPS connection. Recommended in production — a plain simple bind sends the password in clear over the wire. |
DefaultDomain | NetBIOS domain prepended to a bare user name (user → DOMAIN\user). Ignored when BindDNTemplate is set. |
BindDNTemplate | Template with a single %s placeholder that builds the full bind DN from a short user name, e.g. uid=%s,dc=example,dc=com. Takes precedence over the DOMAIN\user / DefaultDomain handling. |
SearchBase | Base DN used to read the user's attributes after the bind. Omit to authenticate only (no attribute lookup). |
SearchFilter | LDAP filter with one %s placeholder for the user name. Default (sAMAccountName=%s). |
Attributes/Email · FirstName · LastName · FullName · Groups | Directory attributes mapped into the auth data as EMAIL_ADDRESS, FIRST_NAME, LAST_NAME, FULL_NAME, MEMBER_OF. Defaults: mail, givenName, sn, displayName, memberOf. |
Passwords are managed by the directory, so changing a password from the application (SetPassword), ResetPassword and PIN/QR generation are not supported: the authenticator answers False to SupportsPasswordChange, so the ChangePassword view refuses with an explicit message instead of reporting success without writing anything. An empty password is rejected before the bind (some servers would otherwise accept it as an anonymous bind). Because the JWT envelope works under any authenticator, adding a JWT: block to Auth: LDAP gives you an AD/LDAP login inside a signed-cookie envelope — this is what KEmployee does. Deploying under IIS/Apache needs no special configuration: the bind is an outbound connection from the app to the directory.
The JWT envelope (signed-cookie tokens)
Any of the authenticators above can carry an optional JWT: sub-block: on a successful login the framework signs a JWT with the user identity plus a few session-bound claims and writes it to a single HttpOnly cookie, and every subsequent request validates the cookie's signature and rehydrates the session from the verified claims, instead of relying on a server-side session-id cookie. The credential check stays with the authenticator; the token layer is orthogonal.
Auth: DB
IsClearPassword: False
IsPassepartoutEnabled: True
PassepartoutPassword: password
.Defaults:
UserName: administrator
Password: password
JWT:
SigningAlgorithm: HS256 # HS256/384/512, RS*, ES*
SigningKey: env:KX_JWT_KEY_MYAPP # env:VAR | file:/path | inline
Issuer: MyAppX
Audience: kx-app
TokenLifetime: 3600 # seconds
SlidingThreshold: 600 # re-issue cookie when (exp - now) < this
ClockSkew: 60 # seconds tolerance
Claims:
IncludeRoles: False
IncludeDB: True
IncludeDisplayName: True
IncludeLanguage: True
# Note: kx_acl is auto-included when AccessControl: JWT — no flag hereThe SQL override keys (ReadUserCommandText, SetPasswordCommandText, ResetPasswordCommandText, RegisterNewUserCommandText), DatabaseChoices, ValidatePassword and every other authenticator key stay directly under Auth/ — only the token envelope keys (SigningAlgorithm, Issuer, Cookie:, Claims:, …) go under Auth/JWT/.
See JWT envelope for the full reference: opt-in from UseKitto.pas, the signing-key registry, cookie attributes, sliding expiration, the kx_acl access-control claim, key rotation and security notes.
