Professional ASP.NET 1.1 [Electronic resources]

Alex Homeret

نسخه متنی -صفحه : 244/ 157
نمايش فراداده

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.