Delphi Clinic C++Builder Gate Training & Consultancy Delphi Notes Weblog Dr.Bob's Webshop
Dr.Bob's Delphi Notes Dr.Bob's Delphi Clinics Dr.Bob's Delphi Courseware Manuals
 Dr.Bob Examines... #127
See Also: Dr.Bob's Delphi Papers and Columns

Securing Chat Server with SOAP Headers
In this article, I'll demonstrate how we can secure the ASP.NET chat server with SOAP headers, so not every (un-authorised) client application can "break in" and participate in the chat.

SOAP Headers
SOAP is a standard protocol for allowing clients to connect to servers, independent of the (programming) language or operating system being used. Native requests from the client are marshalled into SOAP requests, which can be marshalled back into a native request at the server side. The same thing happens with the SOAP response. Both SOAP request and SOAP response are contained within a so-called SOAP enveloppe, which can contain an additional SOAP Header, containing extra information for the request or response. We can use the SOAP header to store and verify credentials for example, in order to determine who may access the web service (or not).

Chat Service
Right now, the Chat Web Service that I wrote several months ago, does not contain any check for credentials: anyone can send chat messages or retrieve all chat messages. This seemed like a good idea at the time, bt the chatroom soon became the victim of spammers (the same people who try to leave spam comments in my weblog).
In order to ensure that only trusted Delphi developers can access the web chatroom, the purpose of this article is to add a little security layer around the Chat Web Service.

Chat SoapHeader
We start by defining a new public class type, derived from System.Web.Services.Protocols.SoapHeader, containing the fields we need for the SOAP header authentication. This can be a username, password, pincode or whatever piece of information you require.

  type
    TChatHeader = public class(System.Web.Services.Protocols.SoapHeader)
    public
      AccessCode: String;
    end;

The next step involves adding a field to the actual chat web service to hold the TChatHeader value, followed by a private method to check the ChatHeader presence and value:

  chat = public class(System.Web.Services.WebService)
  public
    var
      ChatHeader: TChatHeader;
  private
    method CheckChatHeader: Boolean;

The implementation of the CheckChatHeader is straightforward: we need to check if the ChatHeader is assigned, and if so, if the AccessCode is correct (i.e. good enough to be allowed access). Note that I raise an exception if no ChatHeader is found (which probably means the client is not using the correct WSDL).

  method chat.CheckChatHeader: Boolean;
  begin
    Result := False;
    if not Assigned(ChatHeader) then
        raise ApplicationException.Create('No ChatHeader in SOAP Header!');
    if (ChatHeader.AccessCode.ToLower = 'delphi') then Result := True
  end;

The next step involves adding the SoapHeader attribute to the web methods that require SOAP Header authentication. The first property (or argument) of the SoapHeader attribute defines the name of the field inside the web service class to hold the SOAP Header. The second property defines the direction, which can be in, out, inout or error. In this case, the server will not put anything back in the ChatHeader, so the "in" direction is enough.

    [WebMethod]
    [SoapHeader('ChatHeader', Direction := SoapHeaderDirection.&In)]
    method SendMessage(const User, Message: string);
    [WebMethod]
    [SoapHeader('ChatHeader', Direction := SoapHeaderDirection.&In)]
    method GetMessages(const Start: DateTime): String;
    [WebMethod]
    [SoapHeader('ChatHeader', Direction := SoapHeaderDirection.&In)]
    method GetUserMessages(const User: String): String;

Chat SoapHeader Implementation
Within the three web methods, we can now call the CheckChatHeader function to verify the SOAP Header credentials. This is done in the chat.SendMessage method as follows:

  method chat.SendMessage(const User, Message: string);
  var
    Messages: ArrayList;
  begin
    if CheckChatHeader then // BS
    begin
      if not Assigned(Application[key]) then
      begin
        Messages := new ArrayList();
        Application[key] := Messages
      end
      else Messages := Application[key] as ArrayList;
      Messages.Add(ChatMessage.Create(DateTime.Now, User, Message))
    end
  end;

Note that if the CheckChatHeader fails, then the new chat message is not added to the list.
A similar modification to the implementation can be made to the GetMessages and GetUserMessages methods:

  method chat.GetMessages(const Start: DateTime): String;
  begin
    Result := '';
    if CheckChatHeader then // BS
    if Assigned(Application[key]) then
    begin
      var myEnum: IEnumerator := (Application[key] as ArrayList).GetEnumerator;
      while myEnum.MoveNext do
        if (myEnum.Current as ChatMessage).Time > Start then
          Result := Result +
            (myEnum.Current as ChatMessage).Time.ToString('yyyy-MM-dd HH:mm:ss') + ' ' +
            (myEnum.Current as ChatMessage).User + ': ' +
            (myEnum.Current as ChatMessage).Message + #13#10
    end
  end;

  method chat.GetUserMessages(const User: String): String;
  begin
    Result := '';
    if CheckChatHeader then // BS
    if Assigned(Application[key]) then
    begin
      var myEnum: IEnumerator := (Application[key] as ArrayList).GetEnumerator;
      while myEnum.MoveNext do
        if (myEnum.Current as ChatMessage).User = User then
          Result := Result +
            (myEnum.Current as ChatMessage).Time.ToString('yyyy-MM-dd HH:mm:ss') + ' ' +
            (myEnum.Current as ChatMessage).User + ': ' +
            (myEnum.Current as ChatMessage).Message + #13#10
    end
  end;
And this completes the implementation of the secured Chat web service.

HTTPS Deployment
When it comes to the use of SOAP Headers, one thing must be kept in mind: they are part of the SOAP enveloppe, and sent in an unencrypted way over the internet from client to server and vice versa. There is no encryption whatsoever, so if you're storing sensitive username/password or access code information in the SOAP Header, then anyway with a packet sniffer can find out what your credentials are and abuse them.
For this reason, I always combine the use of SOAP Headers with a HTTPS / SSL connection. HTTPS means that the channel used for the communication is secured, so no sniffer or man-in-the-middle can read the message.
I've purchased and installed a HTTPS certificate on my web server, so we can use https://www.bobswart.nl for secure connections.

In order to ensure that the Chat service is only responding to secure connections, we can check the Request and see if the IsSecureConnection returns True. If not, we can raise an exception (and the CheckChatHeader is the ideal place to do that), as follows:

  method chat.CheckChatHeader: Boolean;
  begin
    Result := False;
    if not Context.Request.IsSecureConnection then
      raise ApplicationException.Create('Connection not secure (no HTTPS)');
    if not Assigned(ChatHeader) then
        raise ApplicationException.Create('No ChatHeader in SOAP Header!');
    if (ChatHeader.AccessCode.ToLower = 'delphi') then Result := True
  end;

The modified ASP.NET Web Service is deployed on my server as https://www.bobswart.nl/webchat/chat.asmx. You can check the WSDL, but cannot make a call to any of the methods that require a SOAP Header without using a secure connection. Also note that without the correct username and password in the SOAP Header, the service will do nothing for you - exactly as I planned.

ASP.NET Client
In order to use the new secure (and SOAP Header using) web service, we need to re-import it. This can be done by right-clicking on the project, selecing Add Web Reference. In the dialog that follows, specify https://www.bobswart.nl/webchat/chat.asmx are Address and click on Go. The result is a list of Services (just one) and Operations: GetMessages, GetUserMessages, and SendMessage:

We can now modify the existing client code for the btnSendMessage_Click and btnGetMEssages_Click events, by creating a new TChatHeader instance, assigning the username and password credentials and assigning it to the new ChatHeader field (which also appears in the imported web service).

  method _Default.btnSendMessage_Click(sender: System.Object; e: System.EventArgs);
  var
    ws: chat;
  begin
    ws := new chat();
    ws.ChatHeader := new TChatHeader();
    ws.ChatHeader.AccessCode := 'delphi';
    ws.SendMessage(tbName.Text, tbMessage.Text);
    tbChat.Text := ws.GetMessages(DateTime.Now.AddDays(-1))
  end;

  method _Default.btnGetNessages_Click(sender: System.Object; e: System.EventArgs);
  var
    ws: chat;
  begin
    ws := new chat();
    ws.ChatHeader := new TChatHeader();
    ws.ChatHeader.AccessCode := 'delphi';
    tbChat.Text := ws.GetMessages(DateTime.Now.AddDays(-1))
  end;

As an exercise for the reader, you can try calling these methods without the correct credentials (which will not send your chat message, and will also give you no chat messages), or no SoAP Header at all (which will give you an exception), or using HTTP instead of HTTPS (which wil also give you an exception).

The actual online web chat application has been extended with an extra input field: for the access code. Now, people who want to join the chat must not only enter their name, but also the correct access code (which is "Delphi") in order to be able to send or receive chat messages.

  method _Default.btnSendMessage_Click(sender: System.Object; e: System.EventArgs);
  var
    ws: chat;
  begin
    ws := new chat();
    ws.ChatHeader := new TChatHeader();
    ws.ChatHeader.AccessCode := tbAccessCode.Text;
    ws.SendMessage(tbName.Text, tbMessage.Text);
    tbChat.Text := ws.GetMessages(DateTime.Now.AddDays(-1))
  end;

  method _Default.btnGetNessages_Click(sender: System.Object; e: System.EventArgs);
  var
    ws: chat;
  begin
    ws := new chat();
    ws.ChatHeader := new TChatHeader();
    ws.ChatHeader.AccessCode := tbAccessCode.Text;
    tbChat.Text := ws.GetMessages(DateTime.Now.AddDays(-1))
  end;

SOAP Header Hashing
When it comes to sending passwords, or access codes, and you do not have the ability to use HTTPS, or you want to add another layer of security, you can decide to HASH the password or access code before sending it along. Of course, this means that the server also needs to verify the received HASH value against the Hash value of the actual password or access code.
In order to Hash a string, we can use the HashPasswordForStoringInConfigFile method from the FormsAuthentication class in the System.Web.Security namespace. System.Web.Configuration contains the FormsAuthPasswordFormat enumeration, which can be Clear, MD5, or SHA1.

At the server side, the AccessCode can be versified against the Hash value of the required access code as follows:

  method chat.CheckChatHeader: Boolean;
  begin
    Result := False;
    if not Context.Request.IsSecureConnection then
      raise ApplicationException.Create('Connection not secure (no HTTPS)');
    if not Assigned(ChatHeader) then
        raise ApplicationException.Create('No ChatHeader in SOAP Header!');
    if (ChatHeader.AccessCode =
      FormsAuthentication.HashPasswordForStoringInConfigFile('delphi',
        System.Web.Configuration.FormsAuthPasswordFormat.SHA1.ToString)) then Result := True
  end;

At the client side, the AccessCode can be hashed as follows:

  method _Default.btnSendMessage_Click(sender: System.Object; e: System.EventArgs);
  var
    ws: chat;
  begin
    ws := new chat();
    ws.ChatHeader := new TChatHeader();
    ws.ChatHeader.AccessCode :=
      FormsAuthentication.HashPasswordForStoringInConfigFile(tbAccessCode.Text,
        System.Web.Configuration.FormsAuthPasswordFormat.SHA1.ToString);
    ws.SendMessage(tbName.Text, tbMessage.Text);
    tbChat.Text := ws.GetMessages(DateTime.Now.AddDays(-1))
  end;

  method _Default.btnGetNessages_Click(sender: System.Object; e: System.EventArgs);
  var
    ws: chat;
  begin
    ws := new chat();
    ws.ChatHeader := new TChatHeader();
    ws.ChatHeader.AccessCode :=
      FormsAuthentication.HashPasswordForStoringInConfigFile(tbAccessCode.Text,
        System.Web.Configuration.FormsAuthPasswordFormat.SHA1.ToString);
    tbChat.Text := ws.GetMessages(DateTime.Now.AddDays(-1))
  end;

Summary
In this article I've shown how to use SOAP Headers to secure SOAP Web Services. The Web Service is deployed on IIS and only responds to HTTPS requests (to secure the messages containing the SOAP Header). For further protection, I've decided to send only the HASH value of the AccessCode from the client to the SOAP server.

Feel free to play along at https://www.bobswart.nl/webchat/


This webpage © 2010 by Bob Swart (aka Dr.Bob - www.drbob42.com). All Rights Reserved.