mORMot and Open Source friends
Check-in [f1e7198954]
Not logged in

Many hyperlinks are disabled.
Use anonymous login to enable hyperlinks.

Overview
Comment:{1048} introducing TSQLRestServerAuthenticationActiveDirectory class, thanks to an implementation proposal from EgorovAlex - thanks for sharing!
Timelines: family | ancestors | descendants | both | trunk
Files: files | file ages | folders
SHA1: f1e71989545b57052b8dea70997ec8797ae83041
User & Date: ab 2015-03-06 14:45:11
Context
2015-03-08
19:26
{1049} fixed modNoMongo format support when returning a MongoDB query as JSON check-in: ac92b58ad4 user: ab tags: trunk
2015-03-06
14:45
{1048} introducing TSQLRestServerAuthenticationActiveDirectory class, thanks to an implementation proposal from EgorovAlex - thanks for sharing! check-in: f1e7198954 user: ab tags: trunk
14:44
{1047} added class function TAESAbstract.SimpleEncrypt() check-in: 122f1daec8 user: ab tags: trunk
Changes
Hide Diffs Unified Diffs Ignore Whitespace Patch

Changes to SQLite3/mORMot.pas.

28
29
30
31
32
33
34

35
36
37
38
39
40
41
...
846
847
848
849
850
851
852


853
854
855
856
857
858
859
.....
12077
12078
12079
12080
12081
12082
12083




12084

12085
12086
12087
12088
12089
12090
12091
.....
12344
12345
12346
12347
12348
12349
12350


12351

12352

12353
12354
12355
12356
12357
12358
12359
12360
12361
12362
12363
12364
.....
12371
12372
12373
12374
12375
12376
12377
12378
12379
12380
12381
12382
12383




























12384
12385
12386
12387
12388
12389
12390
12391
.....
40252
40253
40254
40255
40256
40257
40258






40259
40260
40261
40262
40263
40264
40265
.....
40296
40297
40298
40299
40300
40301
40302

40303
40304
40305
40306
40307
40308
40309
.....
40718
40719
40720
40721
40722
40723
40724
40725
40726
40727
40728
40729
40730
40731
40732
40733
40734
40735
40736
40737
40738
40739
40740
.....
40785
40786
40787
40788
40789
40790
40791










































































40792
40793
40794
40795
40796
40797
40798
  Portions created by the Initial Developer are Copyright (C) 2015
  the Initial Developer. All Rights Reserved.

  Contributor(s):
    Alexander (chaa)
    Alfred Glaenzer (alf)
    DigDiver

    Esmond
    Pavel (mpv)
    Jordi Tudela
    Martin Suer
    MilesYou
    Sabbiolina
    Vadim Orel
................................................................................
      InitializeTable methods, to tune underlying table creation (e.g. indexes)
    - introducing TInterfaceStub and TInterfaceMock classes to define
      high-performance interface stubbing and mocking via a fluent interface
    - integrated Windows Authentication to the mORMot Client-Server layer: in
      order to enable it, define a SSPIAUTH conditional and call
      TSQLRestClientURI.SetUser() with an empty user name, and ensure that
      TSQLAuthUser.LoginName contains a matching 'DomainName\UserName' value


    - added TSQLRecordTimed class, and TSQLRecord.AddFilterNotVoidAllTextFields
      and TSQLModel.AddTableInherited methods
    - Windows Authentication can use either NTLM or the more secure Kerberos
      protocol, if the corresponding SPN domain is set as password
    - feature request [5a17a4277f]: you can now define in the Model your custom
      TSQLAuthUser and/or TSQLAuthGroup classes to store the authorization
      information: TSQLRestServer will search for any table inheriting from
................................................................................
  TSQLRestServerAuthenticationClientSetUserPassword = (
    passClear, passHashed, passKerberosSPN);

  /// optional behavior of TSQLRestServerAuthentication class
  // - by default, saoUserByLogonOrID is set, allowing
  // TSQLRestServerAuthentication.GetUser() to retrieve the TSQLAuthUser by
  // logon name or by ID, if the supplied logon name is an integer




  TSQLRestServerAuthenticationOption = (saoUserByLogonOrID);


  /// defines the optional behavior of TSQLRestServerAuthentication class
  TSQLRestServerAuthenticationOptions = set of TSQLRestServerAuthenticationOption;

  /// abstract class used to implement server-side authentication in TSQLRestServer
  // - inherit from this class to implement expected authentication scheme
  TSQLRestServerAuthentication = class
................................................................................
    /// handle the Auth RESTful method with HTTP Basic
    // - will first return HTML_UNAUTHORIZED (401), then expect user and password
    // to be supplied as incoming "Authorization: Basic ...." headers
    function Auth(Ctxt: TSQLRestServerURIContext): boolean; override;
  end;

  {$ifdef SSPIAUTH}


  /// authentication using Windows Security Support Provider Interface (SSPI)

  // - is able to authenticate using either NTLM or Kerberos

  // - ClientSetUser() will ignore aUserName, and expect aPassword to be either
  // '' if you expect NTLM authentication to take place, or contain the SPN
  // registration (e.g. 'mymormotservice/myserver.mydomain.tld') for Kerberos
  // authentication
  TSQLRestServerAuthenticationSSPI = class(TSQLRestServerAuthenticationSignedURI)
  protected
    /// Windows built-in authentication
    // - holds information between calls to ServerSSPIAuth
    fSSPIAuthContexts: TSecContextDynArray;
    /// class method used on client side to create a remote session
    // - will call the ModelRoot/Auth service, i.e. call TSQLRestServer.Auth()
    // published method to create a session for this user: so
................................................................................
    class function ClientComputeSessionKey(Sender: TSQLRestClientURI; User: TSQLAuthUser): RawUTF8; override;
  public
    /// initialize the authentication method to a specified server
    constructor Create(aServer: TSQLRestServer); override;
    /// finalize internal memory structures
    destructor Destroy; override;
    /// will try to handle the Auth RESTful method with Windows SSPI API
    // - to be called in a two pass "challenging" algorithm, as implemented by
    // TSQLRestServerAuthenticationSignedURI.Auth method
    // - the client-side logged user will be identified as valid, according
    // to a Windows SSPI API secure challenge
    function Auth(Ctxt: TSQLRestServerURIContext): boolean; override;
  end;




























  {$endif}

  /// TSynAuthentication* class using TSQLAuthUser/TSQLAuthGroup for credentials
  // - could be used e.g. for SynDBRemote access in conjunction with mORMot
  TSynAuthenticationRest = class(TSynAuthenticationAbstract)
  protected
    fServer: TSQLRestServer;
    fAllowedGroups: TIntegerDynArray;
................................................................................
    err: integer;
begin
  UserID := GetInt64(pointer(aUserName),err);
  if (err=0) and (UserID>0) and (saoUserByLogonOrID in fOptions) then
    // TSQLAuthUser.ID was transmitted -> may use the ORM cache
    result := fServer.fSQLAuthUserClass.Create(fServer,UserID) else
    result := fServer.fSQLAuthUserClass.Create(fServer,'LogonName=?',[aUserName]);






  if result.fID=0 then begin
    {$ifdef WITHLOG}
    fServer.fLogFamily.SynLog.Log(sllUserAuth,
      '%.LogonName=% not found in table',[result.ClassType,aUserName],self);
    {$endif}
    FreeAndNil(result);
  end else
................................................................................
  if Sender=nil then
    exit;
  try
    Sender.SessionClose;
    U := TSQLAuthUser.Create;
    try
      U.LogonName := trim(aUserName);

      if aPassworKind<>passClear then
        U.PasswordHashHexa := aPassword else
        U.PasswordPlain := aPassword; // compute SHA256('salt'+aPassword);
      result := Sender.SessionCreate(self,U,ClientComputeSessionKey(Sender,U));
    finally
      U.Free;
    end;
................................................................................
    if UserName='' then
      exit;
    User := GetUser(Ctxt,UserName);
    if User<>nil then
    try
      User.PasswordHashHexa := ''; // override with context
      fServer.SessionCreate(User,Ctxt,Session);
      if Session<>nil then begin
        if BrowserAuth then
          Ctxt.Returns(JSONEncode(['result',Session.fPrivateSalt,
            'logonname',Session.User.LogonName]),
            HTML_SUCCESS, (SECPKGNAMEHTTPWWWAUTHENTICATE+' ')+BinToBase64(OutData))
        else
          Ctxt.Returns(['result',BinToBase64(SecEncrypt(fSSPIAuthContexts[SecCtxIdx],Session.fPrivateSalt)),
            'logonname',Session.User.LogonName,'data',BinToBase64(OutData)]);
      end;
    finally
      User.Free;
    end;
  finally
    FreeSecContext(fSSPIAuthContexts[SecCtxIdx]);
    CtxArr.Delete(SecCtxIdx);
  end;
................................................................................
destructor TSQLRestServerAuthenticationSSPI.Destroy;
var i: integer;
begin
  for i := 0 to High(fSSPIAuthContexts) do
    FreeSecContext(fSSPIAuthContexts[i]);
  inherited;
end;











































































{$endif SSPIAUTH}

{ TSynAuthenticationRest }

constructor TSynAuthenticationRest.Create(aServer: TSQLRestServer;
  const aAllowedGroups: array of integer);






>







 







>
>







 







>
>
>
>
|
>







 







>
>
|
>
|
>




|







 







|
<




>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
|







 







>
>
>
>
>
>







 







>







 







|


|
|
|
|

<







 







>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>







28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
...
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
.....
12080
12081
12082
12083
12084
12085
12086
12087
12088
12089
12090
12091
12092
12093
12094
12095
12096
12097
12098
12099
.....
12352
12353
12354
12355
12356
12357
12358
12359
12360
12361
12362
12363
12364
12365
12366
12367
12368
12369
12370
12371
12372
12373
12374
12375
12376
.....
12383
12384
12385
12386
12387
12388
12389
12390

12391
12392
12393
12394
12395
12396
12397
12398
12399
12400
12401
12402
12403
12404
12405
12406
12407
12408
12409
12410
12411
12412
12413
12414
12415
12416
12417
12418
12419
12420
12421
12422
12423
12424
12425
12426
12427
12428
12429
12430
.....
40291
40292
40293
40294
40295
40296
40297
40298
40299
40300
40301
40302
40303
40304
40305
40306
40307
40308
40309
40310
.....
40341
40342
40343
40344
40345
40346
40347
40348
40349
40350
40351
40352
40353
40354
40355
.....
40764
40765
40766
40767
40768
40769
40770
40771
40772
40773
40774
40775
40776
40777
40778

40779
40780
40781
40782
40783
40784
40785
.....
40830
40831
40832
40833
40834
40835
40836
40837
40838
40839
40840
40841
40842
40843
40844
40845
40846
40847
40848
40849
40850
40851
40852
40853
40854
40855
40856
40857
40858
40859
40860
40861
40862
40863
40864
40865
40866
40867
40868
40869
40870
40871
40872
40873
40874
40875
40876
40877
40878
40879
40880
40881
40882
40883
40884
40885
40886
40887
40888
40889
40890
40891
40892
40893
40894
40895
40896
40897
40898
40899
40900
40901
40902
40903
40904
40905
40906
40907
40908
40909
40910
40911
40912
40913
40914
40915
40916
40917
  Portions created by the Initial Developer are Copyright (C) 2015
  the Initial Developer. All Rights Reserved.

  Contributor(s):
    Alexander (chaa)
    Alfred Glaenzer (alf)
    DigDiver
    EgorovAlex
    Esmond
    Pavel (mpv)
    Jordi Tudela
    Martin Suer
    MilesYou
    Sabbiolina
    Vadim Orel
................................................................................
      InitializeTable methods, to tune underlying table creation (e.g. indexes)
    - introducing TInterfaceStub and TInterfaceMock classes to define
      high-performance interface stubbing and mocking via a fluent interface
    - integrated Windows Authentication to the mORMot Client-Server layer: in
      order to enable it, define a SSPIAUTH conditional and call
      TSQLRestClientURI.SetUser() with an empty user name, and ensure that
      TSQLAuthUser.LoginName contains a matching 'DomainName\UserName' value
    - introducing TSQLRestServerAuthenticationActiveDirectory class, thanks to
      an implementation proposal from EgorovAlex - thanks for sharing!
    - added TSQLRecordTimed class, and TSQLRecord.AddFilterNotVoidAllTextFields
      and TSQLModel.AddTableInherited methods
    - Windows Authentication can use either NTLM or the more secure Kerberos
      protocol, if the corresponding SPN domain is set as password
    - feature request [5a17a4277f]: you can now define in the Model your custom
      TSQLAuthUser and/or TSQLAuthGroup classes to store the authorization
      information: TSQLRestServer will search for any table inheriting from
................................................................................
  TSQLRestServerAuthenticationClientSetUserPassword = (
    passClear, passHashed, passKerberosSPN);

  /// optional behavior of TSQLRestServerAuthentication class
  // - by default, saoUserByLogonOrID is set, allowing
  // TSQLRestServerAuthentication.GetUser() to retrieve the TSQLAuthUser by
  // logon name or by ID, if the supplied logon name is an integer
  // - if saoHandleUnknownLogonAsStar is defined, any user successfully
  // authenticated could be logged with the same ID (and authorization)
  // than TSQLAuthUser.Logon='*' - of course, this is meaningfull only with
  // an external credential check (e.g. via SSPI or Active Directory)
  TSQLRestServerAuthenticationOption = (
    saoUserByLogonOrID, saoHandleUnknownLogonAsStar);

  /// defines the optional behavior of TSQLRestServerAuthentication class
  TSQLRestServerAuthenticationOptions = set of TSQLRestServerAuthenticationOption;

  /// abstract class used to implement server-side authentication in TSQLRestServer
  // - inherit from this class to implement expected authentication scheme
  TSQLRestServerAuthentication = class
................................................................................
    /// handle the Auth RESTful method with HTTP Basic
    // - will first return HTML_UNAUTHORIZED (401), then expect user and password
    // to be supplied as incoming "Authorization: Basic ...." headers
    function Auth(Ctxt: TSQLRestServerURIContext): boolean; override;
  end;

  {$ifdef SSPIAUTH}

  /// authentication of the current logged user using Windows Security Support
  // Provider Interface (SSPI)
  // - is able to authenticate the currently logged user on the client side,
  // using either NTLM or Kerberos - it would allow to safely authenticate
  // on a mORMot server without prompting the user to enter its password 
  // - ClientSetUser() will ignore aUserName, and expect aPassword to be either
  // '' if you expect NTLM authentication to take place, or contain the SPN
  // registration (e.g. 'mymormotservice/myserver.mydomain.tld') for Kerberos
  // authentication
  TSQLRestServerAuthenticationSSPI = class(TSQLRestServerAuthenticationURI)
  protected
    /// Windows built-in authentication
    // - holds information between calls to ServerSSPIAuth
    fSSPIAuthContexts: TSecContextDynArray;
    /// class method used on client side to create a remote session
    // - will call the ModelRoot/Auth service, i.e. call TSQLRestServer.Auth()
    // published method to create a session for this user: so
................................................................................
    class function ClientComputeSessionKey(Sender: TSQLRestClientURI; User: TSQLAuthUser): RawUTF8; override;
  public
    /// initialize the authentication method to a specified server
    constructor Create(aServer: TSQLRestServer); override;
    /// finalize internal memory structures
    destructor Destroy; override;
    /// will try to handle the Auth RESTful method with Windows SSPI API
    // - to be called in a two pass algorithm, used to cypher the password

    // - the client-side logged user will be identified as valid, according
    // to a Windows SSPI API secure challenge
    function Auth(Ctxt: TSQLRestServerURIContext): boolean; override;
  end;

  /// authentication of a User and its Password using Active Directory
  // - ClientSetUser() will use aUserName as 'Domain\Login', and will validate
  // aPassword against the one registered for his/her account
  TSQLRestServerAuthenticationActiveDirectory = class(TSQLRestServerAuthenticationURI)
  protected
    class function ClientComputeSessionKey(Sender: TSQLRestClientURI; User: TSQLAuthUser): RawUTF8; override;
  public
    /// will try to handle the Auth RESTful method with Windows SSPI API
    // - to be called in a two pass "challenging" algorithm, as implemented by
    // TSQLRestServerAuthenticationSignedURI.Auth method
    // - the user name and password supplied from the client side will be
    // checked against the domain supplied in the user name (e.g. 'Domain\Login')
    function Auth(Ctxt: TSQLRestServerURIContext): boolean; override;
    /// class method to be used on client side to create a remote session
    // - call TSQLRestServerAuthenticationActiveDirectory.ClientSetUser() instead
    // of TSQLRestClientURI.SetUser(), and never the method of this abstract class
    // - expects aUserName to be in form of 'Domain\Login', so that 'Login'
    // will be checked via Active Direction with the supplied password against
    // the supplied 'Domain' - if 'Domain' is the computer name where the server
    // is started (i.e. 'ComputeName\Login'), the user will be searched within
    // the registered local accounts login 
    // - needs the plain aPassword, so aPasswordKind should be passClear
    // - returns true on success
    class function ClientSetUser(Sender: TSQLRestClientURI; const aUserName, aPassword: RawUTF8;
      aPassworKind: TSQLRestServerAuthenticationClientSetUserPassword=passClear): boolean; override;
  end;

  {$endif SSPIAUTH}

  /// TSynAuthentication* class using TSQLAuthUser/TSQLAuthGroup for credentials
  // - could be used e.g. for SynDBRemote access in conjunction with mORMot
  TSynAuthenticationRest = class(TSynAuthenticationAbstract)
  protected
    fServer: TSQLRestServer;
    fAllowedGroups: TIntegerDynArray;
................................................................................
    err: integer;
begin
  UserID := GetInt64(pointer(aUserName),err);
  if (err=0) and (UserID>0) and (saoUserByLogonOrID in fOptions) then
    // TSQLAuthUser.ID was transmitted -> may use the ORM cache
    result := fServer.fSQLAuthUserClass.Create(fServer,UserID) else
    result := fServer.fSQLAuthUserClass.Create(fServer,'LogonName=?',[aUserName]);
  if (result.fID=0) and
     (saoHandleUnknownLogonAsStar in fOptions) then
    if fServer.Retrieve('LogonName=?',[],['*'],result) then begin
      result.LogonName := aUserName;
      result.DisplayName := aUserName;
    end;
  if result.fID=0 then begin
    {$ifdef WITHLOG}
    fServer.fLogFamily.SynLog.Log(sllUserAuth,
      '%.LogonName=% not found in table',[result.ClassType,aUserName],self);
    {$endif}
    FreeAndNil(result);
  end else
................................................................................
  if Sender=nil then
    exit;
  try
    Sender.SessionClose;
    U := TSQLAuthUser.Create;
    try
      U.LogonName := trim(aUserName);
      U.DisplayName := U.LogonName;
      if aPassworKind<>passClear then
        U.PasswordHashHexa := aPassword else
        U.PasswordPlain := aPassword; // compute SHA256('salt'+aPassword);
      result := Sender.SessionCreate(self,U,ClientComputeSessionKey(Sender,U));
    finally
      U.Free;
    end;
................................................................................
    if UserName='' then
      exit;
    User := GetUser(Ctxt,UserName);
    if User<>nil then
    try
      User.PasswordHashHexa := ''; // override with context
      fServer.SessionCreate(User,Ctxt,Session);
      if Session<>nil then
        if BrowserAuth then
          Ctxt.Returns(JSONEncode(['result',Session.fPrivateSalt,
            'logonname',Session.User.LogonName]),HTML_SUCCESS,
            (SECPKGNAMEHTTPWWWAUTHENTICATE+' ')+BinToBase64(OutData)) else
          Ctxt.Returns([
            'result',BinToBase64(SecEncrypt(fSSPIAuthContexts[SecCtxIdx],Session.fPrivateSalt)),
            'logonname',Session.User.LogonName,'data',BinToBase64(OutData)]);

    finally
      User.Free;
    end;
  finally
    FreeSecContext(fSSPIAuthContexts[SecCtxIdx]);
    CtxArr.Delete(SecCtxIdx);
  end;
................................................................................
destructor TSQLRestServerAuthenticationSSPI.Destroy;
var i: integer;
begin
  for i := 0 to High(fSSPIAuthContexts) do
    FreeSecContext(fSSPIAuthContexts[i]);
  inherited;
end;


{ TSQLRestServerAuthenticationActiveDirectory }

const
  AD_SALT = '5aLt';

function TSQLRestServerAuthenticationActiveDirectory.Auth(Ctxt: TSQLRestServerURIContext): boolean;
function CheckPassword(const UserName,Password: RawUTF8): Boolean;
var Domain,Login: RawUTF8;
    hToken: THandle;
begin
  split(UserName,Domain,Login);
  result := LogonUser(pointer(UTF8ToString(Login)),pointer(UTF8ToString(Domain)),
    pointer(UTF8ToString(Password)),9,LOGON32_PROVIDER_WINNT50,hToken);
  if result then
    CloseHandle(hToken);
end;
var aUserName, aHashedPassWord, aPassword: RawUTF8;
    User: TSQLAuthUser;
begin
  result := true;
  if AuthSessionRelease(Ctxt) then
    exit;
  aUserName := Ctxt.InputUTF8OrVoid['UserName'];
  if aUserName<>'' then begin
    User := GetUser(Ctxt,aUserName);
    if User<>nil then
    try
      User.PasswordHashHexa := ''; // not needed, especially if from LogonName='*'
      aHashedPassWord := Base64ToBin(Ctxt.InputUTF8OrVoid['Password']);
      aPassword := TAESCFB.SimpleEncrypt(aHashedPassWord,Nonce(false)+AD_SALT,false);
      if not CheckPassword(aUserName,aPassWord) then begin
        aPassword := TAESCFB.SimpleEncrypt(aHashedPassWord,Nonce(true)+AD_SALT,false);
        if not CheckPassword(aUserName,aPassWord) then
          exit; // current and previous server nonce did not success
      end;
      // now client is authenticated -> create a session
      SessionCreate(Ctxt,User);
    finally
      User.Free;
    end;
  end else 
    if aUserName<>'' then
      // only UserName=... -> return hexadecimal nonce content valid for 5 minutes
      Ctxt.Results([Nonce(false)]) else
      // parameters does not match any expected layout
      result := false;
end;

class function TSQLRestServerAuthenticationActiveDirectory.ClientComputeSessionKey(Sender: TSQLRestClientURI;
  User: TSQLAuthUser): RawUTF8;
var aServerNonce: RawUTF8;
begin
  result := '';
  if User.LogonName='' then
    exit;
  aServerNonce := Sender.CallBackGetResult('Auth',['UserName',User.LogonName]);
  if aServerNonce='' then
    exit;
  result := Sender.CallBackGetResult('Auth',['UserName',User.LogonName,
    'Password',BinToBase64(TAESCFB.SimpleEncrypt(
    User.PasswordHashHexa,aServerNonce+AD_SALT,true))]);
end;

class function TSQLRestServerAuthenticationActiveDirectory.ClientSetUser(
  Sender: TSQLRestClientURI; const aUserName, aPassword: RawUTF8;
  aPassworKind: TSQLRestServerAuthenticationClientSetUserPassword=passClear): boolean;
begin
  if aPassworKind<>passClear then
    raise ESecurityException.CreateUTF8('%.ClientSetUser() expects passClear',[self]);
  result := inherited ClientSetUser(Sender,aUserName,aPassword,passHashed);
end;


{$endif SSPIAUTH}

{ TSynAuthenticationRest }

constructor TSynAuthenticationRest.Create(aServer: TSQLRestServer;
  const aAllowedGroups: array of integer);

Changes to SynopseCommit.inc.

1
'1.18.1047'
|
1
'1.18.1048'