Skip to content

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):

NameUnitDescription
DBKitto.Auth.DBUses a database table of users and optionally hashed passwords
DBServerKitto.Auth.DBServerAuthenticates users as DB server users.
OSDBKitto.Auth.OSDBUses Operating System authentication
TextFileKitto.Auth.TextFileUses a text file of users and optionally hashed passwords
JWTKitto.Auth.JWTWraps another authenticator, issues a self-contained signed JWT in an HttpOnly cookie that replaces the opaque session id. See JWT Authenticator 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.

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.

Default credentials can be specified also partially, in which case the Login dialog is still displayed with pre-filled fields.

Example (Config.yaml file):

yaml
Auth: DB
  Defaults:
    UserName: guest
    Password:

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.

yaml
Auth: DB
  DatabaseChoices: FireDAC_MSSQL, FireDAC_PostgreSQL, FireDAC_Firebird
  ...

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):

sql
-- 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 used when IsBCryptPassword: True is set follow the same pattern (one extra PASSWORD_B_HASH column).

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.

Auth: JWT is a wrapper authenticator: it delegates the actual credential check to an Inner authenticator (DB, TextFile, OSDB, custom — anything registered in TKAuthenticatorRegistry) and, on a successful login, signs a JWT carrying the user identity plus a few session-bound claims and writes it to a single HttpOnly cookie. 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.

yaml
Auth: JWT
  Inner: DB
    IsClearPassword: False
    IsPassepartoutEnabled: True
    PassepartoutPassword: password
    .Defaults: { UserName: administrator, Password: password }
  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: True
    IncludeDB: True
    IncludeDisplayName: True
    IncludeLanguage: True
    # Note: kx_acl is auto-included when AccessControl: JWT — no flag here

Cookies emitted under Auth: JWT

CookieSet byPurpose
kx_tokenserver (login + sliding refresh)The JWT itself: identity + claims, HttpOnly, Secure, SameSite=Lax, path-scoped on AppPath
kx_swclient JavaScript (Home/Templates/_Page.html)Screen size for responsive HomeView selection (no relation to auth)

The legacy session-id cookie named after AppName (e.g. MyAppX) and the multi-database kx_db cookie are not emitted under Auth: JWT: the JWT's sid and db claims carry the same information and live within the single signed envelope. After upgrading from a non-JWT build, the legacy cookies present in the browser are cleared on the next session-end / login POST.

Under non-JWT auth, kx_db was a 30-day cookie that pre-selected the last picked database environment on the login form. Under Auth: JWT the db claim lives inside kx_token (typically 1-hour lifetime, slid every active request), so when the token finally expires the next login form pre-selects DefaultDatabaseName instead of the last picked one. During an active session there is no observable difference: the JWT slides on every request and the chosen database stays.

Implementation note: per-thread JWT context cache

TKJWTAuthenticator.AuthorizeRequest validates the cookie once per request and caches the parsed TKJWTContext for downstream consumers (TKJWTAccessController reads kx_acl from it). The cache is implemented as a unit-level TObjectDictionary<TThreadID, TKJWTContextHolder> (in Source/Kitto.Auth.JWT.pas) protected by a TCriticalSection, owned by the unit so its finalization deterministically frees the holders. A threadvar of record was rejected because the Delphi runtime does not release managed members (strings, dynamic arrays) of records when a worker thread exits, and the Indy thread pool keeps ~20 workers alive for the server lifetime.

Inner authenticator override pattern

All the SQL override keys documented above (ReadUserCommandText, SetPasswordCommandText, ResetPasswordCommandText, RegisterNewUserCommandText) are still read by the Inner authenticator, so put them under Auth/Inner (not directly under Auth):

yaml
Auth: JWT
  Inner: DB
    ReadUserCommandText: |
      select USER_NAME, PASSWORD_HASH, ...
      from MY_LEGACY_USERS where ...
    SetPasswordCommandText: |
      ...
  SigningAlgorithm: HS256
  ...

The same applies to DatabaseChoices and any other key the Inner consumes: TKAuthenticator.EffectiveConfigNode is overridden in TKJWTAuthenticator to expose the Inner's config to callers, so the framework keeps reading the right keys without code changes.

Released under Apache License, Version 2.0.