[ Team LiB ] |
Securing an XML Web ServiceIf 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 ServiceFor 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 TablesThis 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.sqlCREATE 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.sqlCREATE 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() MethodThe 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<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 .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:
The SQL CREATE PROCEDURE statement for the CheckPassword procedure is contained in Listing 23.12. Listing 23.12 CreateCheckPassword.sqlCreate 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 ClassPublic 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 HeaderThe 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 HeaderPublic 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 KeyEvery 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() FunctionPrivate 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 KeysAs 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 SubroutinePrivate 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 ServiceThe 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<%@ 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 .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 ServiceBefore 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. |
[ Team LiB ] |