#1 2012-12-05 09:23:58

Eric
Member
Registered: 2012-11-26
Posts: 129
Website

Weird timing issue in ServerSSPIAuth

I'm experimenting with single-sign on using www-auth and ServerSSPIAuth, which involves challenge-response on an http connection.

I've hacked Request() to pass the ConnectionID so I can track the connection (that parts works fine)

However, I'm getting random logon failed in ServerSSPIAuth when the server runs, but if I have a breakpoint in the IDE before the call to ServerSSPIAuth, which effectively imposes a delay, then the authentication always succeeds AFAICT.

AcceptSecurityContext and the rest of SSPI are supposed to be thread-safe, according to MS docs.

The issue can also happen when there is a single client attempting to connect (so it's not a simultaneous authentication issue)

Any ideas?

Offline

#2 2012-12-05 10:04:04

Eric
Member
Registered: 2012-11-26
Posts: 129
Website

Re: Weird timing issue in ServerSSPIAuth

Offline

#3 2012-12-05 14:24:07

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

Re: Weird timing issue in ServerSSPIAuth

It is difficult to find what is wrong without the code to be looked at.

I suspect this is not a problem of threads, since authentication at mORMot level (in mORmot.pas unit) is reported to work in multi-thread context.

On which code did you base your implementation?

Offline

#4 2012-12-06 11:49:27

Eric
Member
Registered: 2012-11-26
Posts: 129
Website

Re: Weird timing issue in ServerSSPIAuth

I've added that to the SynopseWebServer demo in DWS

uses dwsUtils;

type

   TNTLMAuth = class
      SecContext : TSecContext;
      User : RawUTF8;
   end;

var
   vPendingAuth : TFastCompareStringList;
   vPendingAuthCS : TFixedCriticalSection;

// and in Process method

   vPendingAuthCS.Enter;
   try
      p:=vPendingAuth.IndexOf(IntToHex(connectionId, 16));
      if p>=0 then begin
         auth:=TNTLMAuth(vPendingAuth.Objects[p]);
      end else begin
         auth:=TNTLMAuth.Create;
         InvalidateSecContext(auth.SecContext);
         vPendingAuth.AddObject(IntToHex(connectionId, 16), auth);
      end;
   finally
      vPendingAuthCS.Leave;
   end;

   if auth.User='' then begin

      auth64:=request.Header('Authorization');
      if auth64='' then begin
         OutCustomHeader:='WWW-Authenticate: NTLM'#13#10;
         OutContentType:=HTML_CONTENT_TYPE;
         OutContent:='<HTML><BODY>401 Unauthorized.</BODY></HTML>';
         Result:=401;
         Exit;
      end else if ServerSSPIAuth(auth.SecContext, RawDecode64(RawByteString(StrAfterChar(auth64, ' '))), OutContent) then begin
         OutCustomHeader:='WWW-Authenticate: NTLM '+BinToBase64(OutContent)+#13#10;
         Result:=401;
         Exit;
      end;

      ServerSSPIAuthUser(auth.SecContext, auth.User);

   end;

   OutContent:=auth.User;

To the Process method I just added a connectionId parameter, which takes the Req^.ConnectionId for the http.sys server, and a Int64(Socket) for the thread/socket-based one.

RawDecode64 is just an alternate base64 decoder I tried, but results are the same as with the Synopse one.

Offline

#5 2012-12-06 12:42:20

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

Re: Weird timing issue in ServerSSPIAuth

Is NTLM required after WWW-Authenticate: ?

Offline

#6 2012-12-06 15:28:58

Eric
Member
Registered: 2012-11-26
Posts: 129
Website

Re: Weird timing issue in ServerSSPIAuth

ab wrote:

Is NTLM required after WWW-Authenticate: ?

Yes, see http://www.innovation.ch/personal/ronald/ntlm.html
For Kerberos, the sequence is the same with "Negotiate" in place of "NTLM". That's more secure but requires setting up an SPN, which is overkill when doing the auth inside a LAN or over a VPN.

Offline

#7 2012-12-06 15:35:38

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

Re: Weird timing issue in ServerSSPIAuth

No luck with StackOverflow, either...

Did you check http://synopse.info/forum/viewtopic.php?pid=5809#p5809 ?

Offline

#8 2012-12-10 07:23:56

Eric
Member
Registered: 2012-11-26
Posts: 129
Website

Re: Weird timing issue in ServerSSPIAuth

Unless I'm mistaken this is an URI-based scheme? ie. the user info needs to be entered or saved on the client-side in some fashion.

WWW-Authenticate allow that to be resolved by the browser from OS user session info, besides single-sign-on convenience, it can also be made a bit more secure in a LAN since OS logons can be more richly administered, can be tied to biometric systems, etc. all things which would be problematic (or time consuming) to reproduce in a web browser.
Besides, it also means there is no need to have user administration in the web app.

Offline

#9 2012-12-10 07:50:52

Eric
Member
Registered: 2012-11-26
Posts: 129
Website

Re: Weird timing issue in ServerSSPIAuth

Below is a test app based on the web server, to compile it you should need to expand the Process method to pass the connectionId.
other units are from the DWS svn

program WWWAuth;

{$APPTYPE CONSOLE}

uses
  SysUtils,
  SynCommons,
  SynCrtSock,
  dwsUtils,
  dwsWebEnvironment,
  dwsSynopseWebEnv,
  SynSSPIAuth;

type

   TNTLMAuth = class
      SecContext : TSecContext;
      User : RawUTF8;
   end;

   TTestServer = class
      protected
         FServer : THttpApiServer;

      public
         constructor Create(const basePath : TFileName);
         destructor Destroy; override;

         function Process(const InURL, InMethod, InHeaders, InContent, InContentType: RawByteString;
                          out OutContent, OutContentType, OutCustomHeader: RawByteString;
                          connectionId : Int64) : cardinal;
  end;

var
   vPendingAuth : TFastCompareStringList;
   vPendingAuthCS : TFixedCriticalSection;

{ TTestServer }

constructor TTestServer.Create(const basePath : TFileName);
begin
   FServer:=THttpApiServer.Create(false);
   FServer.AddUrl('', '888', false,'+');
   // FServer.RegisterCompress(CompressDeflate); // our server will deflate html :)
   FServer.OnRequest:=Process;
end;

destructor TTestServer.Destroy;
begin
   FServer.Free;
   inherited;
end;

{$WARN SYMBOL_PLATFORM OFF}

function TTestServer.Process(
      const InURL, InMethod, InHeaders, InContent, InContentType: RawByteString;
      out OutContent, OutContentType, OutCustomHeader: RawByteString;
      connectionId : Int64): cardinal;
var
   p : Integer;
   request : TSynopseWebRequest;
   auth : TNTLMAuth;
   auth64 : String;
begin
   request:=TSynopseWebRequest.Create;
   try
      request.InURL:=InURL;
      request.InMethod:=InMethod;
      request.InHeaders:=InHeaders;
      request.InContent:=InContent;
      request.InContentType:=InContentType;

      vPendingAuthCS.Enter;
      try
         p:=vPendingAuth.IndexOf(IntToHex(connectionId, 16));
         if p>=0 then begin
            auth:=TNTLMAuth(vPendingAuth.Objects[p]);
         end else begin
            auth:=TNTLMAuth.Create;
            InvalidateSecContext(auth.SecContext);
            vPendingAuth.AddObject(IntToHex(connectionId, 16), auth);
         end;
      finally
         vPendingAuthCS.Leave;
      end;

      if auth.User='' then begin

         auth64:=request.Header('Authorization');
         if auth64='' then begin
            OutCustomHeader:='WWW-Authenticate: NTLM'#13#10;
            OutContentType:=HTML_CONTENT_TYPE;
            OutContent:='<HTML><BODY>401 Unauthorized.</BODY></HTML>';
            Result:=401;
            Exit;
         end else if ServerSSPIAuth(auth.SecContext, Base64Decode(RawByteString(StrAfterChar(auth64, ' '))), OutContent) then begin
            OutCustomHeader:='WWW-Authenticate: NTLM '+BinToBase64(OutContent)+#13#10;
            OutContentType:=HTML_CONTENT_TYPE;
            OutContent:='<HTML><BODY>401 Unauthorized.</BODY></HTML>';
            Result:=401;
            Exit;
         end;

         ServerSSPIAuthUser(auth.SecContext, auth.User);

      end;

      OutContent:='<html><body>Logged on as '+auth.User+'</body></html>';

      OutContentType:=HTML_CONTENT_TYPE;
      Result:=200;
   finally
      request.Free;
   end;
end;

begin
   vPendingAuth:=TFastCompareStringList.Create;
   vPendingAuthCS:=TFixedCriticalSection.Create;

   with TTestServer.Create('') do try
      write('Server is now running on http://localhost:888/'#13#10#13#10+
            'Press [Enter] to quit');
      readln;
   finally
      Free;
  end;
end.

Offline

#10 2012-12-10 08:06:32

Chaa
Member
Registered: 2011-03-26
Posts: 219

Re: Weird timing issue in ServerSSPIAuth

In the code above, you you are using NTLM security package for authentication, and Negotiate is used in module SysSSPIAuth.pas (const SecPkgName = 'Negotiate').
Try to fix the code:

if auth64='' then begin
    OutCustomHeader:='WWW-Authenticate: Negotiate'#13#10;
    ...
end else if ServerSSPIAuth(auth.SecContext, ...) then begin
    OutCustomHeader:='WWW-Authenticate: Negotiate '+BinToBase64(OutContent)+#13#10;
    ...
end;

Offline

#11 2012-12-10 15:47:34

Eric
Member
Registered: 2012-11-26
Posts: 129
Website

Re: Weird timing issue in ServerSSPIAuth

Chaa wrote:

In the code above, you you are using NTLM security package for authentication, and Negotiate is used in module SysSSPIAuth.pas (const SecPkgName = 'Negotiate').

You can use Negotiate as well (for Kerberos), but that requires setting up SPN & other site prefixes, which complicates testing the code.

SSPI supports both, I've tried boths, and have the same issue of connection randomly failing in the same conditions, ie. they (AFAICT) never fail for local connections or when a breakpoint is set in the IDE, but have about 50% chance of succeeding when connecting from other machines on the LAN and there is no debugger breakpoints set.

This is so very weird (especially the debugger breakpoint thing), I'm wondering if the issue isn't with the domain controllers here: if one of the controllers were misconfigured in some fashion, it would explain the 50% success rate. Though it wouldn't explain the breakpoints thing or why we don't see the issue from Windows... (unless the OS were to silently fallback).

I guess if someone else tests and can't reproduce my issues, then that'll definitely point the finger at the domain here.

Offline

#12 2012-12-11 10:49:36

Chaa
Member
Registered: 2011-03-26
Posts: 219

Re: Weird timing issue in ServerSSPIAuth

I added some test code to your project. Periodically in the log appears messages of wrong base64-encoded lines, and AcceptSecurityContext returns ERROR_INVALID_PARAMETER, so auth failed.

const
  b64: array[0..64] of AnsiChar =
    'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
...
          if auth.User='' then begin
             auth64:=request.Header('Authorization');
             if auth64<>'' then
                AuthText := StringToUTF8(StrAfterChar(auth64, ' '));

             for i := 1 to Length(AuthText) do
             begin
                 Found := False;
                 for j := Low(b64) to High(b64) do
                 begin
                     if AuthText[i] = b64[j] then
                     begin
                         Found := True;
                         break;
                     end;
                 end;
                 if not Found then
                     vLog.Add.Log(sllInfo, 'Invalid base 64 char % at %: %', [Ord(AuthText[i]), i, Copy(AuthText, i-2, 5)]);
             end;

This is the first problem. The second problem is that in SynSSPIAuth.pas used byte ordering SECURITY_NETWORK_DREP (0), but browsers used SECURITY_NATIVE_DREP ($10). So it is necessary to correct the function calls.

    Status := InitializeSecurityContextW(@aSecContext.CredHandle, LInCtxPtr, nil,
        ISC_REQ_CONNECTION or ISC_REQ_ALLOCATE_MEMORY,
        0, $10, InDescPtr, 0, @aSecContext.CtxHandle, @OutDesc, CtxAttr, Expiry);
    Status := AcceptSecurityContext(@aSecContext.CredHandle, LInCtxPtr, @InDesc,
        ASC_REQ_CONNECTION or ASC_REQ_ALLOCATE_MEMORY,
        $10, @aSecContext.CtxHandle, @OutDesc, CtxAttr, Expiry);

P.S.
I modified code from post #9 and now it works fine for me.

             if auth64<>'' then
                //AuthText := StringToUTF8(StrAfterChar(auth64, ' '));
                AuthText := FindIniNameValue(PUTF8Char(InHeaders), 'AUTHORIZATION: NTLM ');

Last edited by Chaa (2012-12-11 10:58:59)

Offline

#13 2012-12-12 08:25:23

Eric
Member
Registered: 2012-11-26
Posts: 129
Website

Re: Weird timing issue in ServerSSPIAuth

Chaa wrote:

I added some test code to your project. Periodically in the log appears messages of wrong base64-encoded lines [...]

Darn! Thanks for looking it up!

I found the issue: the headers parser was applying UrlDecode, which occasionally turned a '+' in the base64 encoding to a ' ', and since ' ' is allowed (and ignored) by base64 coders, there was no error thrown there... *feels like a fool* as I thought base64 was immune to UTF8 & encoding issues, which it just wasn't sad

@ab: any chance of surfacing the already parsed headers array in SynCrtSock? This would have allowed this mistake, and avoided the double-parsing

Offline

#14 2012-12-12 21:53:27

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

Re: Weird timing issue in ServerSSPIAuth

There is already an IsBase64() optimized function in SynCommons.pas.

Nice catch!

Offline

Board footer

Powered by FluxBB