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.CreateContext 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.CreateUser 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.UpdateUser method.
Example:
User.Email := 'newemail@example.com';
Context.UserManager.UpdateUser(User);
Deleting a User
To delete a user from the database, use the IUserManager.DeleteUser 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.FindById, IUserManager.FindByName, IUserManager.FindByEmail, and IUserManager.FindByPhoneNumber.
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.CheckPassword 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 (TDefaultPasswordHasher). 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 inTUser.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.PasswordHasher 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 TDefaultPasswordHasher. 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.RequireTwoFactor 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.SetTwoFactorRequired 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.AccessFailed, and IUserManager.ResetAccessFailedCount.
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');