Available documentation - own password hashes

Hi, as far as I can see there's the demo application and the sphinx-user-guide.pdf included in TMSSphinx. Is there additional documentation or online courses available or do we need to "explore" that stuff the hard way with reading the source code and exploring the forums? I would like to use bcrypt as password hashing algorithm for example because I have an older app which I want to migrate / upgrade to delphi and sphinx and need to reuse existing hashes and not force the users to change their passwords... (I know how to bcrypt but I want to know how to integrate it in Sphinx)

And the online documentation (which has the same content of PDF, but different format of course): Overview | TMS Sphinx documentation

There is this session about TMS Sphinx from last TMS Training Days 2023:

Well, at some point you will have to do that. There is no way we can cover every an each possible thing you can do with TMS Sphinx (or any other product) in our courses and documentation.

If you have existing password, probably just changing the hash won't solve you problem, because you have existing user tables, that you want to use instead of the existing ones Sphinx creates, right? Because Sphinx checks the password agains a hash saved in its own table. In this case, one alternative is you create your own TUserManager implementation, or inherit from the existing one, and override the methods you need to make it work.

It's low-level, but here is an example that completely removes Sphinx own tables and use custom ones.

unit B2B.UserManager;

interface

uses
  SysUtils, Generics.Collections, Bcl.Validation.Interfaces, Hash,
  Aurelius.Engine.ObjectManager, Aurelius.Drivers.Interfaces,
  Sphinx.UserManager, Sphinx.UserManagerFactory, Sphinx.Entities, Sphinx.CoreOptions,
  Sphinx.UserValidator, Sphinx.PasswordValidator,
  B2B.Entities;

type
  TMyUserManagerFactory = class(TInterfacedObject, IUserManagerFactory)
  public
    function CreateUserManager(AConfig: ICoreOptions; AManager: TObjectManager; AOwnsManager: boolean): IUserManager;
  end;

  TMyUserManager = class(TInterfacedObject, IUserManager)
  strict private
    FConfig: ICoreOptions;
    FSphinxManager: TObjectManager;
    FManager: TObjectManager;
    FOwnsManager: Boolean;
    FUserValidators: TList<IUserValidator>;
    FPasswordValidators: TList<IPasswordValidator>;
    procedure AddOwnership(Obj: TObject);
    function Manager: TObjectManager;
  strict private
    procedure UsuarioToUser(Source: TUsuarios; Dest: TUser);
  public
    constructor Create(AConfig: ICoreOptions; AManager: TObjectManager; AOwnsManager: Boolean);
    destructor Destroy; override;
  public
    procedure CreateUser(User: TUser); overload;
    procedure CreateUser(User: TUser; const Password: string); overload;
    procedure UpdateUser(User: TUser);
    procedure DeleteUser(User: TUser);
    function FindById(const Id: string): TUser;
    function FindByName(const UserName: string): TUser;
    function FindByEmail(const Email: string): TUser;
    function FindByPhoneNumber(const PhoneNumber: string): TUser;
    function CheckPassword(User: TUser; const Password: string): Boolean;
    function GenerateEmailConfirmationToken(User: TUser): string;
    procedure ConfirmEmail(User: TUser; const Token: string); overload;
    function GeneratePhoneNumberConfirmationToken(User: TUser): string;
    procedure ConfirmPhoneNumber(User: TUser; const Token: string); overload;
    function GeneratePasswordResetToken(User: TUser): string;
    function GenerateChangePhoneNumberToken(User: TUser; const NewPhoneNumber: string): string;
    procedure ChangePhoneNumber(User: TUser; const Token, NewPhoneNumber: string);
    function BeginResetPassword(User: TUser; const Token: string): string;
    function GeneratePasswordChangeToken(User: TUser): string;
    procedure ChangePassword(User: TUser; const Token, NewPassword: string);
    function GenerateChangeEmailToken(User: TUser; const NewEmail: string): string;
    procedure ChangeEmail(User: TUser; const Token, NewEmail: string);
    function IsLockedOut(User: TUser): Boolean;
    procedure AccessFailed(User: TUser);
    procedure ResetAccessFailedCount(User: TUser);
    procedure AddPassword(User: TUser; const Password: string);
    function GenerateUserToken(User: TUser; const TokenProvider: string; const Purpose: string): string;
    function VerifyUserToken(User: TUser; const TokenProvider: string; const Purpose: string; const Token: string): Boolean;
    procedure SetAuthenticationToken(User: TUser; const LoginProvider, TokenName, TokenValue: string);
    function GetAuthenticationToken(User: TUser; const LoginProvider, TokenName: string): string;
    procedure RemoveAuthenticationToken(User: TUser; const LoginProvider, TokenName: string);
    procedure ValidateUser(User: TUser);
    procedure ValidatePassword(User: TUser; const Password: string);
    function CreateUserInstance: TUser;
    function CreateRoleInstance: TRole;
  end;

implementation

uses
  Aurelius.Criteria.Linq,
  Sphinx.UserManager.Impl;

{ TMyUserManager }

procedure TMyUserManager.AccessFailed(User: TUser);
begin
end;

procedure TMyUserManager.AddOwnership(Obj: TObject);
begin
  FSphinxManager.AddOwnership(Obj);
end;

procedure TMyUserManager.AddPassword(User: TUser; const Password: string);
begin
  raise Exception.Create('AddPassword not supported');
end;

function TMyUserManager.BeginResetPassword(User: TUser; const Token: string): string;
begin
  raise Exception.Create('BeginResetPassword not supported');
end;

procedure TMyUserManager.ChangeEmail(User: TUser; const Token, NewEmail: string);
begin
  raise Exception.Create('ChangeEmail not supported');
end;

procedure TMyUserManager.ChangePassword(User: TUser; const Token, NewPassword: string);
begin
  raise Exception.Create('ChangePassword not supported');
end;

procedure TMyUserManager.ChangePhoneNumber(User: TUser; const Token, NewPhoneNumber: string);
begin
  raise Exception.Create('ChangePhoneNumber not supported');
end;

function TMyUserManager.CheckPassword(User: TUser; const Password: string): Boolean;
var
  Hash: string;
begin
  Hash := User.PasswordHash.AsUnicodeString;
  if Hash = '' then
    Exit(False);
  Result := SameText(Hash, THashMD5.GetHashString(Password));
end;

procedure TMyUserManager.ConfirmEmail(User: TUser; const Token: string);
begin
  raise Exception.Create('ConfirmEmail not supported');
end;

procedure TMyUserManager.ConfirmPhoneNumber(User: TUser; const Token: string);
begin
  raise Exception.Create('ConfirmPhoneNumber not supported');
end;

constructor TMyUserManager.Create(AConfig: ICoreOptions; AManager: TObjectManager; AOwnsManager: Boolean);
begin
  inherited Create;
  FConfig := AConfig;
  FSphinxManager := AManager;
  FOwnsManager := AOwnsManager;

  FUserValidators := TList<IUserValidator>.Create;
  FPasswordValidators := TList<IPasswordValidator>.Create;

  // Init user validators
  FUserValidators.Add(TUserNameValidator.Create(FConfig.UserOptions));
  FUserValidators.Add(TUserEmailValidator.Create(FConfig.UserOptions));
  FUserValidators.Add(TUserPhoneNumberValidator.Create(FConfig.UserOptions));
  FUserValidators.AddRange(FConfig.UserValidators);

  // Init password validators
  FPasswordValidators.Add(TDefaultPasswordValidator.Create(FConfig.PasswordOptions));
  FPasswordValidators.AddRange(FConfig.PasswordValidators);
end;

function TMyUserManager.CreateRoleInstance: TRole;
begin
  Result := FConfig.EntityFactory.CreateRole;
end;

procedure TMyUserManager.CreateUser(User: TUser; const Password: string);
begin
  raise Exception.Create('CreateUser not supported');
end;

procedure TMyUserManager.CreateUser(User: TUser);
begin
  raise Exception.Create('CreateUser not supported');
end;

function TMyUserManager.CreateUserInstance: TUser;
begin
  Result := FConfig.EntityFactory.CreateUser;
end;

procedure TMyUserManager.DeleteUser(User: TUser);
begin
  raise Exception.Create('DeleteUser not supported');
end;

destructor TMyUserManager.Destroy;
begin
  if FOwnsManager then
    FSphinxManager.Free;
  FManager.Free;
  FUserValidators.Free;
  FPasswordValidators.Free;
  inherited;
end;

function TMyUserManager.FindByEmail(const Email: string): TUser;
var
  Usuario: TUsuarios;
begin
  Usuario := Manager.Find<TUsuarios>
    .Add(Linq['LOGIN'] = Uppercase(Trim(Email)))
    .UniqueResult;
  if Usuario <> nil then
  begin
    Result := TUser.Create;
    AddOwnership(Result);
    UsuarioToUser(Usuario, Result);
  end
  else
    Result := nil;
end;

function TMyUserManager.FindById(const Id: string): TUser;
var
  Usuario: TUsuarios;
begin
  Usuario := Manager.Find<TUsuarios>
    .Add(Linq['ID'] = Uppercase(Trim(Id)))
    .UniqueResult;
  if Usuario <> nil then
  begin
    Result := TUser.Create;
    AddOwnership(Result);
    UsuarioToUser(Usuario, Result);
  end
  else
    Result := nil;
end;

function TMyUserManager.FindByName(const UserName: string): TUser;
begin
  raise Exception.Create('FindByName not supported');
end;

function TMyUserManager.FindByPhoneNumber(const PhoneNumber: string): TUser;
begin
  raise Exception.Create('FindByPhoneNumber not supported');
end;

function TMyUserManager.GenerateChangeEmailToken(User: TUser; const NewEmail: string): string;
begin
  raise Exception.Create('GenerateChangeEmailToken not supported');
end;

function TMyUserManager.GenerateChangePhoneNumberToken(User: TUser; const NewPhoneNumber: string): string;
begin
  raise Exception.Create('GenerateChangePhoneNumberToken not supported');
end;

function TMyUserManager.GenerateEmailConfirmationToken(User: TUser): string;
begin
  raise Exception.Create('GenerateEmailConfirmationToken not supported');
end;

function TMyUserManager.GeneratePasswordChangeToken(User: TUser): string;
begin
  raise Exception.Create('GeneratePasswordChangeToken not supported');
end;

function TMyUserManager.GeneratePasswordResetToken(User: TUser): string;
begin
  raise Exception.Create('CeneratePasswordResetToken not supported');
end;

function TMyUserManager.GeneratePhoneNumberConfirmationToken(User: TUser): string;
begin
  raise Exception.Create('GeneratePhoneNumberConfirmation not supported');
end;

function TMyUserManager.GenerateUserToken(User: TUser; const TokenProvider, Purpose: string): string;
begin
  raise Exception.Create('GenerateUserToken not supported');
end;

function TMyUserManager.GetAuthenticationToken(User: TUser; const LoginProvider, TokenName: string): string;
begin
  raise Exception.Create('GetAuthenticationToken not supported');
end;

function TMyUserManager.IsLockedOut(User: TUser): Boolean;
begin
  Result := False; // users never get locked out
end;

function TMyUserManager.Manager: TObjectManager;
begin
  if FManager = nil then
    FManager := TObjectManager.Create(FSphinxManager.Connection);
  Result := FManager;
end;

procedure TMyUserManager.RemoveAuthenticationToken(User: TUser; const LoginProvider, TokenName: string);
begin
  raise Exception.Create('RemoveAuthenticationToken not supported');
end;

procedure TMyUserManager.ResetAccessFailedCount(User: TUser);
begin
  // Do not anything about failed access count
end;

procedure TMyUserManager.SetAuthenticationToken(User: TUser; const LoginProvider, TokenName, TokenValue: string);
begin
  raise Exception.Create('SetAuthenticationToken not supported');
end;

procedure TMyUserManager.UpdateUser(User: TUser);
begin
  raise Exception.Create('UpdateUser not supported');
end;

procedure TMyUserManager.UsuarioToUser(Source: TUsuarios; Dest: TUser);
begin
  Dest.Id := Source.Id.ToString;
  Dest.Email := Source.Login;
  Dest.UserName := Source.Login;
  Dest.EmailConfirmed := True;
  Dest.PasswordHash.AsUnicodeString := Source.Pass;
end;

procedure TMyUserManager.ValidatePassword(User: TUser; const Password: string);
begin
  raise Exception.Create('ValidatePassword not supported');
end;

procedure TMyUserManager.ValidateUser(User: TUser);
var
  Results: TList<IValidationResult>;
  Validator: IUserValidator;
  ValidatorResult: IValidationResult;
begin
  Results := TList<IValidationResult>.Create;
  try
//    if User.SecurityStamp.ValueOrDefault = '' then
//      raise ESphinxException.Create(SEmptySecurityStamp);

    for Validator in FUserValidators do
    begin
      ValidatorResult := Validator.Validate(Self, User);
      if not ValidatorResult.Succeeded then
        Results.Add(ValidatorResult);
    end;
    if Results.Count > 0 then
      raise EUserValidationException.Create(User.DisplayName, Results);
  finally
    Results.Free;
  end;
end;

function TMyUserManager.VerifyUserToken(User: TUser; const TokenProvider, Purpose, Token: string): Boolean;
begin
  raise Exception.Create('VerifyUserToken not supported');
end;

{ TMyUserManagerFactory }

function TMyUserManagerFactory.CreateUserManager(AConfig: ICoreOptions; AManager: TObjectManager;
  AOwnsManager: boolean): IUserManager;
begin
  Result := TMyUserManager.Create(AConfig, AManager, AOwnsManager);
end;

end.

And register it this way:

type
  TInternalConfig = class(TSphinxConfig)
  end;

procedure TDataModule2.DataModuleCreate(Sender: TObject);
begin
  // Set specific user manager
  TInternalConfig(SphinxConfig1).UserManagerFactory := TMyUserManagerFactory.Create;