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
LDAPKitto.Auth.LDAPValidates users with a simple bind against an LDAP directory (Active Directory or generic LDAP); no local user table. See Auth: LDAP.
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
  ...

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

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 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.

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

yaml
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: memberOf

Generic (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:

yaml
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: sn

The KEmployee live demo uses exactly this setup against the public ldap.forumsys.com test directory.

Parameters

ParameterDescription
HostLDAP server host name or IP (required).
PortTCP port. Default 389, or 636 when UseSSL: True.
UseSSLTrue opens an LDAPS connection. Recommended in production — a plain simple bind sends the password in clear over the wire.
DefaultDomainNetBIOS domain prepended to a bare user name (userDOMAIN\user). Ignored when BindDNTemplate is set.
BindDNTemplateTemplate 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.
SearchBaseBase DN used to read the user's attributes after the bind. Omit to authenticate only (no attribute lookup).
SearchFilterLDAP filter with one %s placeholder for the user name. Default (sAMAccountName=%s).
Attributes/Email · FirstName · LastName · FullName · GroupsDirectory 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 ResetPassword and PIN/QR generation are not supported. An empty password is rejected before the bind (some servers would otherwise accept it as an anonymous bind). Because Auth: JWT can wrap any inner authenticator, Inner: 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.

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.