Securing an XML Web Service


If you want to build a subscription Web service, you need some method of identifying your subscribers. Currently, there is no standard method of authenticating users of a Web service. So, to identify them, you have to be creative. This section explores one approach to securing an XML Web service.

NOTE

There are a number of security specifications currently in the works that will provide standard ways to pass user credentials and encrypt Web service transactions. For example, the WS-Security specification adds a <Security> element to the SOAP header so that there is a standard way to pass security credentials when accessing a Web service. To learn more about WS-Security and related specifications, visit msdn.microsoft.com/ WebServices .


Overview of the Secure XML Web Service

For this example, you'll build a simple XML Web service that enables users to retrieve a number. This Web service will have one method named GetLuckyNumber() , which always returns the number 7 . The goal is to prevent unauthorized users from successfully calling the GetLuckyNumber() method.

One approach to preventing unauthorized access to the GetLuckyNumber() method would be to force every user to pass a username and password parameter whenever the method is called. However, if the Web service has multiple methods , this requirement could quickly become cumbersome. You would need to add the username and password parameters to each and every method.

Our approach will be to use a custom SOAP header to pass authentication information. You can pass a SOAP header as part of the SOAP packet being transmitted to the Web service and verify the authentication information in the SOAP header with every method call.

You could simply pass a username and password in the SOAP header. However, doing so would be dangerous. SOAP packets are transmitted as human-readable XML documents across the Internet. If you transmit a password in a SOAP header, the password could be intercepted and read by the wrong person.

To protect the integrity of user passwords, you could use the Secure Sockets Layer (SSL) to encrypt every message sent to the Web service. However, using SSL has a significant impact on the performance of a Web server. It takes a lot of work to encrypt and decrypt each message.

Instead, you need to add only one method to the Web service that requires SSL. Create a single method named Login() to accept a username and password and return a session key that is valid for 30 minutes. The session key is passed in the SOAP header to every other method call to authenticate the user.

The advantage of this approach is that it is reasonably secure and does not have a significant impact on performance. The user needs to use SSL only when calling the Login() method. After the Login() method is called, only the session key, not the user password, is transmitted when calling other Web methods.

If someone manages to steal the session key, the session key will be valid for less than 30 minutes. After 30 minutes elapse, the key will be useless.

Creating the Database Tables

This secure XML Web service will use two Microsoft SQL Server database tables named WebServiceUsers and SessionKeys . The WebServiceUsers database table contains a list of valid usernames and passwords. You can create this database table by using the SQL CREATE TABLE statement contained in Listing 23.9.

Listing 23.9 CreateWebServiceUsers.sql
 CREATE TABLE WebServiceUsers (userid int IDENTITY NOT NULL ,   username varchar (20)  NOT NULL ,   password varchar (20)  NOT NULL ,   role int NOT NULL) 

The C# version of this code can be found on the CD-ROM.

After you create the WebServiceUsers table, you should add at least one username and password to the table (for example, Steve and Secret ).

When a user logs in, a session key is added to the SessionKeys database table. The SQL CREATE TABLE statement for the SessionKeys table is contained in Listing 23.10.

Listing 23.10 CreateSessionKeys.sql
 CREATE TABLE SessionKeys (session_key  uniqueidentifier ROWGUIDCOL  NOT NULL ,   session_expiration datetime NOT NULL ,   session_userID int NOT NULL ,   session_username varchar(20) NOT NULL ,   session_role int NOT NULL) 

The C# version of this code can be found on the CD-ROM.

Creating the Login() Method

The Web service will have a method named Login() that users must access once to retrieve a valid session key. Because passwords are passed to the Login() method, this method should be accessed only through a secure channel (with https :// rather than http:// ).

The complete code for the Login() method is contained in Listing 23.11.

Listing 23.11 The Login() Method
[View full width]
 <WebMethod()> Public Function Login(username As String, password As String) As ServiceTicket   Dim conMyData As SqlConnection   Dim cmdCheckPassword As SqlCommand   Dim parmWork As SqlParameter   Dim intUserID As Integer   Dim intRole As Integer   Dim objServiceTicket As ServiceTicket   Dim drowSession As DataRow   ' Initialize Sql command   conMyData = New SqlConnection("Server=localhost;UID=sa;pwd=secret; database=myData")   cmdCheckPassword = New SqlCommand("CheckPassword", conMyData)   cmdCheckPassword.CommandType = CommandType.StoredProcedure   ' Add parameters   parmWork = cmdCheckPassword.Parameters.Add(_     New SqlParameter("@validuser", SqlDbType.Int))   parmWork.Direction = ParameterDirection.ReturnValue   cmdCheckPassword.Parameters.Add(_     New SqlParameter("@username", username))   cmdCheckPassword.Parameters.Add(_     New SqlParameter("@password", password))   parmWork = cmdCheckPassword.Parameters.Add(_     New SqlParameter("@sessionkey", SqlDbType.UniqueIdentifier))   parmWork.Direction = ParameterDirection.Output   parmWork = cmdCheckPassword.Parameters.Add(_     New SqlParameter("@expiration", SqlDbType.DateTime))   parmWork.Direction = ParameterDirection.Output   parmWork = cmdCheckPassword.Parameters.Add(_     New SqlParameter("@userID", SqlDbType.Int))   parmWork.Direction = ParameterDirection.Output   parmWork = cmdCheckPassword.Parameters.Add(_     New SqlParameter("@role", SqlDbType.Int))   parmWork.Direction = ParameterDirection.Output   ' Execute the command   conMyData.Open()     cmdCheckPassword.ExecuteNonQuery()     objServiceTicket = New ServiceTicket     If cmdCheckPassword.Parameters("@validuser").Value = 0 Then       objServiceTicket.IsAuthenticated = True     objServiceTicket.SessionKey = cmdCheckPassword.Parameters ("@sessionkey").Value graphics/ccc.gif .ToString()     objServiceTicket.Expiration = cmdCheckPassword.Parameters ("@expiration").Value     intUserID = cmdCheckPassword.Parameters("@userID").Value     intRole = cmdCheckPassword.Parameters("@role").Value     Else       objServiceTicket.IsAuthenticated = False     End If   conMyData.Close()   ' Add session to cache   If objServiceTicket.IsAuthenticated Then     If Context.Cache("SessionKeys") Is Nothing Then       LoadSessionKeys     End If     drowSession = Context.Cache("SessionKeys").NewRow()     drowSession("session_key") = objServiceTicket.SessionKey     drowSession("session_expiration") = objServiceTicket.Expiration     drowSession("session_userID") = intUserID     drowSession("session_username") = username     drowSession("Session_role") = intRole     Context.Cache("SessionKeys").Rows.Add(drowSession)   End If   ' Return ServiceTicket   Return objServiceTicket End Function 

The C# version of this code can be found on the CD-ROM.

The Login() method does the following:

  • Executes a SQL stored procedure named CheckPassword

  • Adds the current session to the cache

  • Returns a valid session key

The SQL CREATE PROCEDURE statement for the CheckPassword procedure is contained in Listing 23.12.

Listing 23.12 CreateCheckPassword.sql
 Create Procedure CheckPassword (@username varchar(20),   @password varchar(20),   @sessionkey uniqueidentifier Output,   @expiration DateTime Output,   @userID int Output,   @role int Output) As Select   @userID = userid,   @role = role   From WebServiceUsers   Where username = @username   And password = @password If @userID Is Not Null   Begin   SET @sessionkey = NEWID()   SET @expiration = DateAdd(mi, 30, GetDate())   Insert SessionKeys   (session_key,     session_expiration,     session_userID,     session_username,     session_role) values (@sessionkey,    @expiration,    @userID,    @username,    @role)   End Else   Return 1 

The C# version of this code can be found on the CD-ROM.

The CheckPassword stored procedure checks whether the username and password passed to the procedure exist in the WebServiceUsers table. If the username and password combination is valid, a new session key is generated. The SQL NEWID() function then generates a Globally Unique Identifier (GUID) to use as the session key.

The stored procedure also generates the expiration time for the session key. The DataAdd() function returns a time that is 30 minutes in the future.

Both the GUID and expiration time are added as part of a new record to the SessionKeys database table, which tracks all the active current sessions.

After the Login() method calls the CheckPassword stored procedure, the method constructs a new instance of the ServiceTicket class. The declaration for this class is contained in Listing 23.13.

Listing 23.13 The ServiceTicket Class
 Public Class ServiceTicket   Public IsAuthenticated As Boolean   Public SessionKey As String   Public Expiration As DateTime End Class 

The C# version of this code can be found on the CD-ROM.

The Login() method returns an instance of the ServiceTicket class, which contains the validated session key, session key expiration time, and a value indicating whether the user was successfully authenticated.

Retrieving the Custom SOAP Header

The XML Web service uses a custom SOAP header to retrieve authentication information. The session key is passed in a SOAP header named AuthHeader . You create the actual SOAP header by using the class in Listing 23.14.

Listing 23.14 The SOAP Header
 Public Class AuthHeader:Inherits SoapHeader   Public SessionKey As String End Class 

The C# version of this code can be found on the CD-ROM.

Notice that this class inherits from the SoapHeader class. It contains a single property named SessionKey that is used to store the session key.

NOTE

The SoapHeader class inhabits the System.Web.Services.Protocols namespace. You need to import this namespace before you can use the SoapHeader class.


To use the AuthHeader class in your Web service, you need to add a public property that represents the header. You declare a public property in your Web service class like this:

 
 Public AuthenticationHeader As AuthHeader 

Finally, if you want to retrieve this header in any particular method, you must add the SoapHeader attribute to the method. The GetLuckyNumber() method in Listing 23.15 illustrates how to use this attribute.

Listing 23.15 The GetLuckyNumber() Method
 <WebMethod(), SoapHeader("AuthenticationHeader")> _ Public Function GetLuckyNumber As Integer   If Authenticate(AuthenticationHeader) Then     Return 7   End If End Function 

The C# version of this code can be found on the CD-ROM.

The GetLuckyNumber() method simply passes AuthenticationHeader to a function named Authenticate() . This function is described in the next section.

Authenticating the Session Key

Every time someone calls the GetLuckyNumber() method, you could check the user's session key against the SessionKeys database table. However, with enough users, checking every session key would be slow. Instead, you can check the session key against a cached copy of the database table.

The private Authenticate() function in Listing 23.16 checks the session key retrieved from the authentication header against a cached copy of the SessionKeys database table.

Listing 23.16 The Authenticate() Function
 Private Function Authenticate(objAuthenticationHeader) As Boolean   Dim arrSessions As DataRow()   Dim strMatch As String   ' Load Session keys   If Context.Cache("SessionKeys") Is Nothing Then     LoadSessionKeys   End If   ' Test for match   strMatch = "session_key='" & objAuthenticationHeader.SessionKey   strMatch &= "' And session_expiration > #" & DateTime.Now() & "#"   arrSessions = Context.Cache("SessionKeys").Select(strMatch)   If arrSessions.Length > 0 Then     Return True   Else     Return False   End If End Function End Class 

The C# version of this code can be found on the CD-ROM.

Caching the Session Keys

As I mentioned previously, the SessionKeys database table is cached in memory to improve performance. The table is cached with the LoadSessionKeys subroutine contained in Listing 23.17.

Listing 23.17 The LoadSessionKeys Subroutine
 Private Sub LoadSessionKeys   Dim conMyData As SqlConnection   Dim dadMyData As SqlDataAdapter   Dim dstSessionKeys As DataSet   conMyData = New SqlConnection("Server=localhost;UID=sa;PWD=secret; database=myData")   dadMyData = New SqlDataAdapter("LoadSessionKeys", conMyData)   dadMyData.SelectCommand.CommandType = CommandType.StoredProcedure   dstSessionKeys = New DataSet   dadMyData.Fill(dstSessionKeys, "SessionKeys")   Context.Cache.Insert(_     "SessionKeys", _      dstSessionKeys.Tables("SessionKeys"), _      Nothing, _      DateTime.Now.AddHours(3), _      TimeSpan.Zero) End Sub 

The C# version of this code can be found on the CD-ROM.

The SessionKeys table is cached in memory for a maximum of three hours. The table is dumped every three hours to get rid of expired session keys. When the table is automatically reloaded into the cache, only fresh session keys are retrieved.

The LoadSessionKeys subroutine uses the LoadSessionKeys SQL stored procedure. This procedure is included on the CD with the name CreateLoadSessionKeys.sql .

Building the Secure XML Web Service

The complete code for the XML Web service described in this section is contained in Listing 23.18. You can test the Login() method by opening the SecureService.asmx page in a Web browser. However, you cannot test the GetLuckyNumber() method because it requires the custom SOAP authentication header.

Listing 23.18 SecureService.asmx
[View full width]
 <%@ WebService Class="SecureService" debug="True"%> Imports System Imports System.Web.Services Imports System.Web.Services.Protocols Imports System.Data Imports System.Data.SqlClient <WebService(Namespace:="http://yourdomain.com/webservices")> _ Public Class SecureService : Inherits WebService Public AuthenticationHeader As AuthHeader <WebMethod()> Public Function Login(username As String, password As String) As ServiceTicket   Dim conMyData As SqlConnection   Dim cmdCheckPassword As SqlCommand   Dim parmWork As SqlParameter   Dim intUserID As Integer   Dim intRole As Integer   Dim objServiceTicket As ServiceTicket   Dim drowSession As DataRow   ' Initialize Sql command   conMyData = New SqlConnection("Server=localhost;UID=sa;pwd=secret; database=myData")   cmdCheckPassword = New SqlCommand("CheckPassword", conMyData)   cmdCheckPassword.CommandType = CommandType.StoredProcedure   ' Add parameters   parmWork = cmdCheckPassword.Parameters.Add(_     New SqlParameter("@validuser", SqlDbType.Int))   parmWork.Direction = ParameterDirection.ReturnValue   cmdCheckPassword.Parameters.Add(_     New SqlParameter("@username", username))   cmdCheckPassword.Parameters.Add(_     New SqlParameter("@password", password))   parmWork = cmdCheckPassword.Parameters.Add(_     New SqlParameter("@sessionkey", SqlDbType.UniqueIdentifier))   parmWork.Direction = ParameterDirection.Output   parmWork = cmdCheckPassword.Parameters.Add(_     New SqlParameter("@expiration", SqlDbType.DateTime))   parmWork.Direction = ParameterDirection.Output   parmWork = cmdCheckPassword.Parameters.Add(_     New SqlParameter("@userID", SqlDbType.Int))   parmWork.Direction = ParameterDirection.Output   parmWork = cmdCheckPassword.Parameters.Add(_     New SqlParameter("@role", SqlDbType.Int))   parmWork.Direction = ParameterDirection.Output   ' Execute the command   conMyData.Open()     cmdCheckPassword.ExecuteNonQuery()     objServiceTicket = New ServiceTicket     If cmdCheckPassword.Parameters("@validuser").Value = 0 Then       objServiceTicket.IsAuthenticated = True     objServiceTicket.SessionKey = cmdCheckPassword.Parameters ("@sessionkey").Value graphics/ccc.gif .ToString()     objServiceTicket.Expiration = cmdCheckPassword.Parameters ("@expiration").Value     intUserID = cmdCheckPassword.Parameters("@userID").Value     intRole = cmdCheckPassword.Parameters("@role").Value     Else       objServiceTicket.IsAuthenticated = False     End If   conMyData.Close()   ' Add session to cache   If objServiceTicket.IsAuthenticated Then     If Context.Cache("SessionKeys") Is Nothing Then       LoadSessionKeys     End If     drowSession = Context.Cache("SessionKeys").NewRow()     drowSession("session_key") = objServiceTicket.SessionKey     drowSession("session_expiration") = objServiceTicket.Expiration     drowSession("session_userID") = intUserID     drowSession("session_username") = username     drowSession("Session_role") = intRole     Context.Cache("SessionKeys").Rows.Add(drowSession)   End If   ' Return ServiceTicket   Return objServiceTicket End Function <WebMethod(), SoapHeader("AuthenticationHeader")> _ Public Function GetLuckyNumber As Integer   If Authenticate(AuthenticationHeader) Then     Return 7   End If End Function Private Sub LoadSessionKeys   Dim conMyData As SqlConnection   Dim dadMyData As SqlDataAdapter   Dim dstSessionKeys As DataSet   conMyData = New SqlConnection("Server=localhost;UID=sa;PWD=secret; database=myData")   dadMyData = New SqlDataAdapter("LoadSessionKeys", conMyData)   dadMyData.SelectCommand.CommandType = CommandType.StoredProcedure   dstSessionKeys = New DataSet   dadMyData.Fill(dstSessionKeys, "SessionKeys")   Context.Cache.Insert(_     "SessionKeys", _      dstSessionKeys.Tables("SessionKeys"), _      Nothing, _      DateTime.Now.AddHours(3), _      TimeSpan.Zero) End Sub Private Function Authenticate(objAuthenticationHeader) As Boolean   Dim arrSessions As DataRow()   Dim strMatch As String   ' Load Session keys   If Context.Cache("SessionKeys") Is Nothing Then     LoadSessionKeys   End If   ' Test for match   strMatch = "session_key='" & objAuthenticationHeader.SessionKey   strMatch &= "' And session_expiration > #" & DateTime.Now() & "#"   arrSessions = Context.Cache("SessionKeys").Select(strMatch)   If arrSessions.Length > 0 Then     Return True   Else     Return False   End If End Function End Class Public Class AuthHeader:Inherits SoapHeader   Public SessionKey As String End Class Public Class ServiceTicket   Public IsAuthenticated As Boolean   Public SessionKey As String   Public Expiration As DateTime End Class 

The C# version of this code can be found on the CD-ROM.

Accessing the Secure Web Service

Before you can access the SecureService Web service, you need to create a proxy class by executing the following two statements:

 
 wsdl /l:vb /n:services http://localhost/webservices/SecureService.asmx?wsdl vbc /t:library /r:System.dll,System.Web.Services.dll,System.xml.dll SecureService.vb 

After you create the proxy class, remember to copy it into your application /bin directory.

You can test the SecureService Web service by using the ASP.NET page in Listing 23.19. You'll need to change the username and password to match the username and password that you entered into the WebServiceUsers table.

Listing 23.19 TestSecureService.aspx
 <%@ Import Namespace="Services" %> <Script runat="Server"> Const strUsername As String = "Steve" Const strPassword As String = "Secret" Sub Page_Load   Dim objSecureService As SecureService   Dim objServiceTicket As ServiceTicket   Dim objAuthHeader As AuthHeader   objSecureService = New SecureService   objServiceTicket = Session("ServiceTicket")   ' Check for ticket existence   If objServiceTicket Is Nothing Then     objServiceTicket = objSecureService.Login(strUsername, strPassword)     Session("ServiceTicket") = objServiceTicket   End If   ' Check for ticket expiration   If objServiceTicket.Expiration < DateTime.Now Then     objServiceTicket = objSecureService.Login(strUsername, strPassword)     Session("ServiceTicket") = objServiceTicket   End If   ' Call the web service   If objServiceTicket.IsAuthenticated Then     objAuthHeader = New AuthHeader     objAuthHeader.SessionKey = objServiceTicket.SessionKey     objSecureService.AuthHeaderValue = objAuthHeader     lblLuckyNumber.Text = objSecureService.GetLuckyNumber()   Else     lblLuckyNumber.Text = "Invalid username or password!"   End If End Sub </Script> <html> <head><title>TestSecureService.aspx</title></head> <body> <asp:Label   id="lblLuckyNumber"   EnableViewState="False"   Runat="Server" /> </body> </html> 

The C# version of this code can be found on the CD-ROM.

In the Page_Load subroutine in Listing 23.19, an instance of the SecureService proxy class is created. Next, a ServiceTicket is retrieved by calling the Login() Web service method. The ServiceTicket is stored in session state so that it can be used multiple times.

The subroutine checks whether the ServiceTicket has expired before trying to use it. If the Expiration property of the ServiceTicket class contains a time that has passed, the Login() method is called to retrieve a new ServiceTicket .

The ServiceTicket contains a session key in its SessionKey property. The session key is used when constructing the authentication header. The authentication header is created and the GetLuckyNumber() method is called with the following statements:

 
 objAuthHeader = New AuthHeader objAuthHeader.SessionKey = objServiceTicket.SessionKey objSecureService.AuthHeaderValue = objAuthHeader lblLuckyNumber.Text = objSecureService.GetLuckyNumber() 

The lucky number retrieved from the Web service is assigned to a label named lblLuckyNumber .



ASP.NET Unleashed
ASP.NET 4 Unleashed
ISBN: 0672331128
EAN: 2147483647
Year: 2003
Pages: 263

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