Table of Contents

User Management

Overview

TMS Sphinx provides a comprehensive set of tools for managing user authentication and authorization in Delphi applications. Central to this functionality is the IUserManager interface, which offers a variety of methods for handling user-related operations such as creating, updating, and deleting users, as well as managing advanced security features like two-factor authentication.

The ISphinxContext Interface

ISphinxContext is an interface that manages the lifecycle of user-related objects. It ensures that user objects are properly maintained and destroyed when no longer needed. Obtaining a user manager through a context is recommended because it guarantees the correct management of user object lifetimes.

Retrieving the Sphinx Context

To retrieve the ISphinxContext, you use the TSphinxServer.​Create​Context method of the TSphinxServer component. This method provides a ready-to-use context for managing users.

Example:

procedure TForm1.SetupSphinxContext;
var
  Context: ISphinxContext;
begin
  // Retrieve the Sphinx context from the SphinxServer component
  Context := SphinxServer1.CreateContext;
end;

The IUserManager Interface

The IUserManager interface provides a variety of methods for managing users in the database. Here is a detailed list of the main tasks you can accomplish using IUserManager.

Creating a User

You can create a user with or without a password using the IUserManager.​Create​User method.

Example:

var
  User: TUser;
begin
  User := Context.UserManager.CreateUserInstance;
  User.UserName := 'newuser';
  User.Email := 'newuser@example.com';
  Context.UserManager.CreateUser(User, 'password123');
end;

Updating a User

To update user details, use the IUserManager.​Update​User method.

Example:

User.Email := 'newemail@example.com';
Context.UserManager.UpdateUser(User);

Deleting a User

To delete a user from the database, use the IUserManager.​Delete​User method.

Example:

Context.UserManager.DeleteUser(User);

Finding Users

You can find users by their ID, username, email, or phone number using methods such as IUserManager.​Find​ById, IUserManager.​Find​ByName, IUserManager.​Find​ByEmail, and IUserManager.​Find​ByPhone​Number.

Example:

User := Context.UserManager.FindByName('newuser');
if User <> nil then
  ShowMessage('User found: ' + User.Email);

Checking Password

To check if a provided password is correct for a user, use the IUserManager.​Check​Password method.

Example:

if Context.UserManager.CheckPassword(User, 'password123') then
  ShowMessage('Password is correct');

Email and Phone Number Confirmation

You can generate confirmation tokens and confirm email and phone numbers using the respective methods.

Example for Email Confirmation:

var
  Token: string;
begin
  Token := Context.UserManager.GenerateEmailConfirmationToken(User);
  // Send token to user via email and later confirm
  Context.UserManager.ConfirmEmail(User, Token);
end;

Example for Phone Number Confirmation:

var
  Token: string;
begin
  Token := Context.UserManager.GeneratePhoneNumberConfirmationToken(User);
  // Send token to user via SMS and later confirm
  Context.UserManager.ConfirmPhoneNumber(User, Token);
end;

Password Management

To manage user passwords, use methods for generating reset tokens, changing passwords, and adding passwords.

Example for Resetting Password:

var
  ResetToken: string;
  NewPasswordToken: string;
begin
  ResetToken := Context.UserManager.GeneratePasswordResetToken(User);
  // Send reset token to user and later exchange it for a change password token
  NewPasswordToken := Context.UserManager.BeginResetPassword(User, ResetToken);
  // Change the password using the new password token
  Context.UserManager.ChangePassword(User, NewPasswordToken, 'newpassword123');
end;

Custom Password Hashing

By default, Sphinx hashes user passwords with a built-in, salted SHA-256 algorithm (TDefaultPassword​Hasher). If you need a different algorithm — for example PBKDF2, bcrypt, argon2, or one based on the TMS Cryptography Pack — you can plug in your own implementation without changing anything else in Sphinx.

All password hashing goes through the IPasswordHasher interface, which has just two methods:

  • HashPassword — receives the plain-text password and returns the string to store (Sphinx saves it in TUser.PasswordHash).
  • VerifyHashedPassword — receives the stored hash and a provided password, and returns whether they match.

To use a custom hasher, implement the interface and assign an instance to the TSphinxConfig.​Password​Hasher property. When left unassigned, Sphinx keeps using its built-in default, so existing applications are unaffected.

type
  TMyPasswordHasher = class(TInterfacedObject, IPasswordHasher)
  public
    function HashPassword(User: TUser; const Password: string): string;
    function VerifyHashedPassword(User: TUser; const HashedPassword: string;
      const ProvidedPassword: string): Boolean;
  end;

function TMyPasswordHasher.HashPassword(User: TUser; const Password: string): string;
begin
  // Replace with your algorithm. Store everything the verifier needs (e.g. algorithm id,
  // parameters and salt) inside the returned string so it can be self-describing.
  Result := MyAlgorithm.Hash(Password);
end;

function TMyPasswordHasher.VerifyHashedPassword(User: TUser;
  const HashedPassword, ProvidedPassword: string): Boolean;
begin
  Result := MyAlgorithm.Verify(HashedPassword, ProvidedPassword);
end;

Then assign it once, when you configure the server:

SphinxConfig1.PasswordHasher := TMyPasswordHasher.Create;
Note

The hasher instance is shared across concurrent requests, so it must be stateless / thread-safe (as the built-in one is).

Keeping existing users' passwords working

If your database already contains users whose passwords were hashed by the built-in default hasher, be aware that those stored hashes are in the default's format. If your custom VerifyHashedPassword does not understand that format, those users will no longer be able to sign in.

The recommended pattern is to let your custom hasher hash new passwords with your algorithm, but still verify legacy hashes by delegating to TDefaultPassword​Hasher. Because your algorithm's stored hashes look different from the default's, you can tell them apart and route verification accordingly:

type
  TMyPasswordHasher = class(TInterfacedObject, IPasswordHasher)
  strict private
    const NewPrefix = 'myalg$'; // marks hashes produced by this hasher
    FDefault: IPasswordHasher;
  public
    constructor Create;
    function HashPassword(User: TUser; const Password: string): string;
    function VerifyHashedPassword(User: TUser; const HashedPassword: string;
      const ProvidedPassword: string): Boolean;
  end;

constructor TMyPasswordHasher.Create;
begin
  inherited Create;
  // Used to verify passwords that were hashed before this hasher was installed.
  FDefault := TDefaultPasswordHasher.Create;
end;

function TMyPasswordHasher.HashPassword(User: TUser; const Password: string): string;
begin
  // New passwords always use the new algorithm, tagged so they can be recognized later.
  Result := NewPrefix + MyAlgorithm.Hash(Password);
end;

function TMyPasswordHasher.VerifyHashedPassword(User: TUser;
  const HashedPassword, ProvidedPassword: string): Boolean;
begin
  if StartsStr(NewPrefix, HashedPassword) then
    Result := MyAlgorithm.Verify(Copy(HashedPassword, Length(NewPrefix) + 1), ProvidedPassword)
  else
    // Legacy hash produced by the built-in default hasher: keep verifying it as before.
    Result := FDefault.VerifyHashedPassword(User, HashedPassword, ProvidedPassword);
end;

With this approach existing users keep signing in seamlessly, while every newly created or changed password is stored with your algorithm. Over time, as users are created and change their passwords, the database naturally migrates to the new format.

Two-Factor Authentication

You can force users to enable two-factor authentication (2fa) and use it to login. You can do this by setting TLoginOptions.​Require​TwoFactor property to true:

SphinxConfig1.LoginOptions.RequireTwoFactor := True;

When 2fa is required for the user and the user tries to login with a simple password, the login application will also display a QR Code for the user to enable 2fa via authentication app (e.g., Google Authenticator or similar), and ask for the code. If the user provides the correct code, 2fa will be enabled and then authenticator code will be needed whenever the user tries to login in future.

If you want to require 2fa only for a few selected users, you can use the IUserManager.​Set​TwoFactor​Required method for that specific user:

procedure RequireTwoFactorAuthentication(Context: ISphinxContext; const UserName: string);
var
  User: TUser;
  UserManager: IUserManager;
begin
  // Retrieve the user
  User := Context.UserManager.FindByName(UserName);
  if User = nil then
    raise Exception.Create('User not found');
  UserManager := Context.UserManager;

  // Require two-factor authentication next time user tries to login
  UserManager.SetTwoFactorRequired(User, True);
end;

You can also fully enable and manage two-factor authentication yourself, in case you want to display the QR Code in your own app. You can use methods to retrieve, reset, and verify the authenticator key.

Example for Enabling Two-Factor Authentication manually:

procedure EnableTwoFactorAuthentication(Context: ISphinxContext; const UserName: string);
var
  User: TUser;
  UserManager: IUserManager;
  AuthenticatorKey: string;
begin
  // Retrieve the user
  User := Context.UserManager.FindByName(UserName);
  if User = nil then
    raise Exception.Create('User not found');
  UserManager := Context.UserManager;

  // Enable two-factor authentication
  UserManager.SetTwoFactorEnabled(User, True);

  // Ensure the user has an authenticator key
  if Context.UserManager.GetAuthenticatorKey(User) = '' then
    Context.UserManager.ResetAuthenticatorKey(User);

  // Retrieve the authenticator key
  AuthenticatorKey := Context.UserManager.GetAuthenticatorKey(User);

  // Display the QR code URI
  Log('Two-factor QR code URI: ' + TOtpUri.Build(AuthenticatorKey, 'YourApp', UserName));
end;

Lockout Management

To manage user lockouts, you can use methods like IUserManager.​IsLockedOut, IUserManager.​Access​Failed, and IUserManager.​Reset​Access​Failed​Count.

Example:

if Context.UserManager.IsLockedOut(User) then
  ShowMessage('User is locked out');

Context.UserManager.AccessFailed(User); // Increment failed access count

Context.UserManager.ResetAccessFailedCount(User); // Reset the failed access count

Token Management

You can generate and validate tokens for various purposes, such as email confirmation and password reset.

Example:

var
  Token: string;
begin
  Token := Context.UserManager.GenerateUserToken(User, 'ProviderName', 'Purpose');
  if Context.UserManager.VerifyUserToken(User, 'ProviderName', 'Purpose', Token) then
    ShowMessage('Token is valid');
end;

Authentication Tokens

You can save, retrieve, and remove authentication tokens using the respective methods.

Example:

// Save a token
Context.UserManager.SetAuthenticationToken(User, 'LoginProvider', 'TokenName', 'TokenValue');

// Retrieve a token
var
  TokenValue: string;
begin
  TokenValue := Context.UserManager.GetAuthenticationToken(User, 'LoginProvider', 'TokenName');
  ShowMessage('Token: ' + TokenValue);

// Remove a token
Context.UserManager.RemoveAuthenticationToken(User, 'LoginProvider', 'TokenName');