Advanced Topics


All of the security options we've discussed so far rely upon the HTTP transport protocol for the security details: Windows NTLM, Basic, and Forms authentications, all rely on either an HTTP header or an HTTP cookie.

Now, we'll look at an advanced example that demonstrates a custom authentication and authorization example using SOAP headers.

Custom Authentication and Authorization

SOAP headers provide a great way to send out-of- band data. We use HTTP headers to send details that aren't directly part of the body with the HTTP message, and can do the same thing with SOAP headers. This allows us to decouple application details, such as session cookies and authentication, from the transport protocol and instead pass them as part of the SOAP message. This way, no matter what transport the SOAP message is sent over, these details remain with the message instead of being lost when transport protocols change.

We'll look at an example that uses SOAP headers (rather than relying upon a cookie or an HTTP header) to send an 'authentication' header. This example shows several great features of ASP.NET:

  • Custom authentication :Bypass the authentication features that ASP.NET offers, such as Forms or Windows, and plug our own authentication system into ASP.NET.

  • SOAP headers :Use SOAP headers to transmit credentials and decouple the information from the HTTP headers, making the message transport-independent.

  • HTTP module :Use of an HTTP module that looks at each request and determines if it is a SOAP message.

  • Custom application events :The HTTP module raises a custom global.asax event, within which we implement our application logic to verify credentials.

Let's start by examining the web service.

ASP.NET Web Service

The following is the code for a web service (written in VB.NET) that implements an authentication SOAP header:

  <%@ WebService Class="SecureWebService" %>     Imports System   Imports System.Web.Services   Imports System.Web.Services.Protocols     Public Class Authentication : Inherits SoapHeader   Public User As String   Public Password As String   End Class     Public Class SecureWebService : Inherits WebService   Public authentication As Authentication     <WebMethod> _   <SoapHeader("authentication")> _   Public Function ValidUser() As String   If User.IsInRole("Customer") Then   Return "User is in the Customer role..."   Else If User.Identity.IsAuthenticated Then   Return "User is valid..."   Else   Return "Not authenticated"   End If   End Function     End Class  

This code implements a single WebMethod , ValidUser , which simply uses the ASP.NET User intrinsic object to determine if the request is from a validated user:

  • If the user is in the Customer role, the function returns User is in the Customer role... .

  • If the user is simply authenticated but is not in the Customer role, the function returns User is valid... .

  • If the user is not authenticated, the function returns Not authenticated .

The code is quite simple. The web service uses the ASP.NET User intrinsic object to validate the authentication of a request.

The web service also defines a SOAP header, Authentication . Applications using the web service will set this header with appropriate values, and our application will validate the credentials and create a valid User object. Let's see how that's done.

Sample Application

The following VB.NET code is for a simple ASP.NET page that makes use of a proxy, SecureWebService . The proxy is used to make calls to the ASP.NET Web service we just created:

  <%@ Import Namespace="Security"%>   <%@ Import Namespace="System.Web.Services.Protocols" %>   <script runat="server">   Public Sub Page_Load(sender As Object, e As EventArgs)   span1.InnerHtml = ""   span2.InnerHtml = ""   End Sub   Public Sub Authenticate_Click(sender As Object, e As EventArgs)   Dim secureWebService As New SecureWebService()     ' Create the Authentication header and set values   Dim authenticationHeader As New Authentication()   authenticationHeader.User = user.Value   authenticationHeader.Password = password.Value     ' Assign the Header   secureWebService.AuthenticationValue = authenticationHeader   ' Call method   Try   span1.InnerHtml = s.ValidUser()   Catch soap As SoapException   span2.InnerHtml = soap.Message   End Try   End Sub   </script>  

This ASP.NET page simply renders an HTML form that allows the user to enter credentials. These credentials are then made part of the request sent to the server. Here is the SOAP message that a caller makes to the server (minus the HTTP headers):

  <?xml version="1.0" encoding="utf-8"?>   <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"   xmlns:xsd="http://www.w3.org/2001/XMLSchema">   <soap:Header>   <Authentication xmlns="http://tempuri.org/">   <User>John</User>   <Password>password</Password>   </Authentication>   </soap:Header>   <soap:Body>   <ValidUser xmlns="http://tempuri.org/" />   </soap:Body>   </soap:Envelope>  

And here is the response (again, minus HTTP headers):

  <?xml version="1.0" encoding="utf-8"?>     <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"   xmlns:xsd="http://www.w3.org/2001/XMLSchema">   <soap:Body>   <ValidUserResponse xmlns="http://tempuri.org/">   <ValidUserResult>Not authenticated</ValidUserResult>   </ValidUserResponse>   </soap:Body>     </soap:Envelope>  

The SOAP header is passed in clear-text “ we'll address encryption shortly.

Calls to the web service are obviously getting processed . However, we've left a few details out, including how the credentials are authenticated. Let's look at that now.

Validating SOAP Header Credentials

The ASP.NET Web service requires a global.asax file in its application root. Here's what the global.asax file contains:

  <%@ Import Namespace="Microsoft.WebServices.Security" %>   <%@ Import Namespace="System.Security.Principal" %>     <script runat=server>   Public Sub WebServiceAuthentication_OnAuthenticate(sender As Object, _   e As WebServiceAuthenticationEvent)   If (e.User = "bwhite@bar.com") And (e.Password = "password") Then   e.Authenticate()   Else If (e.User = "sswienton@foo.com") _   And (e.Password = "password") Then   Dim s(1) As String   s(0) = "Customer"   e.Authenticate(s)   End If   End Sub     </script>  

Our global.asax file implements a WebServiceAuthentication_OnAuthenticate event handler that is raised whenever a request is made to a web service within the current application. Inside the event, application logic is used to determine the validity of a given set of credentials. In this case, we have two hardcoded cases. One simply authenticates the user and the other authenticates the user and also adds the user to the Customer role. Although the usernames and passwords are hardcoded into the logic, it is easy to envision replacing this with calls to a database, an XML file, or another resource that you want to verify credentials against.

The actual event is raised by a custom HTTP module. The HTTP module looks at each request, and for requests that are web services, the HTTP module opens the message, parses the SOAP value for an authentication header (and values), raises a custom event, and finally implements the necessary calls to authenticate a user and add a given user to the role. By the time the request actually reaches the web service, all of this has already taken place.

WebServiceAuthentication HTTP Module

The HTTP module encapsulates all the work of authenticating the request. Although all of the work done by the module could be achieved using global.asax , this method provides a clean level of abstraction and allows the developer to focus on authenticating the request.

The HTTP module listens for an ASP.NET authenticate event (discussed in Chapter 12). When this ASP.NET application event is raised, the HTTP module has the opportunity to execute code. In this case, the module raises its own OnEnter event.

The OnEnter event handler starts by examining the request for the HTTP_SOAPACTION header. The existence of this header is required by the SOAP 1.1 specification, but the value it contains is optional. If the header exists, we know the HTTP request contains a SOAP message. The code is shown here in C#:

  void OnEnter(Object source, EventArgs eventArgs) {   HttpApplication app = (HttpApplication)source;   HttpContext context = app.Context;   Stream HttpStream = context.Request.InputStream;     // Current position of stream   long posStream = HttpStream.Position;     // If the request contains an HTTP_SOAPACTION   // header we'll look at this message   if (context.Request.ServerVariables["HTTP_SOAPACTION"] == null)   return;     // Load the body of the HTTP message   // into an XML document   XmlDocument dom = new XmlDocument();   string soapUser;   string soapPassword;   try {   dom.Load(HttpStream);  

If the request is a valid SOAP message, we will attempt to load the SOAP message into an XML document. If this succeeds, we'll reset the location in the stream (so that the ASP.NET Web service still has the chance to read the request). Then we'll attempt to read the User and Password values of the Authentication header. As these operations are wrapped in a try...catch block, if it fails, an exception is thrown.

  // Reset the stream position   HttpStream.Position = posStream;     // Bind to the Authentication header   soapUser = dom.GetElementsByTagName("User").Item(0).InnerText;   soapPassword = dom.GetElementsByTagName("Password").Item(0).InnerText;   } catch (Exception e) {   // Reset Position of stream   HttpStream.Position = posStream;     // Throw exception   throw soapException   }  

If no exceptions are raised, we'll call the code to raise the event that we can listen for in global.asax :

  // Raise the custom global.asax event   OnAuthenticate(new WebServiceAuthenticationEvent(context,   soapUser,   soapPassword));   return;   }  

Finally, the function returns, and the processing of the request is handed to the ASP.NET .asmx handler.

This example demonstrates how to use SOAP headers to pass additional information, such as authentication details, as part of the SOAP message “decoupling those details from the transport. In this particular example, we passed the data in clear text, obviously a less than ideal solution. However, this example is only meant to be a conceptual sample that you can extend, not a solution in itself.

Next , we'll look at another advanced feature of ASP.NET Web services: SOAP extensions.

SOAP Extensions

ASP.NET provides a great programming model for working with and building web services, and in some ways it has trivialized the work required to build SOAP-based applications. We simply author the application logic, sprinkle some WebMethod attributes on the functions, and it's done.

However, life isn't always so easy. The use of the WebMethod is simple, but it also abstracts a lot of what is happening behind the scenes. A special base class, SoapExtension , allows for implementing our own custom extensions that are able to manipulate ASP.NET Web services at a very low level.

The SoapExtension base class, which our class must inherit from, requires that we implement some virtual functions. The most important function that we must implement is ProcessMessage .

The ProcessMessage method allows you to look at the raw message before and after it is serialized from a .NET class into SOAP or deserialized from SOAP back into a .NET class. This functionality is available for the web service and the proxy.

Let's look at a simple SOAP extension that enables us to trace the SOAP request/response data to disk. This is a very helpful attribute written by some of the developers on the ASP.NET Web services team.

Tracing

One of the most frustrating aspects of developing web services is the lack of tools available to view the SOAP message exchange. The trace extension (the source of which follows ) outputs the incoming and outgoing SOAP message to a file.

The trace extension can be used as follows:

 <WebMethod()> _  <TraceExtension(Filename:="c:\trace.log")> _  Public Function Add(a As Integer, b As Integer) As Integer    Return a + b End Function 

We simply compile the trace extension attribute and deploy it to our application's \bin directory. We then use the attribute, as shown, providing it with the name of a file to log results to. The result of this is:

  ================================== Request at 6/6/2001 2:10:20 AM   <?xml version="1.0" encoding="utf-8"?>   <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"   xmlns:xsd="http://www.w3.org/2001/XMLSchema">   <soap:Body>   <Add xmlns="http://tempuri.org/">   <a>10</a>   <b>5</b>   </Add>   </soap:Body>   </soap:Envelope>     ---------------------------------- Response at 6/6/2001 2:10:20 AM   <?xml version="1.0" encoding="utf-8"?>   <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"   xmlns:xsd="http://www.w3.org/2001/XMLSchema">   <soap:Body>   <AddResponse xmlns="http://tempuri.org/">   <AddResult>15</AddResult>   </AddResponse>   </soap:Body>   </soap:Envelope>  

You can see the trace results of a request to the Add Web service and the response. Here's the C# source to the trace extension attribute:

  using System;   using System.IO;   using System.Web.Services.Protocols;     [AttributeUsage(AttributeTargets.Method)]   public class TraceExtensionAttribute : SoapExtensionAttribute {   private string filename = "c:\log.txt";   private int priority;   public override Type ExtensionType {   get { return typeof(TraceExtension); }   }   public override int Priority {   get { return priority; }   set { priority = value; }   }     public string Filename {   get {   return filename;   }   set {   filename = value;   }   }   }  

In the TraceExtensionAttribute class, we inherit from SoapExtensionAttribute , implementing both our public property for configuring the filename used for logging ( Filename ), and the Extension type attribute, which returns a class of type TraceExtension :

  public class TraceExtension : SoapExtension {     Stream oldStream;   Stream newStream;   string filename;     public override object GetInitializer(LogicalMethodInfo methodInfo,   SoapExtensionAttribute attribute) {   return ((TraceExtensionAttribute) attribute).Filename;   }     public override object GetInitializer(Type serviceType){   return typeof(TraceExtension);   }     public override void Initialize(object initializer) {   filename = (string) initializer;   }  

In the TraceExtension class, we override some methods that are marked as virtual in SoapExtension . The most important of these is ProcessMessage , which allows you to interact with the message at four different stages of processing:

  • BeforeSerialize :Allows you to interact with the message before you serialize the data as a SOAP message.

  • AfterSerialize :Allows you to interact with the message after you serialize the data as a SOAP message. In our case, we call the WriteOutput function to write the current SOAP message to our log during this stage.

  • BeforeDeserialize :Allows you to interact with the message before you deserialize the SOAP message back into .NET data types. Here, we call WriteInput to write the current SOAP message to the log.

  • AfterDeserialize :Allows you to interact with the message after you deserialize the SOAP message back into .NET data types.

The code for these methods follows:

  public override void ProcessMessage(SoapMessage message) {   switch (message.Stage) {     case SoapMessageStage.BeforeSerialize:   break;     case SoapMessageStage.AfterSerialize:   WriteOutput( message );   break;     case SoapMessageStage.BeforeDeserialize:   WriteInput( message );   break;     case SoapMessageStage.AfterDeserialize:   break;     default:   throw new Exception("invalid stage");   }   }     public override Stream ChainStream( Stream stream ){   oldStream = stream;   newStream = new MemoryStream();   return newStream;   }     public void WriteOutput( SoapMessage message ){   newStream.Position = 0;   FileStream fs = new FileStream(filename, FileMode.Append, FileAccess.Write);   StreamWriter w = new StreamWriter(fs);   w.WriteLine("---------------------------- Response at " + DateTime.Now);   w.Flush();   Copy(newStream, fs);   fs.Close();   newStream.Position = 0;   Copy(newStream, oldStream);   }     public void WriteInput( SoapMessage message ){   Copy(oldStream, newStream);   FileStream fs = new FileStream(filename, FileMode.Append, FileAccess.Write);   StreamWriter w = new StreamWriter(fs);   w.WriteLine("============================= Request at " + DateTime.Now);   w.Flush();   newStream.Position = 0;   Copy(newStream, fs);   fs.Close();   newStream.Position = 0;   }   void Copy(Stream from, Stream to) {   TextReader reader = new StreamReader(from);   TextWriter writer = new StreamWriter(to);   writer.WriteLine(reader.ReadToEnd());   writer.Flush();   }   }  

It should be clear how powerful SOAP extensions are. You could, for example, have written the custom authentication example demonstrated earlier using SOAP extensions. Another possibility would have been to use SOAP extensions to perform custom encryption of data.

Custom Encryption

This example was originally written to demonstrate an alternative to using HTTP/SSL to send data. Consider this: as long as the SOAP message is sent via HTTP, you can use a complementary protocol such as HTTPS to encrypt the data. However, if the SOAP message is routed between various servers, each of those servers must have a trust relationship with the others as each will have to establish a new SSL connection to transmit the data to the next server.

We want to route over public networks, but exchange private data. The custom encryption attribute discussed next allows a valid SOAP message to be routed over the public network, but the contents of that SOAP message can be encrypted. For example, you could write the following web service and use the tracing extensions to see exactly what is exchanged via SOAP on the wire:

  ...   <WebMethod()>   <TraceExtension(Filename:="c:\trace.log")>   Public Function SayHello() As String   Return "Secret message"   End Function   ...  

The log result of the exchange follows:

  ---------------------------------- Response at 6/7/2001 3:11:47 AM   <?xml version="1.0" encoding="utf-8"?>   <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"   xmlns:xsd="http://www.w3.org/2001/XMLSchema">   <soap:Body>   <SayHello xmlns="http://tempuri.org/" />   </soap:Body>   </soap:Envelope>   ================================== Request at 6/7/2001 3:11:49 AM   <?xml version="1.0" encoding="utf-8"?>   <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"   xmlns:xsd="http://www.w3.org/2001/XMLSchema">   <soap:Body>   <SayHelloResponse xmlns="http://tempuri.org/">   <SayHelloResult>Secret message</SayHelloResult>   </SayHelloResponse>   </soap:Body>   </soap:Envelope>  

To prevent prying eyes from examining the details of your message exchange, you could:

  • Use HTTPS and encrypt the entire data exchange

  • Use a custom SOAP extension and encrypt only part of the data

Let's look at a custom SOAP extension that is capable of encrypting only part of the data:

  ...   <WebMethod()>   <EncryptionExtension(Encrypt=EncryptMode.Response)>   <TraceExtension(Filename:="c:\trace.log")>   Public Function SayHello() As String   Return "Secret message"   End Function   ...  

We've now added an EncryptionExtension attribute to the SayHello function, and also set a property (in this new attribute) that sets EncryptMode.Response . Now, when we make a SOAP request to this service and trace the result, our data is encrypted:

 ---------------------------------- Response at 6/7/2001 3:18:25 AM <?xml version="1.0" encoding="utf-8"?> <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"                xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"                xmlns:xsd="http://www.w3.org/2001/XMLSchema">    <soap:Body>       <SayHello xmlns="http://tempuri.org/" />    </soap:Body> </soap:Envelope> ================================== Request at 6/7/2001 3:18:27 AM <?xml version="1.0" encoding="utf-8"?> <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"                xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"                xmlns:xsd="http://www.w3.org/2001/XMLSchema">    <soap:Body>       <SayHelloResponse xmlns="http://tempuri.org/">  <SayHelloResult>   158 68 233 236 56 189 240 27 73 27 17 214 65 142 207 77   </SayHelloResult>  </SayHelloResponse>    </soap:Body> </soap:Envelope> 

The value of the <SayHelloResult> element is now an array of bytes. These bytes are a single-pass, symmetric encryption using Data Encryption Standard ( DES ) of the string "Secret Message" .

The EncryptionExtension attribute that is added to the function encrypts the value after the message is serialized into SOAP. This allows us to send a valid SOAP message that can be routed between various intermediaries and sent over alternative protocols. Our data remains secure, as it is sent over the network.

To decrypt the message, we use the same attribute, but add it to the proxy used by the application using our service:

  <EncryptionExtension(Decrypt=DecryptMode.Request)> _  <SoapDocumentMethodAttribute("http://tempuri.org/SayHello",                            Use:=SoapBindingUse.Literal,                            ParameterStyle:= SoapParameterStyle.Wrapped)> _ Public Function SayHello() As String    Dim results() As Object = Me.Invoke("SayHello", New Object(0) {})    Return CType(results(0),String) End Function 

Instead of setting an Encrypt property in the EncryptionExtension attribute, we now set a Decrypt property. This instructs the EncryptionExtension to decrypt any incoming messages. The result is that data is encrypted on the wire as it's exchanged, and decrypted again by the intended recipient.

The ProcessMessage function of EncryptionExtension follows:

  public override void ProcessMessage(SoapMessage message) {   switch (message.Stage) {     case SoapMessageStage.BeforeSerialize:   break;     case SoapMessageStage.AfterSerialize:   Encrypt();   break;     case SoapMessageStage.BeforeDeserialize:   Decrypt();   break;     case SoapMessageStage.AfterDeserialize:   break;     default:   throw new Exception("invalid stage");   }   }  

It's very similar to the tracing extension we saw earlier. However, instead of writing the SOAP message to a file, this extension is capable of using DES encryption to both encrypt and decrypt the SOAP message. Here's the Encrypt routine that is called:

  private void Encrypt() {   newStream.Position = 0;     if (encryptMode == EncryptMode.Response)   newStream = EncryptSoap(newStream);   Copy(newStream, oldStream);   }  

If the Encrypt property of the attribute is set to EncryptMode.Response , the Encrypt function will call another routine, EncryptSoap :

  public MemoryStream EncryptSoap(Stream streamToEncrypt) {   streamToEncrypt.Position = 0;   XmlTextReader reader = new XmlTextReader(streamToEncrypt);   XmlDocument dom = new XmlDocument();   dom.Load(reader);     XmlNamespaceManager nsmgr = new XmlNamespaceManager(dom.NameTable);   nsmgr.AddNamespace("soap", "http://schemas.xmlsoap.org/soap/envelope/");   XmlNode node = dom.SelectSingleNode("//soap:Body", nsmgr);   node = node.FirstChild.FirstChild;     byte[] outData = Encrypt(node.InnerText);     StringBuilder s = new StringBuilder();     for(int i=0; i<outData.Length; i++) {   if(i==(outData.Length-1))   s.Append(outData[i]);   else   s.Append(outData[i] + " ");   }     node.InnerText = s.ToString();     MemoryStream ms = new MemoryStream();   dom.Save(ms);   ms.Position = 0;     return ms;   }  

EncryptSoap reads in the memory stream that represents the SOAP message and navigates down to the appropriate node. Once this is found, the Encrypt method that accepts a string and returns the DES encrypted byte array is called. Afterwards, EncryptSoap simply converts the encrypted byte array to a string and adds that string back into the SOAP message stream, which is then returned.

Although this is quite a complex sample, it should provide you with an excellent starting point with which to build more secure web services. Here are some recommendations:

  • Ideally, this encryption extension should be using asymmetric encryption, rather than having both the web service and the proxy sharing the same key.

  • A SOAP header should be used to send additional details about the message relating to the encryption; for example, details of the public key (if you were to re-implement this to support asymmetric encoding), as well as an encrypted timestamp to prevent replay attacks.

  • Currently, the message exchange only supports clear-text requests (from the proxy) and encrypted results. Ideally, the extension should support encrypted requests.

These suggestions are beyond the scope of what can be covered in this chapter. However, hopefully they will be implemented in the near future and released into the public domain.




Professional ASP. NET 1.1
Professional ASP.NET MVC 1.0 (Wrox Programmer to Programmer)
ISBN: 0470384611
EAN: 2147483647
Year: 2006
Pages: 243

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net