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

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

Overview
Comment:{1259} BREAKING CHANGE: renamed TSQLRestServer.OnSessionFailed event as TSQLRestServer.OnAuthenticationFailed, including a new TNotifyAuthenticationFailedReason parameter so that the callback would be able to identify which kind of failure did occur
Timelines: family | ancestors | descendants | both | trunk
Files: files | file ages | folders
SHA1: 59a04453f15231c405aedf1c5e22d4e01eea7112
User & Date: ab 2015-04-27 08:34:51
Context
2015-04-27
09:39
{1260} fixed regression tests to handle [58a19f830d] commit ('"*" or a set of ' comment in ObjectToJSON for enumerates or sets) check-in: 3000457992 user: ab tags: trunk
08:34
{1259} BREAKING CHANGE: renamed TSQLRestServer.OnSessionFailed event as TSQLRestServer.OnAuthenticationFailed, including a new TNotifyAuthenticationFailedReason parameter so that the callback would be able to identify which kind of failure did occur check-in: 59a04453f1 user: ab tags: trunk
2015-04-26
19:13
{1258} added the ability to create a REST API documentation in AsciiDoc format check-in: 58a19f830d user: ab tags: trunk
Changes
Hide Diffs Unified Diffs Ignore Whitespace Patch

Changes to SQLite3/mORMot.pas.

867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
....
4788
4789
4790
4791
4792
4793
4794








4795
4796
4797
4798
4799
4800
4801
....
4995
4996
4997
4998
4999
5000
5001

5002
5003

5004
5005
5006
5007
5008
5009
5010
5011
.....
12334
12335
12336
12337
12338
12339
12340





12341
12342
12343
12344
12345
12346
12347
.....
12620
12621
12622
12623
12624
12625
12626


12627
12628
12629
12630
12631
12632
12633
.....
13386
13387
13388
13389
13390
13391
13392


13393
13394
13395
13396
13397
13398
13399
.....
13467
13468
13469
13470
13471
13472
13473
13474
13475
13476
13477
13478
13479
13480
13481
.....
32510
32511
32512
32513
32514
32515
32516
32517

32518
32519
32520
32521
32522
32523
32524
32525
32526
32527
32528
32529
32530
32531
.....
33872
33873
33874
33875
33876
33877
33878
33879

33880
33881
33882
33883
33884
33885
33886
33887
33888
33889
.....
34123
34124
34125
34126
34127
34128
34129
34130
34131
34132
34133
34134
34135
34136
34137
34138
34139
34140
34141
34142
34143
34144
34145
34146
34147
34148
34149
34150
34151
34152
34153
34154
.....
41936
41937
41938
41939
41940
41941
41942
41943
41944
41945
41946
41947
41948
41949
41950
.....
42301
42302
42303
42304
42305
42306
42307
42308
42309
42310
42311
42312
42313
42314
42315
42316
42317
.....
42441
42442
42443
42444
42445
42446
42447
42448
42449
42450
42451
42452

42453
42454
42455
42456
42457
42458
42459
42460
42461
42462
42463
.....
42493
42494
42495
42496
42497
42498
42499
42500
42501
42502
42503
42504
42505
42506


42507
42508
42509
42510
42511
42512
42513
42514
.....
42641
42642
42643
42644
42645
42646
42647
42648
42649
42650
42651
42652
42653
42654
42655

42656
42657
42658
42659
42660
42661
42662
42663
42664
42665
42666
.....
42751
42752
42753
42754
42755
42756
42757
42758
42759
42760
42761
42762
42763
42764
42765
42766
42767
42768
42769
42770
42771
42772
42773
42774
42775
42776
42777
42778
42779
      TSQLAuthUser and/or TSQLAuthGroup classes to store the authorization
      information: TSQLRestServer will search for any table inheriting from
      TSQLAuthUser/TSQLAuthGroup in the TSQLModel - see also corresponding
      TSQLRestServer.SQLAuthUserClass/SQLAuthGroupClass new properties
    - introducing TSQLAuthUser.CanUserLog() to ensure authentication is allowed,
      as requested by feature request [842906425928]
    - added TSynAuthenticationRest e.g. for SynDBRemote to check REST users
    - added TSQLRestServer.OnSessionCreate / OnSessionClosed / OnSessionFailed
      callbacks, and TSQLRestServerURIContext.AuthenticationFailed virtual method
    - added TSQLRestServer.SessionClass property to specify the class type
      to handle in-memory sessions, and override e.g. IsValidURI() method
    - CreateMissingTables() method is not declared as virtual in TSQLRestServer
    - TSQLRestServer.URI() and TSQLRestClientURI.InternalURI() methods now uses
      one TSQLRestURIParams parameter for all request input and output values
    - TSQLRestServer.URI() method will return "405 Method Not Allowed" error
................................................................................
  // !  context := @ServiceContext; // threadvar access once
  // !  ...
  PServiceRunningContext = ^TServiceRunningContext;

  TSQLRestServerURIContext = class;
  TAuthSession = class;









  /// will identify the currently running service on the server side
  // - is the type of the global ServiceContext threadvar
  // - to access the current TSQLRestServer instance (and e.g. its ORM/CRUD
  // or SOA methods), use Request.Server and not Factory.Server, which may not
  // be available e.g. if you run the service from the server side (so no
  // factory is involved)
  TServiceRunningContext = record
................................................................................
    // - should set Service member (and possibly ServiceMethodIndex)
    // - abstract implementation which is to be overridden
    procedure URIDecodeSOAByInterface; virtual; abstract;
    /// process authentication
    // - return FALSE in case of invalid signature, TRUE if authenticated
    function Authenticate: boolean; virtual;
    /// method called in case of authentication failure

    // - this default implementation will just set OutStatus := HTML_FORBIDDEN
    // and call Server.OnSessionFailed event

    procedure AuthenticationFailed; virtual;
    /// direct launch of a method-based service
    // - URI() will ensure that MethodIndex>=0 before calling it
    procedure ExecuteSOAByMethod; virtual;
    /// direct launch of an interface-based service
    // - URI() will ensure that Service<>nil before calling it
    // - abstract implementation which is to be overridden
    procedure ExecuteSOAByInterface; virtual; abstract;
................................................................................
  TNotifyFieldSQLEvent = function(Sender: TSQLRestServer; Event: TSQLEvent;
    aTable: TSQLRecordClass; const aID: TID; const aAffectedFields: TSQLFieldBits): boolean of object;
  /// session-related callbacks triggered by TSQLRestServer
  // - for OnSessionCreate, returning TRUE will abort the session creation -
  // and you can set Ctxt.Call^.OutStatus to a corresponding error code
  TNotifySQLSession = function(Sender: TSQLRestServer; Session: TAuthSession;
    Ctxt: TSQLRestServerURIContext): boolean of object;






  TSQLRestStorageInMemory = class;
  TSQLVirtualTableModule = class;

  /// class-reference type (metaclass) of our abstract table storage
  // - may be e.g. TSQLRestStorageInMemory, TSQLRestStorageInMemoryExternal,
  // TSQLRestStorageExternal or TSQLRestStorageMongoDB
................................................................................
    // TSQLAuthGroup instance for fast retrieval in TSQLRestServer.URI
    function GetUser(Ctxt: TSQLRestServerURIContext;
      const aUserName: RawUTF8): TSQLAuthUser; virtual;
    /// create a session on the server for a given user
    // - this default implementation will call fServer.SessionCreate() and
    // return a '{"result":"HEXASALT","logonname":"UserName"}' JSON content
    // and will always call User.Free


    procedure SessionCreate(Ctxt: TSQLRestServerURIContext; var User: TSQLAuthUser); virtual;
    /// abstract method which will be called by ClientSetUser() to process the
    // authentication step on the client side
    // - at call, a TSQLAuthUser instance will be supplied, with LogonName set
    // with aUserName and PasswordHashHexa with a SHA-256 hash of aPassword
    // - override with the expected method, returning the session key on success
    class function ClientComputeSessionKey(Sender: TSQLRestClientURI; User: TSQLAuthUser): RawUTF8;
................................................................................
    /// this method is overridden for setting the NoAJAXJSON field
    // of all associated TSQLRestStorage servers
    procedure SetNoAJAXJSON(const Value: boolean); virtual;
    /// add a new session to the internal session list
    // - do not use this method directly: this callback is to be used by
    // TSQLRestServerAuthentication* classes
    // - will check that the logon name is valid


    procedure SessionCreate(var User: TSQLAuthUser; Ctxt: TSQLRestServerURIContext;
      out Session: TAuthSession); virtual;
    /// fill the supplied context from the supplied aContext.Session ID
    // - returns nil if not found, or fill aContext.User/Group values if matchs
    // - this method will also check for outdated sessions, and delete them
    // - this method is not thread-safe: caller should use fSessions.Lock
    function SessionAccess(Ctxt: TSQLRestServerURIContext): TAuthSession;
................................................................................
    // (DenyOfService attack?) or the request is not valid (ManIntheMiddle attack?)
    // - e.g. if the URI signature is invalid, or OnSessionCreate event handler
    // aborted the session creation by returning TRUE (in this later case,
    // the Session parameter is not nil)
    // - you can access the current execution context from the Ctxt parameter,
    // e.g. to retrieve the caller's IP and ban aggressive users:
    // ! FindIniNameValue(pointer(Ctxt.Call^.InHead),'REMOTEIP: ')
    OnSessionFailed: TNotifySQLSession;
    /// a method can be specified to be notified when a session is closed
    // - for OnSessionClosed, the returning boolean value is ignored
    // - Ctxt is nil if the session is closed due to a timeout
    // - Ctxt is not nil if the session is closed explicitly by the client
    OnSessionClosed: TNotifySQLSession;
    /// this event will be executed to push notifications from the server to
    // a remote client, using a (fake) interface parameter
................................................................................
      result := Server.fPublishedMethod[MethodIndex].ByPassAuthentication;
  end else begin // default unique session if authentication is not enabled
    Session := CONST_AUTHENTICATION_NOT_USED;
    result := true;
  end;
end;

procedure TSQLRestServerURIContext.AuthenticationFailed;

begin
  // 401 Unauthorized response MUST include a WWW-Authenticate header,
  // which is not what we used, so here we won't send 401 error code but 403
  Call.OutStatus := HTML_FORBIDDEN;
  // call the notification event
  if Assigned(Server.OnSessionFailed) then
    Server.OnSessionFailed(Server,nil,self);
end;

destructor TSQLRestAcquireExecution.Destroy;
begin
  inherited Destroy;
  Thread.Free;
end;
................................................................................
    if (RootRedirectGet<>'') and (Ctxt.Method=mGet) and
       (Call.Url=Model.Root) and (Call.InBody='') then
      Ctxt.Redirect(RootRedirectGet) else begin
      Ctxt.URIDecodeSOAByMethod;
      if (Ctxt.MethodIndex<0) and (Ctxt.URI<>'') then
        Ctxt.URIDecodeSOAByInterface;
      // 2. handle security
      if (not Ctxt.Authenticate) or

         ((Ctxt.Service<>nil) and
           not (reService in Call.RestAccessRights^.AllowRemoteExecute)) then
        Ctxt.AuthenticationFailed else
      // 3. call appropriate ORM / SOA commands in fAcquireExecution[] context
      try
        if Ctxt.MethodIndex>=0 then
          if Ctxt.MethodIndex=fPublishedMethodBatchIndex then
            Ctxt.Command := execORMWrite else
            Ctxt.Command := execSOAByMethod else
        if Ctxt.Service<>nil then
................................................................................
    for i := 0 to fSessions.Count-1 do
      if TAuthSession(fSessions.List[i]).User.fID=User.fID then begin
        {$ifdef WITHLOG}
        with TAuthSession(fSessions.List[i]) do
          Ctxt.Log.Log(sllUserAuth,'User.LogonName=% already connected from "%/%"',
            [User.LogonName,RemoteIP,Ctxt.Call^.LowLevelConnectionID],self);
        {$endif}
        Ctxt.Call^.OutStatus := HTML_NOTALLOWED;
        if Assigned(OnSessionFailed) then
          OnSessionFailed(self,nil,Ctxt);
        exit; // user already connected -> error 404
      end;
  Session := fSessionClass.Create(Ctxt,User);
  if Assigned(OnSessionCreate) then
    if OnSessionCreate(self,Session,Ctxt) then begin
      {$ifdef WITHLOG}
      Ctxt.Log.Log(sllUserAuth,'Session aborted by OnSessionCreate() callback '+
         'for User.LogonName=% (connected from "%/%") - clients=%, sessions=%',
        [User.LogonName,Session.RemoteIP,Ctxt.Call^.LowLevelConnectionID,
         fStats.ClientsCurrent,fSessions.Count],self);
      {$endif}
      if Assigned(OnSessionFailed) then
        OnSessionFailed(self,Session,Ctxt);
      User := nil;
      FreeAndNil(Session); // returning TRUE aborts the session creation
      exit;
    end;
  User := nil; // will be freed by TAuthSession.Destroy
  fSessions.Add(Session);
  fStats.ClientConnect;
end;

................................................................................

constructor TAuthSession.Create(aCtxt: TSQLRestServerURIContext; aUser: TSQLAuthUser);
var GID: TSQLAuthGroup;
begin
  fUser := aUser;
  if (aCtxt<>nil) and (User<>nil) and (User.fID<>0) then begin
    GID := User.GroupRights; // save pseudo TSQLAuthGroup = ID
    User.GroupRights := aCtxt.Server.fSQLAuthGroupClass.Create(aCtxt.Server,User.GroupRights);
    if User.GroupRights.fID<>0 then begin
      // compute the next Session ID
      with aCtxt.Server do begin
        if fSessionCounter>=cardinal(maxInt) then
          fSessionCounter := 10 else
          if fSessionCounter=75 then // avoid IDCardinal=0 (77) or 1 (76)
            fSessionCounter := 78 else
................................................................................
end;

procedure TSQLRestServerAuthentication.SessionCreate(Ctxt: TSQLRestServerURIContext;
  var User: TSQLAuthUser);
var Session: TAuthSession;
begin
  if User<>nil then
  try
    // now client is authenticated -> create a session
    fServer.SessionCreate(User,Ctxt,Session);
    if Session<>nil then
      Ctxt.Returns(['result',Session.fPrivateSalt,'logonname',Session.User.LogonName]);
  finally
    User.Free;
  end;
end;

................................................................................
  aClientNonce := Ctxt.InputUTF8OrVoid['ClientNonce'];
  if (aUserName<>'') and (aClientNonce<>'') then begin
    // GET ModelRoot/auth?UserName=...&PassWord=...&ClientNonce=... -> handshaking
    User := GetUser(Ctxt,aUserName);
    if User<>nil then
    try
      // check if match TSQLRestClientURI.SetUser() algorithm
      if CheckPassword(Ctxt,User,aClientNonce,aPassWord) then begin
        // now client is authenticated -> create a session
        SessionCreate(Ctxt,User);
        exit;
      end;

    finally
      User.Free;
    end;
    Ctxt.AuthenticationFailed;
  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 -> try next authentication
      result := false;
end;
................................................................................

function TSQLRestServerAuthenticationNone.Auth(Ctxt: TSQLRestServerURIContext): boolean;
var aUserName: RawUTF8;
    U: TSQLAuthUser;
begin
  aUserName := Ctxt.InputUTF8OrVoid['UserName'];
  if aUserName='' then begin
    result := false;
    exit;
  end;
  result := true;
  if AuthSessionRelease(Ctxt) then
    exit;
  U := GetUser(Ctxt,aUserName);


  SessionCreate(Ctxt,U);
end;

class function TSQLRestServerAuthenticationNone.ClientComputeSessionKey(
  Sender: TSQLRestClientURI; User: TSQLAuthUser): RawUTF8;
begin
  result := Sender.CallBackGetResult('Auth',['UserName',User.LogonName]);
end;
................................................................................
  if GetUserPassFromInHead(Ctxt,userPass,user,pass) then begin
    U := GetUser(Ctxt,user);
    if U<>nil then
    try
      expectedPass := U.PasswordHashHexa;
      U.PasswordPlain := pass; // override with SHA-256 hash from HTTP header
      if U.PasswordHashHexa=expectedPass then begin
        fServer.SessionCreate(U,Ctxt,Session);
        if Session<>nil then begin
          // see TSQLRestServerAuthenticationHttpAbstract.ClientSessionSign()
          Ctxt.SetOutSetCookie((COOKIE_SESSION+'=')+CardinalToHex(Session.IDCardinal));
          Ctxt.Returns(['result',Session.IDCardinal,'logonname',Session.User.LogonName]);
          exit; // success
        end;
      end;

    finally
      U.Free;
    end;
    Ctxt.AuthenticationFailed;
  end else begin
    Ctxt.Call.OutHead := 'WWW-Authenticate: Basic realm="mORMot Server"';;
    Ctxt.Error('',HTML_UNAUTHORIZED); // will popup for credentials in browser
  end;
end;


................................................................................
  try
    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)]);
        exit;
      end;
    finally
      User.Free;
    end;
    Ctxt.AuthenticationFailed;
  finally
    FreeSecContext(fSSPIAuthContexts[SecCtxIdx]);
    CtxArr.Delete(SecCtxIdx);
  end;
end;

class function TSQLRestServerAuthenticationSSPI.ClientComputeSessionKey(






|







 







>
>
>
>
>
>
>
>







 







>

<
>
|







 







>
>
>
>
>







 







>
>







 







>
>







 







|







 







|
>





|
|







 







|
>
|
|
|







 







|
<
<
|



|






|
<

|







 







|







 







<
|
|







 







|
<
|
<
<
>


|
|







 







|


|



>
>
|







 







|






|
>


|
|







 







|
|







<
<


|
|







867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
....
4788
4789
4790
4791
4792
4793
4794
4795
4796
4797
4798
4799
4800
4801
4802
4803
4804
4805
4806
4807
4808
4809
....
5003
5004
5005
5006
5007
5008
5009
5010
5011

5012
5013
5014
5015
5016
5017
5018
5019
5020
.....
12343
12344
12345
12346
12347
12348
12349
12350
12351
12352
12353
12354
12355
12356
12357
12358
12359
12360
12361
.....
12634
12635
12636
12637
12638
12639
12640
12641
12642
12643
12644
12645
12646
12647
12648
12649
.....
13402
13403
13404
13405
13406
13407
13408
13409
13410
13411
13412
13413
13414
13415
13416
13417
.....
13485
13486
13487
13488
13489
13490
13491
13492
13493
13494
13495
13496
13497
13498
13499
.....
32528
32529
32530
32531
32532
32533
32534
32535
32536
32537
32538
32539
32540
32541
32542
32543
32544
32545
32546
32547
32548
32549
32550
.....
33891
33892
33893
33894
33895
33896
33897
33898
33899
33900
33901
33902
33903
33904
33905
33906
33907
33908
33909
.....
34143
34144
34145
34146
34147
34148
34149
34150


34151
34152
34153
34154
34155
34156
34157
34158
34159
34160
34161
34162

34163
34164
34165
34166
34167
34168
34169
34170
34171
.....
41953
41954
41955
41956
41957
41958
41959
41960
41961
41962
41963
41964
41965
41966
41967
.....
42318
42319
42320
42321
42322
42323
42324

42325
42326
42327
42328
42329
42330
42331
42332
42333
.....
42457
42458
42459
42460
42461
42462
42463
42464

42465


42466
42467
42468
42469
42470
42471
42472
42473
42474
42475
42476
42477
.....
42507
42508
42509
42510
42511
42512
42513
42514
42515
42516
42517
42518
42519
42520
42521
42522
42523
42524
42525
42526
42527
42528
42529
42530
.....
42657
42658
42659
42660
42661
42662
42663
42664
42665
42666
42667
42668
42669
42670
42671
42672
42673
42674
42675
42676
42677
42678
42679
42680
42681
42682
42683
.....
42768
42769
42770
42771
42772
42773
42774
42775
42776
42777
42778
42779
42780
42781
42782
42783


42784
42785
42786
42787
42788
42789
42790
42791
42792
42793
42794
      TSQLAuthUser and/or TSQLAuthGroup classes to store the authorization
      information: TSQLRestServer will search for any table inheriting from
      TSQLAuthUser/TSQLAuthGroup in the TSQLModel - see also corresponding
      TSQLRestServer.SQLAuthUserClass/SQLAuthGroupClass new properties
    - introducing TSQLAuthUser.CanUserLog() to ensure authentication is allowed,
      as requested by feature request [842906425928]
    - added TSynAuthenticationRest e.g. for SynDBRemote to check REST users
    - added TSQLRestServer.OnSessionCreate/OnSessionClosed/OnAuthenticationFailed
      callbacks, and TSQLRestServerURIContext.AuthenticationFailed virtual method
    - added TSQLRestServer.SessionClass property to specify the class type
      to handle in-memory sessions, and override e.g. IsValidURI() method
    - CreateMissingTables() method is not declared as virtual in TSQLRestServer
    - TSQLRestServer.URI() and TSQLRestClientURI.InternalURI() methods now uses
      one TSQLRestURIParams parameter for all request input and output values
    - TSQLRestServer.URI() method will return "405 Method Not Allowed" error
................................................................................
  // !  context := @ServiceContext; // threadvar access once
  // !  ...
  PServiceRunningContext = ^TServiceRunningContext;

  TSQLRestServerURIContext = class;
  TAuthSession = class;

  /// used to identify the authentication failure reason
  // - as transmitted e.g. by TSQLRestServerURIContext.AuthenticationFailed or
  // TSQLRestServer.OnAuthenticationFailed
  TNotifyAuthenticationFailedReason = (
   afInvalidSignature,afRemoteServiceExecutionNotAllowed,
   afUnknownUser,afInvalidPassword,
   afSessionAlreadyStartedForThisUser,afSessionCreationAborted);

  /// will identify the currently running service on the server side
  // - is the type of the global ServiceContext threadvar
  // - to access the current TSQLRestServer instance (and e.g. its ORM/CRUD
  // or SOA methods), use Request.Server and not Factory.Server, which may not
  // be available e.g. if you run the service from the server side (so no
  // factory is involved)
  TServiceRunningContext = record
................................................................................
    // - should set Service member (and possibly ServiceMethodIndex)
    // - abstract implementation which is to be overridden
    procedure URIDecodeSOAByInterface; virtual; abstract;
    /// process authentication
    // - return FALSE in case of invalid signature, TRUE if authenticated
    function Authenticate: boolean; virtual;
    /// method called in case of authentication failure
    // - the failure origin is stated by the Reason parameter
    // - this default implementation will just set OutStatus := HTML_FORBIDDEN

    // and call TSQLRestServer.OnAuthenticationFailed event (if any)
    procedure AuthenticationFailed(Reason: TNotifyAuthenticationFailedReason); virtual;
    /// direct launch of a method-based service
    // - URI() will ensure that MethodIndex>=0 before calling it
    procedure ExecuteSOAByMethod; virtual;
    /// direct launch of an interface-based service
    // - URI() will ensure that Service<>nil before calling it
    // - abstract implementation which is to be overridden
    procedure ExecuteSOAByInterface; virtual; abstract;
................................................................................
  TNotifyFieldSQLEvent = function(Sender: TSQLRestServer; Event: TSQLEvent;
    aTable: TSQLRecordClass; const aID: TID; const aAffectedFields: TSQLFieldBits): boolean of object;
  /// session-related callbacks triggered by TSQLRestServer
  // - for OnSessionCreate, returning TRUE will abort the session creation -
  // and you can set Ctxt.Call^.OutStatus to a corresponding error code
  TNotifySQLSession = function(Sender: TSQLRestServer; Session: TAuthSession;
    Ctxt: TSQLRestServerURIContext): boolean of object;
  /// callback raised in case of authentication failure
  // - as used by TSQLRestServerURIContext.AuthenticationFailed event
  TNotifyAuthenticationFailed = procedure(Sender: TSQLRestServer;
    Reason: TNotifyAuthenticationFailedReason; Session: TAuthSession;
    Ctxt: TSQLRestServerURIContext) of object;

  TSQLRestStorageInMemory = class;
  TSQLVirtualTableModule = class;

  /// class-reference type (metaclass) of our abstract table storage
  // - may be e.g. TSQLRestStorageInMemory, TSQLRestStorageInMemoryExternal,
  // TSQLRestStorageExternal or TSQLRestStorageMongoDB
................................................................................
    // TSQLAuthGroup instance for fast retrieval in TSQLRestServer.URI
    function GetUser(Ctxt: TSQLRestServerURIContext;
      const aUserName: RawUTF8): TSQLAuthUser; virtual;
    /// create a session on the server for a given user
    // - this default implementation will call fServer.SessionCreate() and
    // return a '{"result":"HEXASALT","logonname":"UserName"}' JSON content
    // and will always call User.Free
    // - on failure, will call TSQLRestServerURIContext.AuthenticationFailed()
    // with afSessionAlreadyStartedForThisUser or afSessionCreationAborted reason
    procedure SessionCreate(Ctxt: TSQLRestServerURIContext; var User: TSQLAuthUser); virtual;
    /// abstract method which will be called by ClientSetUser() to process the
    // authentication step on the client side
    // - at call, a TSQLAuthUser instance will be supplied, with LogonName set
    // with aUserName and PasswordHashHexa with a SHA-256 hash of aPassword
    // - override with the expected method, returning the session key on success
    class function ClientComputeSessionKey(Sender: TSQLRestClientURI; User: TSQLAuthUser): RawUTF8;
................................................................................
    /// this method is overridden for setting the NoAJAXJSON field
    // of all associated TSQLRestStorage servers
    procedure SetNoAJAXJSON(const Value: boolean); virtual;
    /// add a new session to the internal session list
    // - do not use this method directly: this callback is to be used by
    // TSQLRestServerAuthentication* classes
    // - will check that the logon name is valid
    // - on failure, will call TSQLRestServerURIContext.AuthenticationFailed()
    // with afSessionAlreadyStartedForThisUser or afSessionCreationAborted reason
    procedure SessionCreate(var User: TSQLAuthUser; Ctxt: TSQLRestServerURIContext;
      out Session: TAuthSession); virtual;
    /// fill the supplied context from the supplied aContext.Session ID
    // - returns nil if not found, or fill aContext.User/Group values if matchs
    // - this method will also check for outdated sessions, and delete them
    // - this method is not thread-safe: caller should use fSessions.Lock
    function SessionAccess(Ctxt: TSQLRestServerURIContext): TAuthSession;
................................................................................
    // (DenyOfService attack?) or the request is not valid (ManIntheMiddle attack?)
    // - e.g. if the URI signature is invalid, or OnSessionCreate event handler
    // aborted the session creation by returning TRUE (in this later case,
    // the Session parameter is not nil)
    // - you can access the current execution context from the Ctxt parameter,
    // e.g. to retrieve the caller's IP and ban aggressive users:
    // ! FindIniNameValue(pointer(Ctxt.Call^.InHead),'REMOTEIP: ')
    OnAuthenticationFailed: TNotifyAuthenticationFailed;
    /// a method can be specified to be notified when a session is closed
    // - for OnSessionClosed, the returning boolean value is ignored
    // - Ctxt is nil if the session is closed due to a timeout
    // - Ctxt is not nil if the session is closed explicitly by the client
    OnSessionClosed: TNotifySQLSession;
    /// this event will be executed to push notifications from the server to
    // a remote client, using a (fake) interface parameter
................................................................................
      result := Server.fPublishedMethod[MethodIndex].ByPassAuthentication;
  end else begin // default unique session if authentication is not enabled
    Session := CONST_AUTHENTICATION_NOT_USED;
    result := true;
  end;
end;

procedure TSQLRestServerURIContext.AuthenticationFailed(
  Reason: TNotifyAuthenticationFailedReason);
begin
  // 401 Unauthorized response MUST include a WWW-Authenticate header,
  // which is not what we used, so here we won't send 401 error code but 403
  Call.OutStatus := HTML_FORBIDDEN;
  // call the notification event
  if Assigned(Server.OnAuthenticationFailed) then
    Server.OnAuthenticationFailed(Server,Reason,nil,self);
end;

destructor TSQLRestAcquireExecution.Destroy;
begin
  inherited Destroy;
  Thread.Free;
end;
................................................................................
    if (RootRedirectGet<>'') and (Ctxt.Method=mGet) and
       (Call.Url=Model.Root) and (Call.InBody='') then
      Ctxt.Redirect(RootRedirectGet) else begin
      Ctxt.URIDecodeSOAByMethod;
      if (Ctxt.MethodIndex<0) and (Ctxt.URI<>'') then
        Ctxt.URIDecodeSOAByInterface;
      // 2. handle security
      if not Ctxt.Authenticate then
        Ctxt.AuthenticationFailed(afInvalidSignature) else
      if (Ctxt.Service<>nil) and
          not (reService in Call.RestAccessRights^.AllowRemoteExecute) then
        Ctxt.AuthenticationFailed(afRemoteServiceExecutionNotAllowed) else
      // 3. call appropriate ORM / SOA commands in fAcquireExecution[] context
      try
        if Ctxt.MethodIndex>=0 then
          if Ctxt.MethodIndex=fPublishedMethodBatchIndex then
            Ctxt.Command := execORMWrite else
            Ctxt.Command := execSOAByMethod else
        if Ctxt.Service<>nil then
................................................................................
    for i := 0 to fSessions.Count-1 do
      if TAuthSession(fSessions.List[i]).User.fID=User.fID then begin
        {$ifdef WITHLOG}
        with TAuthSession(fSessions.List[i]) do
          Ctxt.Log.Log(sllUserAuth,'User.LogonName=% already connected from "%/%"',
            [User.LogonName,RemoteIP,Ctxt.Call^.LowLevelConnectionID],self);
        {$endif}
        Ctxt.AuthenticationFailed(afSessionAlreadyStartedForThisUser);


        exit; // user already connected 
      end;
  Session := fSessionClass.Create(Ctxt,User);
  if Assigned(OnSessionCreate) then
    if OnSessionCreate(self,Session,Ctxt) then begin // TRUE aborts session creation
      {$ifdef WITHLOG}
      Ctxt.Log.Log(sllUserAuth,'Session aborted by OnSessionCreate() callback '+
         'for User.LogonName=% (connected from "%/%") - clients=%, sessions=%',
        [User.LogonName,Session.RemoteIP,Ctxt.Call^.LowLevelConnectionID,
         fStats.ClientsCurrent,fSessions.Count],self);
      {$endif}
      Ctxt.AuthenticationFailed(afSessionCreationAborted);

      User := nil;
      FreeAndNil(Session);
      exit;
    end;
  User := nil; // will be freed by TAuthSession.Destroy
  fSessions.Add(Session);
  fStats.ClientConnect;
end;

................................................................................

constructor TAuthSession.Create(aCtxt: TSQLRestServerURIContext; aUser: TSQLAuthUser);
var GID: TSQLAuthGroup;
begin
  fUser := aUser;
  if (aCtxt<>nil) and (User<>nil) and (User.fID<>0) then begin
    GID := User.GroupRights; // save pseudo TSQLAuthGroup = ID
    User.GroupRights := aCtxt.Server.fSQLAuthGroupClass.Create(aCtxt.Server,GID);
    if User.GroupRights.fID<>0 then begin
      // compute the next Session ID
      with aCtxt.Server do begin
        if fSessionCounter>=cardinal(maxInt) then
          fSessionCounter := 10 else
          if fSessionCounter=75 then // avoid IDCardinal=0 (77) or 1 (76)
            fSessionCounter := 78 else
................................................................................
end;

procedure TSQLRestServerAuthentication.SessionCreate(Ctxt: TSQLRestServerURIContext;
  var User: TSQLAuthUser);
var Session: TAuthSession;
begin
  if User<>nil then

  try // now client is authenticated -> create a session
    fServer.SessionCreate(User,Ctxt,Session); // call Ctxt.AuthenticationFailed on error
    if Session<>nil then
      Ctxt.Returns(['result',Session.fPrivateSalt,'logonname',Session.User.LogonName]);
  finally
    User.Free;
  end;
end;

................................................................................
  aClientNonce := Ctxt.InputUTF8OrVoid['ClientNonce'];
  if (aUserName<>'') and (aClientNonce<>'') then begin
    // GET ModelRoot/auth?UserName=...&PassWord=...&ClientNonce=... -> handshaking
    User := GetUser(Ctxt,aUserName);
    if User<>nil then
    try
      // check if match TSQLRestClientURI.SetUser() algorithm
      if CheckPassword(Ctxt,User,aClientNonce,aPassWord) then

        SessionCreate(Ctxt,User) else // will call Ctxt.AuthenticationFailed on error


        Ctxt.AuthenticationFailed(afInvalidPassword);
    finally
      User.Free;
    end else
      Ctxt.AuthenticationFailed(afUnknownUser);
  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 -> try next authentication
      result := false;
end;
................................................................................

function TSQLRestServerAuthenticationNone.Auth(Ctxt: TSQLRestServerURIContext): boolean;
var aUserName: RawUTF8;
    U: TSQLAuthUser;
begin
  aUserName := Ctxt.InputUTF8OrVoid['UserName'];
  if aUserName='' then begin
    result := false; // let's try another TSQLRestServerAuthentication class
    exit;
  end;
  result := true; // this kind of weak authentication avoid stronger ones 
  if AuthSessionRelease(Ctxt) then
    exit;
  U := GetUser(Ctxt,aUserName);
  if U=nil then
    Ctxt.AuthenticationFailed(afUnknownUser) else
    SessionCreate(Ctxt,U); // call Ctxt.AuthenticationFailed on error
end;

class function TSQLRestServerAuthenticationNone.ClientComputeSessionKey(
  Sender: TSQLRestClientURI; User: TSQLAuthUser): RawUTF8;
begin
  result := Sender.CallBackGetResult('Auth',['UserName',User.LogonName]);
end;
................................................................................
  if GetUserPassFromInHead(Ctxt,userPass,user,pass) then begin
    U := GetUser(Ctxt,user);
    if U<>nil then
    try
      expectedPass := U.PasswordHashHexa;
      U.PasswordPlain := pass; // override with SHA-256 hash from HTTP header
      if U.PasswordHashHexa=expectedPass then begin
        fServer.SessionCreate(U,Ctxt,Session); // call Ctxt.AuthenticationFailed on error
        if Session<>nil then begin
          // see TSQLRestServerAuthenticationHttpAbstract.ClientSessionSign()
          Ctxt.SetOutSetCookie((COOKIE_SESSION+'=')+CardinalToHex(Session.IDCardinal));
          Ctxt.Returns(['result',Session.IDCardinal,'logonname',Session.User.LogonName]);
          exit; // success
        end;
      end else
        Ctxt.AuthenticationFailed(afInvalidPassword);
    finally
      U.Free;
    end else
      Ctxt.AuthenticationFailed(afUnknownUser);
  end else begin
    Ctxt.Call.OutHead := 'WWW-Authenticate: Basic realm="mORMot Server"';;
    Ctxt.Error('',HTML_UNAUTHORIZED); // will popup for credentials in browser
  end;
end;


................................................................................
  try
    if UserName='' then
      exit;
    User := GetUser(Ctxt,UserName);
    if User<>nil then
    try
      User.PasswordHashHexa := ''; // override with context
      fServer.SessionCreate(User,Ctxt,Session); // call Ctxt.AuthenticationFailed on error
      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 else
      Ctxt.AuthenticationFailed(afUnknownUser);
  finally
    FreeSecContext(fSSPIAuthContexts[SecCtxIdx]);
    CtxArr.Delete(SecCtxIdx);
  end;
end;

class function TSQLRestServerAuthenticationSSPI.ClientComputeSessionKey(

Changes to SynopseCommit.inc.

1
'1.18.1258'
|
1
'1.18.1259'