Wednesday, May 20, 2009

Delphi Prism Web Services and SOAP Security

In this article, I'll demonstrate how to use SOAP Headers as security technique for ASP.NET Web Service projects using Delphi Prism (extending the example from last month by adding a security layer to it).


SOAP Headers

In order to work with SOAP Headers in an ASP.NET Web Service, we have to add System.Web.Services.Protocols to the uses clause, and define a new class derived from the SoapHeader class found in the aforementioned namespace.
The new class can be called anything, so I've decided to call it TCredentials (I know that in the .NET Framework you're not supposed to prefix everything with a "T", but since I'm a proud Delphi developer, I always use a T prefix for my types).


It must be a public class, since we need to export it so any client importing our ASP.NET web service can also know about it.
Adding a Username and Password field to the TCredentials class, this could lead to the following definition.



type
TCredentials = public class(System.Web.Services.Protocols.SoapHeader)
public
Username: String;
Password: String;
end;


With this class definition in mind, we can add a public field (for example called token) to the public class Service, as well as a private method (for example called CheckCredentials) to check if the credentials have been received and contain the correct username/password information.
This leads to the following modification of the Service class:



Service = public class(System.Web.Services.WebService)
public
var

token: TCredentials;

private
method
CheckCredentials: Boolean;


In order to specify that the token has to be used for certain web method (from the original set), we have to add the attribute SoapHeader to the web method, specifying the token as well as the direction in which the token is assigned (in our case only on input, so only the web service client will be writing information in the token, and the Service web service "engine" can check the token for valid credentials by looking at its value).


Using the example from last month, we can modify some of the web methods as follows:



public
[WebMethod(EnableSession := true)]
[SoapHeader('token', Direction := SoapHeaderDirection.&In)]
method Remember(const Name,Value: String);

[WebMethod(EnableSession := true)]
[SoapHeader('token', Direction := SoapHeaderDirection.&In)]
method Recall(const Name: String): String;

[WebMethod(EnableSession := true)]
method Forget(const Name: String);

[WebMethod(EnableSession := true)]
method Amnesia; // forget all
end;


Note that I only added the SoapHeader attribute to the Remember and Recall web methods, and not to the Forget or Amnesia methods.
In other words: you are free to forget, but in order to remember or recall something, you need security clearance.


Inside that Remember and Recall methods, we will have to call the CheckCredentials method before doing their actual work, in order to verify the value of the token.
Which leads us to the implementation of CheckCredentials, which is shown below (using a hard-coded check for the values of the Username and Password properties of the token - you should obviously implement a stronger check here!).


Note that apart from returning False is the Username and Password are incorrect, we can also raise an exception, for example if the token is not found in the SOAP header (which might happen if the web service is consumed and used by web service client applications who do not add the token to the SOAP header in the first place).



method Service.CheckCredentials: Boolean;
begin
Result := False;
if not Assigned(token) then
raise
ApplicationException.Create('No token in SOAP Header!');

if (token.Username.ToLower = 'bob') and
(token.Password.ToLower = 'swart') then Result := True
end;


The implementation of Remember and Recall can simple be extended by calling the CheckCredentials method before doing anything:



method Service.Remember(const Name,Value: String);
begin
if
CheckCredentials then
Session[Name] := Value
end;

method Service.Recall(const Name: String): String;
begin
if
CheckCredentials then
if
Assigned(Session[Name]) then
Result := Session[Name].ToString
else Result := ''
end;


Note that Forget and Amnesia remain unchanged, since you are always allowed to forget:



method Service.Forget(const Name: String);
begin
Session.Remove(Name) // always allowed
end;

method Service.Amnesia;
begin
Session.Abandon // always allowed
end;


Before deploying and consuming this new secure web service, there's one thing to keep in mind about using SOAP Headers: the contents are sent in plain text over the network.
So passing a username and password inside a SOAP Header is adding authorization capabilities, but still lacks a little security.
For that reason, I always recommend to use SOAP Headers in combination with HTTPS and SSL.
That way, the traffic between the web service and client is encrypted, and nobody else should be able to decypher the information sent by the client to the server (since it's encrypted with the public key from the server, and can only be decrypted by the private key of the server, so anyone sniffing the network only gets an encrypted package).


Implementing HTTPS and SSL requires two things: first, you should purchase a certificate and install it on your web server (or ask your provider if a certificate is already available for use by your web service), and second, all URLs that are used to communicate with the web service should use https:// and not http://.


For this example, I give you the option of exploring the difference, so I've deployed the secure web service on two different web servers: one with HTTPS and one without.
On one server, you can access the web service via http://www.bobswart.net/MyWebService/Service.asmx and on the other (secure) web server you have to use HTTPS and the URL https://www.bobswart.nl/MyWebService/Service.asmx instead.


Both will work the same (the MyWebService.dll code behind assembly is exactly the same on both machines), but one connection is more secure than the other, which is important when using SOAP Headers.

No comments: