#1 2019-01-09 21:40:35

EMartin
Member
From: Buenos Aires - Argentina
Registered: 2013-01-09
Posts: 332

JWT and interfaced based services with session

Hi @ab,

  I use interfaced based service with session for our applications, but for external integration I need that the user can invoke such services but with JWT, that is, using JWT for validate the request without existing session. Is this posible ?

Thanks.


Esteban

Offline

#2 2019-01-10 16:11:04

ab
Administrator
From: France
Registered: 2010-06-21
Posts: 13,048
Website

Re: JWT and interfaced based services with session

Currently, JWT is not integrated with mORMot sessions.
Either you use the regular mORMot authentication classes - with URI signature - or you use a JWT.

But you could extract any information stored within the JWT when calling a method-based or interface-based services, from the execution context.

So in practice, what we do is:
1. use mORMot sessions for internal Delphi/FPC client communication
2. use JWT for public API, which won't use mORMot sessions, but a dedicated authentication service which returns a JWT for a given user

Offline

#3 2019-01-10 16:52:50

EMartin
Member
From: Buenos Aires - Argentina
Registered: 2013-01-09
Posts: 332

Re: JWT and interfaced based services with session

Ok, I can confirm that when calling an interface based service without session signature the invokation never arrives to the service implementation because Ctxt.Authenticate reject by invalid signature. This not allow invoke interface based service with a valid JWT. May be I'm doing something wrong.

If I implement a method based service that return a valid JWT, how invoke then interface based service with these JWT ?

Thanks.


Esteban

Offline

#4 2019-01-10 17:42:08

ab
Administrator
From: France
Registered: 2010-06-21
Posts: 13,048
Website

Re: JWT and interfaced based services with session

You can call an interface-based service with a valid JWT using TSQLRestServer.JWTForUnauthenticatedRequest.

The only restriction is that the interface is should be defined as sicShared or sicSingle, since there is no mORMot user (nor session) associated with the JWT.

Offline

#5 2019-01-10 18:09:30

EMartin
Member
From: Buenos Aires - Argentina
Registered: 2013-01-09
Posts: 332

Re: JWT and interfaced based services with session

I have created the TSQLRestServer.JWTForUnauthenticatedRequest and the interface is defined as sicShared.

For example, this is my code excerpt in the REST server creation:

TARIS.Run // TARIS is a container 
  ...
  // 6. Se crea el servidor REST para poder hacer migraciones si es necesario
  // - HandleAuthentication debe ser true para poder manejar usuarios
  fRestServer := TARISRestServer.Create(fModel, fARISDB, fConfig.RESTServer.HandleAuthentication);
  fRestServer.ServicesRouting := TSQLRestRoutingREST_JWT;
  // - Se permite autenticación/autorización básica (Authorization: Basic ...)
  fRestServer.AuthenticationRegister(TSQLRestServerAuthenticationHttpBasic);
  // - Se permite JWT para peticiones sin sesión
  fRestServer.JWTForUnauthenticatedRequest := TJWTHS256.Create('secret',10,[jrcIssuer,jrcSubject],[]);
  ...
end;

TARIS.LoadServices
var
  lGroupID: TID;
begin
  lGroupID := fRestServer.MainFieldID(TSQLAuthGroupApproach,DEFAULT_GROUP_APP_APPROACH);
  fRestServer.ServiceDefine(TDBService, [IDBService], sicShared, CONTRACT_SERVICE_DB).AllowAllByID([lGroupID]);
  fRestServer.ServiceDefine(TContactDataService, [IContactDataService], sicShared, CONTRACT_SERVICE_CONTACTDATA).AllowAllByID([lGroupID]);
  fRestServer.ServiceDefine(TSchedulerService, [ISchedulerService], sicShared, CONTRACT_SERVICE_SCHEDULER).AllowAllByID([lGroupID]);
  ...
end;

I invoke from client side (testing) in the following way:

var
  lResp: SockString;
begin
  lResp :=  TWinHTTP.Post('https://localhost:8898/svc/ContactDataService.UpdateByRecId',
                          '{"aTableName":"TEST01","aRecId":1,"aData":{"PHONE":"1"}}',
                          JSON_CONTENT_TYPE_HEADER+sLineBreak+AuthorizationBearer(
                          'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoidXNlciIsInBhc3N3b3JkIjoic3lub3BzZSIsImlzcyI6IlRlY25vVm96IFMuQS4iL'+
                          'CJzdWIiOiJBUklTIEpXVCJ9.sYUHk0wIHU-J-Jha5IMyUAaGon-fVKKFjR8oUzcDhSc'));
fVKKFjR8oUzcDhSc'),@lResp);
  ShowMessage(lResp);

When is executed, the server return 403 error because Ctxt.Authenticate reject by invalid signature.

Is this correct way ?

What else should I do ?

Thanks.


Esteban

Offline

#6 2019-01-10 18:36:54

ab
Administrator
From: France
Registered: 2010-06-21
Posts: 13,048
Website

Re: JWT and interfaced based services with session

I would use #13#10 instead of sLineBreak, but it sounds fine.

Please check https://synopse.info/files/html/Synopse … l#TITL_198
(I have just enhanced the documentation about JWT)

Offline

#7 2019-01-10 21:47:11

EMartin
Member
From: Buenos Aires - Argentina
Registered: 2013-01-09
Posts: 332

Re: JWT and interfaced based services with session

@ab, this not works, I had to do the following to make it work:

Implement the TSQLRestRoutingREST for override Authenticate function:

  /// Clase que implementa una customización del manejo de peticiones REST
  TSQLRestRoutingREST_JWT = class(TSQLRestRoutingREST)
  protected
    // Se incorpora la autenticación con JWT
    function Authenticate: boolean; override;
  end;

function TSQLRestRoutingREST_JWT.Authenticate: boolean;
begin
  Result := inherited Authenticate;
  if (not Result) then
    Result := (Session = CONST_AUTHENTICATION_SESSION_NOT_STARTED) and (Server.JWTForUnauthenticatedRequest<>nil);
  if Result then
      Session := CONST_AUTHENTICATION_NOT_USED; //--> force call Ctxt.AuthenticationCheck(fJWTForUnauthenticatedRequest) in TSQLRestServer.URI
end;

and I assigned this TSQLRoutingREST to the REST server:

...
  // 6. Se crea el servidor REST para poder hacer migraciones si es necesario
  // - HandleAuthentication debe ser true para poder manejar usuarios
  fRestServer := TARISRestServer.Create(fModel, fARISDB, fConfig.RESTServer.HandleAuthentication);
  fRestServer.ServicesRouting := TSQLRestRoutingREST_JWT; //--> custom routing rest class
  // - Se permite autenticación/autorización básica (Authorization: Basic ...)
  fRestServer.AuthenticationRegister(TSQLRestServerAuthenticationHttpBasic);
  // - Se permite JWT para peticiones sin sesión
  fRestServer.JWTForUnauthenticatedRequest := TJWTHS256.Create('secret',10,[jrcIssuer,jrcSubject],[]);
...

Is this correct this way ? Is there better way ?

Thanks.


Esteban

Offline

#8 2019-08-06 15:21:52

moctes
Member
From: Mexico
Registered: 2013-05-11
Posts: 126

Re: JWT and interfaced based services with session

Hi @ab, @EMartin

Is this the ultimate solution ? I have a similar requirement, give access to a third party to some interface based services and I will like to use JWT for that.

Couldn't find any example project using JWT and I thought it would be better asking before start coding

Thank you

Offline

#9 2019-08-06 19:11:05

EMartin
Member
From: Buenos Aires - Argentina
Registered: 2013-01-09
Posts: 332

Re: JWT and interfaced based services with session

I used what I wrote in my last post.


Esteban

Offline

#10 2019-08-07 14:22:33

moctes
Member
From: Mexico
Registered: 2013-05-11
Posts: 126

Re: JWT and interfaced based services with session

EMartin wrote:

I used what I wrote in my last post.

Thank you Esteban, well I'll have to dig into that.

Offline

#11 2019-08-08 12:02:42

Chris75018
Member
From: France
Registered: 2012-09-14
Posts: 26
Website

Re: JWT and interfaced based services with session

Hi,

I have done this to keep session :

Implement a TSQLRestServerAuthenticationHttpBasic :

  TSQLRestServerAuthenticationJWT = class(TSQLRestServerAuthenticationHttpBasic)
  protected
    procedure SessionCreate(Ctxt: TSQLRestServerURIContext; var User: TSQLAuthUser); override;
    function getAlgo(const Value : RawUTF8) : TSignAlgo;
    procedure AuthenticationFailed(Ctxt: TSQLRestServerURIContext; Reason: TNotifyAuthenticationFailedReason);
  public
    function RetrieveSession(Ctxt: TSQLRestServerURIContext): TAuthSession; override;
    function Auth(Ctxt: TSQLRestServerURIContext): boolean; override;
  end;
....
procedure TSQLRestServerAuthenticationJWT.SessionCreate(
  Ctxt: TSQLRestServerURIContext; var User: TSQLAuthUser);
var Session: TAuthSession;
    i : Integer;
    Token : RawUTF8;
    jWtClass : TJWTSynSignerAbstractClass;
    vUser, Signat : RawUTF8;
begin
  vUser := User.LogonName;
  inherited SessionCreate(Ctxt, User);
  if Ctxt.Call^.OutStatus = HTTP_SUCCESS then begin
    if fServer.JWTForUnauthenticatedRequest <> nil then begin
      jwtClass := JWT_CLASS[getAlgo(fServer.JWTForUnauthenticatedRequest.Algorithm)];
      (fServer.JWTForUnauthenticatedRequest as jwtClass).SignPrepared.Init(saSha256, SHA256(Ctxt.UserAgent + 'salt'));
      Token := fServer.JWTForUnauthenticatedRequest.Compute(['sessionkey', Ctxt.Call^.OutBody],
                                                             vUser,
                                                             'MyPortail',
                                                             '',0,60, @Signat);
      Ctxt.Call^.OutBody := Token; // <- on retourne le token
    end;
  end;
end;

function TSQLRestServerAuthenticationJWT.getAlgo(
  const Value: RawUTF8): TSignAlgo;
var i : TSignAlgo;
begin
  Result := saSha256;
  for i := low(JWT_TEXT) to High(JWT_TEXT) do
    if SameTextU(Value, JWT_TEXT[i]) then begin
      result := i;
      break;
    end;
end;

procedure TSQLRestServerAuthenticationJWT.AuthenticationFailed(Ctxt: TSQLRestServerURIContext;
  Reason: TNotifyAuthenticationFailedReason);
begin
  if Ctxt is TSQLRestRoutingREST_JWT then
    TSQLRestRoutingREST_JWT(Ctxt).AuthenticationFailed(Reason);
end;

function TSQLRestServerAuthenticationJWT.RetrieveSession(
  Ctxt: TSQLRestServerURIContext): TAuthSession;
var aUserName : RawUTF8;
    User: TSQLAuthUser;
    i : Integer;
    tmpIdsession : Cardinal;
    vSession : variant;
    pSession : PDocVariantData;
    vSessionPrivateSalt : RawUTF8;
begin
  result := inherited RetrieveSession(Ctxt);

  if result <> nil then
    Exit;

  if not Assigned(fServer.JWTForUnauthenticatedRequest) then
    Exit;

  vSessionPrivateSalt := '';
  if Ctxt.AuthenticationBearerToken <> '' then
    if Ctxt.AuthenticationCheck(fServer.JWTForUnauthenticatedRequest) then begin
      aUserName := Ctxt.JWTContent.reg[jrcIssuer];
      User := GetUser(Ctxt,aUserName);
      try
        if User <> nil then begin
          if Ctxt.Server.Sessions <> nil then begin
            if Ctxt.JWTContent.data.GetValueIndex('sessionkey') >= 0 then begin
              vSession := _Json(Ctxt.JWTContent.data.U['sessionkey']);
              if DocVariantType.IsOfType(vSession) then
                vSessionPrivateSalt := vSession.Result;
            end;

            Ctxt.Server.Sessions.Safe.Lock;
            try
              // Search session by privatesalt
              for i := 0 to Pred(Ctxt.Server.Sessions.Count) do
                if SameTextU(vSessionPrivateSalt, TAuthSession(Ctxt.Server.Sessions[i]).ID + '+' + TAuthSession(Ctxt.Server.Sessions[i]).PrivateKey) then begin
                  Result := TAuthSession(Ctxt.Server.Sessions[i]);
                  Ctxt.Session := Result.IDCardinal;
                  break;
                end;

              // Search session for User
              if result = nil then
                for i := 0 to Pred(Ctxt.Server.Sessions.Count) do
                  if TAuthSession(Ctxt.Server.Sessions[i]).User.ID = User.ID then begin
                    Result := TAuthSession(Ctxt.Server.Sessions[i]);
                    Ctxt.Session := Result.IDCardinal;
                    break;
                  end;

            finally
              Ctxt.Server.Sessions.Safe.unLock;
            end;
          end;
        end;
      finally
        User.free;
      end;
    end;
end;

function TSQLRestServerAuthenticationJWT.Auth(
  Ctxt: TSQLRestServerURIContext): boolean;
var aUserName, aPassWord : RawUTF8;
    User: TSQLAuthUser;
begin
  result := False;
  if AuthSessionRelease(Ctxt) then
    exit;

  if not Assigned(fServer.JWTForUnauthenticatedRequest) then begin
      AuthenticationFailed(Ctxt, afJWTRequired);
    Exit;
  end;

  aUserName := Ctxt.InputUTF8OrVoid['UserName'];
  aPassWord := Ctxt.InputUTF8OrVoid['Password'];

  if (aUserName<>'') and (length(aPassWord)>0) then begin
    User := GetUser(Ctxt,aUserName);
    try
      result := User<>nil;
      if result then begin
        if CheckPassword(Ctxt, User, aPassWord) then
          SessionCreate(Ctxt, User)
        else AuthenticationFailed(Ctxt, afInvalidPassword);
      end
      else AuthenticationFailed(Ctxt, afUnknownUser);
    finally
      if result then User.Free;
    end;
  end
  else AuthenticationFailed(Ctxt, afUnknownUser);
end;

Implement the TSQLRestRoutingREST for access to protected AuthenticationFailed function :

  TSQLRestRoutingREST_JWT = class(TSQLRestRoutingREST)
  protected
    procedure AuthenticationFailed(Reason: TNotifyAuthenticationFailedReason); override;
  end;
....
procedure TSQLRestRoutingREST_JWT.AuthenticationFailed(
  Reason: TNotifyAuthenticationFailedReason);
begin
  inherited AuthenticationFailed(Reason);
end;

For the Rest server creation :

   fRestServer.AuthenticationRegister(TSQLRestServerAuthenticationJWT);
   fRestServer.ServicesRouting := TSQLRestRoutingREST_JWT;
   fRestServer.JWTForUnauthenticatedRequest := fServerSettings.AuthenticationMode.JWTClass.Create('secret', 0, [jrcIssuer, jrcSubject], [], 60);

JWTForUnauthenticatedRequest instance is used in TSQLRestServerAuthenticationJWT.

Offline

#12 2019-08-08 15:22:51

moctes
Member
From: Mexico
Registered: 2013-05-11
Posts: 126

Re: JWT and interfaced based services with session

Thank you @Chris75018 I'm still busy making new services and once I finish I'll take a deep dive into this, maybe (just maybe) you will have me here asking more questions wink .

Offline

#13 2019-08-09 13:45:48

Chris75018
Member
From: France
Registered: 2012-09-14
Posts: 26
Website

Re: JWT and interfaced based services with session

Hi,

I have made modifications for use TSQLHttpClient with this authentication.

Here the complete Unit :

unit Web.Serveur.U_JWT;

interface
uses
  Windows,
  SysUtils,
  Classes,
  SynZip,
  SynLZ,
  SynCrtSock,
  {$ifndef NOHTTPCLIENTWEBSOCKETS}
  SynBidirSock, // for WebSockets
  {$endif}
  SynCrypto,    // for hcSynShaAes
  SynCommons,
  SynLog,
  mORMot,
  mORMotHttpClient;

type
  TSQLRestRoutingREST_JWT = class(TSQLRestRoutingREST)
  protected
    procedure AuthenticationFailed(Reason: TNotifyAuthenticationFailedReason); override;
  end;

  TSQLRestServerAuthenticationJWT = class(TSQLRestServerAuthenticationHttpBasic)
  protected
    function CheckPassword(Ctxt: TSQLRestServerURIContext;
      User: TSQLAuthUser; const aPassWord: RawUTF8): boolean; override;

    procedure SessionCreate(Ctxt: TSQLRestServerURIContext; var User: TSQLAuthUser); override;
    function getAlgo(const Value : RawUTF8) : TSignAlgo;
    procedure AuthenticationFailed(Ctxt: TSQLRestServerURIContext; Reason: TNotifyAuthenticationFailedReason);

    class function ClientGetSessionKey(Sender: TSQLRestClientURI;
      User: TSQLAuthUser; const aNameValueParameters: array of const): RawUTF8; override;
  public
    function RetrieveSession(Ctxt: TSQLRestServerURIContext): TAuthSession; override;
    function Auth(Ctxt: TSQLRestServerURIContext): boolean; override;

    class function ClientSetUser(Sender: TSQLRestClientURI; const aUserName, aPassword: RawUTF8;
      aPasswordKind: TSQLRestServerAuthenticationClientSetUserPassword=passClear;
      const aHashSalt: RawUTF8=''; aHashRound: integer=20000): boolean; override;
  end;

  TSQLHttpClientJWT = class(TSQLHttpClientRequest)
  private
    fJWT: RawUTF8;
  protected
    procedure InternalSetClass; override;
    function InternalRequest(const url, method: RawUTF8;
      var Header, Data, DataType: RawUTF8): Int64Rec; override;
  public
    function SetUser(const aUserName, aPassword: RawUTF8;
      aHashedPassword: Boolean=false): boolean; reintroduce;
    property jwt : RawUTF8 read fJWT write fJWT;
  end;

implementation

function HeaderOnce(const Head : RawUTF8; upper: PAnsiChar): RawUTF8;
  {$ifdef HASINLINE}inline;{$endif}
begin
  if (Head <> '') then
    result := FindIniNameValue(pointer(Head),upper)
  else result := '';
end;

{ TSQLRestRoutingREST_JWT }

procedure TSQLRestRoutingREST_JWT.AuthenticationFailed(
  Reason: TNotifyAuthenticationFailedReason);
begin
  inherited AuthenticationFailed(Reason);
end;

{ TSQLRestServerAuthenticationJWT }

function TSQLRestServerAuthenticationJWT.Auth(
  Ctxt: TSQLRestServerURIContext): boolean;
var aUserName, aPassWord : RawUTF8;
    User: TSQLAuthUser;
begin
  result := False;
  if AuthSessionRelease(Ctxt) then
    exit;

  if not Assigned(fServer.JWTForUnauthenticatedRequest) then begin
      AuthenticationFailed(Ctxt, afJWTRequired);
    Exit;
  end;

  aUserName := Ctxt.InputUTF8OrVoid['UserName'];
  aPassWord := Ctxt.InputUTF8OrVoid['Password'];

  if (aUserName<>'') and (length(aPassWord)>0) then begin
    User := GetUser(Ctxt,aUserName);
    try
      result := User<>nil;
      if result then begin
        if CheckPassword(Ctxt, User, aPassWord) then
          SessionCreate(Ctxt, User)
        else AuthenticationFailed(Ctxt, afInvalidPassword);
      end
      else AuthenticationFailed(Ctxt, afUnknownUser);
    finally
      if result then User.Free;
    end;
  end
  else AuthenticationFailed(Ctxt, afUnknownUser);
end;

procedure TSQLRestServerAuthenticationJWT.AuthenticationFailed(Ctxt: TSQLRestServerURIContext;
  Reason: TNotifyAuthenticationFailedReason);
begin
  if Ctxt is TSQLRestRoutingREST_JWT then
    TSQLRestRoutingREST_JWT(Ctxt).AuthenticationFailed(Reason);
end;

function TSQLRestServerAuthenticationJWT.CheckPassword(
  Ctxt: TSQLRestServerURIContext; User: TSQLAuthUser;
  const aPassWord: RawUTF8): boolean;
var expectedPass: RawUTF8;
begin
  expectedPass := User.PasswordHashHexa;
  User.PasswordPlain := aPassWord;
  User.PasswordHashHexa := SHA256(aPassWord);
  result := IdemPropNameU(User.PasswordHashHexa,expectedPass);
end;

class function TSQLRestServerAuthenticationJWT.ClientGetSessionKey(
  Sender: TSQLRestClientURI; User: TSQLAuthUser;
  const aNameValueParameters: array of const): RawUTF8;
var resp: RawUTF8;
    values: array[0..9] of TValuePUTF8Char;
    a: integer;
    algo: TSQLRestServerAuthenticationSignedURIAlgo absolute a;
begin
  Result := '';
  if (Sender.CallBackGet('Auth',aNameValueParameters,resp)=HTTP_SUCCESS) then
    result := resp;
end;

class function TSQLRestServerAuthenticationJWT.ClientSetUser(
  Sender: TSQLRestClientURI; const aUserName, aPassword: RawUTF8;
  aPasswordKind: TSQLRestServerAuthenticationClientSetUserPassword;
  const aHashSalt: RawUTF8; aHashRound: integer): boolean;
var res: RawUTF8;
    U: TSQLAuthUser;
    vTmp : Variant;
begin
  result := false;
  if (aUserName='') or (Sender=nil) then
    exit;
  if not Sender.InheritsFrom(TSQLHttpClientJWT) then
    exit;

  if aPasswordKind<>passClear then
    raise ESecurityException.CreateUTF8('%.ClientSetUser(%) expects passClear',
      [self,Sender]);
  Sender.SessionClose; // ensure Sender.SessionUser=nil
  try // inherited ClientSetUser() won't fit with Auth() method below
    ClientSetUserHttpOnly(Sender,aUserName,aPassword);
    TSQLHttpClientJWT(Sender).jwt := '';
    U := TSQLAuthUser(Sender.Model.GetTableInherited(TSQLAuthUser).Create);
    try
      U.LogonName := trim(aUserName);
      res := ClientGetSessionKey(Sender,U,['Username', aUserName, 'password', aPassword]);

      if res<>'' then begin
        vTmp := _JsonFast(res);
        if DocVariantType.IsOfType(vTmp) then begin
          result := TSQLHttpClientJWT(Sender).SessionCreate(self,U,TDocvariantData(vTmp).U['result']);
          if result then
            TSQLHttpClientJWT(Sender).jwt := TDocvariantData(vTmp).U['jwt'];
        end;
      end;
    finally
      U.Free;
    end;
  finally
    if not result then begin
      // on error, reverse all values
      TSQLHttpClientJWT(Sender).jwt := '';
    end;
    if Assigned(Sender.OnSetUser) then
      Sender.OnSetUser(Sender); // always notify of user change, even if failed
  end;
end;

function TSQLRestServerAuthenticationJWT.getAlgo(
  const Value: RawUTF8): TSignAlgo;
var i : TSignAlgo;
begin
  Result := saSha256;
  for i := low(JWT_TEXT) to High(JWT_TEXT) do
    if SameTextU(Value, JWT_TEXT[i]) then begin
      result := i;
      break;
    end;
end;

function TSQLRestServerAuthenticationJWT.RetrieveSession(
  Ctxt: TSQLRestServerURIContext): TAuthSession;
var aUserName : RawUTF8;
    User: TSQLAuthUser;
    i : Integer;
    tmpIdsession : Cardinal;
    vSession : variant;
    pSession : PDocVariantData;
    vSessionPrivateSalt : RawUTF8;
begin
  result := inherited RetrieveSession(Ctxt);

  if result <> nil then
    Exit;

  if not Assigned(fServer.JWTForUnauthenticatedRequest) then
    Exit;

  vSessionPrivateSalt := '';
  if Ctxt.AuthenticationBearerToken <> '' then
    if Ctxt.AuthenticationCheck(fServer.JWTForUnauthenticatedRequest) then begin
      aUserName := Ctxt.JWTContent.reg[jrcIssuer];
      User := GetUser(Ctxt,aUserName);
      try
        if User <> nil then begin
          if Ctxt.Server.Sessions <> nil then begin
            if Ctxt.JWTContent.data.GetValueIndex('sessionkey') >= 0 then begin
              vSession := _Json(Ctxt.JWTContent.data.U['sessionkey']);
              if DocVariantType.IsOfType(vSession) then
                vSessionPrivateSalt := vSession.Result;
            end;

            Ctxt.Server.Sessions.Safe.Lock;
            try
              // Search session for User
              if (reOneSessionPerUser in Ctxt.Call^.RestAccessRights^.AllowRemoteExecute) and
                (Ctxt.Server.Sessions<>nil) then
                for i := 0 to Pred(Ctxt.Server.Sessions.Count) do
                  if TAuthSession(Ctxt.Server.Sessions[i]).User.ID = User.ID then begin
                    Result := TAuthSession(Ctxt.Server.Sessions[i]);
                    Ctxt.Session := Result.IDCardinal;
                    break;
                  end;

              // Search session by privatesalt
              if result = nil then
                for i := 0 to Pred(Ctxt.Server.Sessions.Count) do
                  if SameTextU(vSessionPrivateSalt, TAuthSession(Ctxt.Server.Sessions[i]).ID + '+' + TAuthSession(Ctxt.Server.Sessions[i]).PrivateKey) then begin
                    Result := TAuthSession(Ctxt.Server.Sessions[i]);
                    Ctxt.Session := Result.IDCardinal;
                    break;
                  end;

            finally
              Ctxt.Server.Sessions.Safe.unLock;
            end;
          end;
        end;
      finally
        User.free;
      end;
    end;
end;

procedure TSQLRestServerAuthenticationJWT.SessionCreate(
  Ctxt: TSQLRestServerURIContext; var User: TSQLAuthUser);
var Session: TAuthSession;
    i : Integer;
    Token : RawUTF8;
    jWtClass : TJWTSynSignerAbstractClass;
    vPass, vUser, Signat, vSessionKey : RawUTF8;
    vTmp : Variant;
begin
  vUser := User.LogonName;
  vPass := User.PasswordHashHexa;
  inherited SessionCreate(Ctxt, User);
  if Ctxt.Call^.OutStatus = HTTP_SUCCESS then begin
    if fServer.JWTForUnauthenticatedRequest <> nil then begin
      jwtClass := JWT_CLASS[getAlgo(fServer.JWTForUnauthenticatedRequest.Algorithm)];
      Token := (fServer.JWTForUnauthenticatedRequest as jwtClass).Compute([
                                                                           'sessionkey', Ctxt.Call^.OutBody],
                                                                           vUser,
                                                                           'jwt.access',
                                                                           '',0,60, @Signat);
      vTmp := _JsonFast(Ctxt.Call^.OutBody);
      if DocVariantType.IsOfType(vTmp) then
        Ctxt.Call^.OutBody := _Obj(['result', TDocvariantData(vTmp).U['result'], 'jwt', Token]);
    end;
  end;
end;

{ TSQLHttpClientJWT }

function TSQLHttpClientJWT.InternalRequest(const url, method: RawUTF8;
  var Header, Data, DataType: RawUTF8): Int64Rec;
var vBasic : RawUTF8;
    h : Integer;
begin
  if fjwt <> '' then begin // Change Header if jwt exist
    vBasic := HeaderOnce(Header, 'AUTHORIZATION: BASIC ');
    if vBasic <> '' then begin
      h := PosEx(vBasic, Header);
      if h = 22 then
        header := copy(Header, h + Length(vBasic), Length(header))
      else header := copy(Header, 1, h - 21) + copy(Header, h + Length(vBasic), Length(header));
      header := Trim(header);
    end;
    Header := trim(HEADER_BEARER_UPPER + fJWT + #13#10 + Header);
  end;
  result := inherited InternalRequest(url, method, Header, Data, DataType);
end;

procedure TSQLHttpClientJWT.InternalSetClass;
begin
  fRequestClass := TWinHTTP;
  inherited;
end;

function TSQLHttpClientJWT.SetUser(const aUserName, aPassword: RawUTF8;
  aHashedPassword: Boolean): boolean;
const HASH: array[boolean] of TSQLRestServerAuthenticationClientSetUserPassword =
  (passClear, passHashed);
begin
  if self=nil then begin
    result := false;
    exit;
  end;
  result := TSQLRestServerAuthenticationJWT.
    ClientSetUser(self,aUserName,aPassword,HASH[aHashedPassword]);
end;

end.

TSQLRestServerAuthenticationJWT.Auth() method return a RawUTF8 JSON Object containing two fields :
- result : session privatesalt (ID + '+' + privatekey)
- jwt :  the Token

The TSQLHttpClientJWT class is to used with Delphi client.

FClient := TSQLHttpClientJWT.Create(SERVER_ADDRESS, DEFAULT_PORT, FDBModel);
if FClient.SetUser('synopse', 'mormot', False) then
....

For Ajax client, you must parse Auth() API response.

$("#submit").click(function(){
 
 var xhr = new XMLHttpRequest();
    xhr.onreadystatechange = function(event) {
      if (this.readyState === XMLHttpRequest.DONE) {
        if (this.status === 200) {
            tmp = JSON.parse(xhr.responseText);
            sessionStorage.setItem("jwt",tmp.jwt);
        } else {
            tmp = xhr.statusText;
        }
      }	  
    };

    username = $("#login").val();
    mpass = $("#mdp").val();
	
    xhr.open('GET', "http://127.0.0.1/MyServer/Auth?username=" + username + "&password=" + mpass, true);
    xhr.send(null);    
});

And use jwt value in each call :

var GET_Testjwt = function(jwt,callback){
  var xhr = new XMLHttpRequest();
   xhr.onreadystatechange = function(event) {
      if (this.readyState === XMLHttpRequest.DONE) {
        if (this.status === 200) {
            response = xhr.responseText;
        } else {
            response = xhr.statusText;
        }
       callback(200, response);
      }	  
    };
  jwt = sessionStorage.getItem("jwt");
  xhr.open('GET', "http://127.0.0.1/MyServer/" + arequest, true);
  xhr.setRequestHeader('Authorization', ' bearer ' + jwt);
  xhr.send(null);    
}

I hope I did not forget anything.
The code runs on the tests and will soon be in production. I'll see if it's okay.
Any comments or suggestions are welcome.

Offline

#14 2019-08-10 13:47:28

ab
Administrator
From: France
Registered: 2010-06-21
Posts: 13,048
Website

Re: JWT and interfaced based services with session

Thanks for sharing!

Please don't post such huge piece of code directly in the forum.
Check the https://synopse.info/forum/misc.php?action=rules

Offline

#15 2019-08-12 08:13:15

Chris75018
Member
From: France
Registered: 2012-09-14
Posts: 26
Website

Re: JWT and interfaced based services with session

Sorry Ab. I'll be careful next time.

Offline

#16 2019-08-12 08:43:13

ab
Administrator
From: France
Registered: 2010-06-21
Posts: 13,048
Website

Re: JWT and interfaced based services with session

wink

Offline

#17 2019-08-12 20:38:39

moctes
Member
From: Mexico
Registered: 2013-05-11
Posts: 126

Re: JWT and interfaced based services with session

@Chris75018  Great! Thank you

Offline

#18 2019-08-13 14:20:39

igors233
Member
Registered: 2012-09-10
Posts: 177

Re: JWT and interfaced based services with session

This is great contribution, something that's really needed when dealing with external services.
@ab it would be great if integrated into main codebase.

Offline

#19 2019-08-13 16:40:32

Chris75018
Member
From: France
Registered: 2012-09-14
Posts: 26
Website

Re: JWT and interfaced based services with session

Hi,

For those who are interested here is the latest version of this piece of code.
https://1drv.ms/u/s!Ajd1--MjHzRWqw7-27x … i?e=Oz6DWO


I will try to make a sample project quickly.
AB, if you can watch and tell me if I have not done a lot of bullshit, that would help me a lot.
Thank you.

Offline

#20 2019-08-14 10:48:43

Chris75018
Member
From: France
Registered: 2012-09-14
Posts: 26
Website

Re: JWT and interfaced based services with session

You can find a sample server here :
https://1drv.ms/u/s!Ajd1--MjHzRWqxG2SeS … N?e=1JwpMd


I'm using an overloaded version of TSQLRestDBServer to add two APIs:
- IsValidToken () to test whether the token is still active and allowed.
- RefreshToken () to regenerate it if it has expired. If the session is no longer existing, this method adds a new session.
  RefreshToken expects two parameters (User and password). 

These methods must be called with a bearer in the http header.

Compiled with Delphi XE3.

Last edited by Chris75018 (2019-08-16 07:58:01)

Offline

#21 2019-08-15 00:06:06

moctes
Member
From: Mexico
Registered: 2013-05-11
Posts: 126

Re: JWT and interfaced based services with session

Hi @Chris75018

I just downloaded your demo to give it a quick glance, and although I have XE3 on my personal computer, for this project I must use XE and sadly this :

JwtHelperForlAuthenticationMode = record helper for lAuthenticationMode

Seems not supported on XE, I will try later with XE3 but I must find out how to replace that code to make it work on XE, either way, it's very nice of you to share this code with us, thank you.

Offline

#22 2019-08-16 07:55:23

Chris75018
Member
From: France
Registered: 2012-09-14
Posts: 26
Website

Re: JWT and interfaced based services with session

Hi @moctes

i have made the correction.
You can download the sample project here
https://1drv.ms/u/s!Ajd1--MjHzRWqxG2SeS … N?e=1JwpMd

PS : I don't have XE to validate the compilation.

Offline

#23 2019-08-20 15:58:45

moctes
Member
From: Mexico
Registered: 2013-05-11
Posts: 126

Re: JWT and interfaced based services with session

@Chris75018 I couldn't find the time to check the updated demo until today, works like a Charm !!  We are still pretty busy devoloping the services and testing them without authentication, I hope to be trying out with authentication very soon, your sample and @Emartin's code are extremely useful to me.

Offline

Board footer

Powered by FluxBB