Often you want to secure your Web Services. After all, even in the Internet age where everything is shared, you must protect some things. Imagine you've developed a Web Service in which the user types a stock ticker symbol and gets the stock's real-time quote. Typically, free stock quotes are delayed 20 minutes or more, so your service is something that people would be willing to pay for. You'd want to keep out any freeloaders.
So far, you've looked at how to create Web Services that are publicly available. Anyone who knows of the service can consume it (either through the .disco file or the direct URL). Fortunately, you can take steps to secure your Web Services so that only authorized parties may use them.
This security measure involves using SOAP (and thus XML) to send authentication information as part of the service commands. Because information is sent directly to the Web Service, the service is free to implement authentication however necessary. For example, you could access a database to determine the proper credentials for a user.
Before examining the technical details, let's look at an analogy. Imagine that you want to go to a very exclusive restaurant. They require identification at the door before they'll even let you in. To gain entrance and receive service, you must bring along some form of identification, such as a driver's license.
To pass along identification information when trying to consume a Web service, you'll use a SOAP header. This is an additional XML message attached to the regular communication of the service that provides custom information. It works similarly to a driver's license. Although it's transported with the SOAP messages, it's a separate entity. Your SOAP header will be used to pass username and password information so that only the users you choose can access the service.
To pass a SOAP header, you simply create a class that inherits the System.Web.Services.Protocols.SoapHeader class. Your Web Service class can then use this class to gather the authentication information. For example, Listing 17.5 shows a simple SOAP header class that accepts a username and password.
Listing 17.5 A Simple SOAP Header Class
1: Imports System.Web.Services 2: Imports System.Web.Services.Protocols 3: 4: Namespace TYASPNET 5: 6: Public Class Authenticator : Inherits SoapHeader 7: Public Username as string 8: Public Password as string 9: End Class 10: 11: End Namespace
| || |
This class, named Authenticator, can be used in the same file as your Web Service class. It contains two public string variables, Username and Password, that will contain the authentication information from the client.
Each class that you want to protect should declare an instance of this Authenticator class. You'll use this instance to restrict access to your Web methods. Each Web method, then, must use the <SoapHeader> attribute in addition to the <WebMethod> attribute:
Public Class DataBaseService dim sHeader as Authenticator ... <WebMethod(), SoapHeader("sHeader")> Public Function MyMethod() as String ... End Function End Class
On the second line, you create the instance of the Authenticator class, stored in the variable sHeader. Then, in the function declaration, you add the <SoapHeader> attribute with the name of your Authenticator instance. This tells ASP.NET that it should expect a SOAP header in addition to the regular communications. Without this header, any messages from the client will be rejected.
Let's build a real-world example using the database service you created yesterday. First, let's modify your Web Service class to take advantage of custom SOAP headers. Listing 17.6 shows your modified class.
Listing 17.6 A Secure Database Service Class
1: Imports System 2: Imports System.Data 3: Imports System.Data.OleDb 4: Imports System.Web.Services 5: Imports System.Web.Services.Protocols 6: 7: Namespace TYASPNET 8: 9: Public Class Authenticator : Inherits SoapHeader 10: Public Username as string 11: Public Password as string 12: End Class 13: 14: Public Class SecureDatabaseService : Inherits WebService 15: private objConn as OleDbConnection 16: private objCmd as OleDbCommand 17: public sHeader as Authenticator 18: 19: <WebMethod(), SoapHeader("sHeader")> public function _ 20: SelectSQL(strSelect as string) as DataSet 21: 22: if sHeader is Nothing then 23: throw new Exception("Error: Invalid login!") 24: end if 25: 26: if Authenticate(sHeader.UserName, _ 27: sHeader.Password) then 28: try 29: objConn = new OleDbConnection("Provider=" & _ 30: "Microsoft.Jet.OLEDB.4.0;" & _ 31: "Data Source=c:\ASPNET\data\banking.mdb") 32: dim objDataCmd as OleDbDataAdapter = new _ 33: OleDbDataAdapter(strSelect, objConn) 34: 35: Dim objDS as new DataSet 36: objDataCmd.Fill(objDS, "tblUsers") 37: return objDS 38: catch ex as OleDbException 39: return nothing 40: end try 41: else 42: return nothing 43: end if 44: end function 45: 46: private function Authenticate(strUser as string, _ 47: strPass as string) as boolean 48: try 49: dim intID as integer = 0 50: objConn = new OleDbConnection("Provider=" & _ 51: "Microsoft.Jet.OLEDB.4.0;" & _ 52: "Data Source=c:\ASPNET\data\banking.mdb") 53: 54: dim strSelect as string = "SELECT UserID " & _ 55: "FROM tblUsers WHERE Username = '" & _ 56: strUser & "' AND Password = '" & _ 57: strPass & "'" 58: objCmd = new OleDbCommand(strSelect, objConn) 59: objCmd.Connection.Open() 60: 61: Dim objReader as OleDbDataReader 62: objReader = objCmd.ExecuteReader 63: do while objReader.Read 64: intID = objReader.GetInt32(0) 65: loop 66: objReader.Close 67: 68: if intID <> 0 then 69: return true 70: else 71: return false 72: end if 73: catch ex as OleDbException 74: return false 75: end try 76: end function 77: End Class 78: 79: End Namespace
| || |
Save this file as Database.vb in the day17 folder. Beginning on line 9 is the authentication class you build that inherits from SoapHeader. This class will be sent along as extra baggage during trips to and from the service. It has only two variables, UserName and Password, which you'll use to authenticate users.
On line 17, you create a new instance of the SOAP header class named sHeader. This instance will contain the authentication information passed from the client, which will be used by your methods to determine if the user has the correct permissions.
The new database service class is called SecureDatabaseService, just so you can keep the services you develop straight in your mind. Your SelectSQL function begins on line 19, but it's different from what you may remember from yesterday. First of all, in the declaration you include the SoapHeader attribute, set to the instance of the SOAP header class, sHeader. The rest of the declaration is the same as normal.
The first thing you do inside this function is determine if a username and password were supplied with the call from the client. If these parameters are missing, sHeader will equal nothing and you want to exit your function. You throw a custom Exception error on line 23, which is used to tell the client that he's forgotten to supply something.
It's not necessary to throw a specific error because ASP.NET will alert the user if the proper header isn't passed along. This error is shown in Figure 17.8.
Figure 17.8. An error is generated if you forget to supply the SOAP header.
On line 26, you call your custom Authenticate method, which performs a database lookup to determine if the specified user is allowed to access this function. If the credentials are valid, Authenticate returns true and you move into the actual body of the function, beginning on line 28. This part should look familiar. It hasn't changed from the last version of this class. It executes the supplied query and returns a DataSet object.
The Authenticate function on line 44 performs the validation check. This function checks the database to see if the username and password supplied by the SOAP header describe an actual user. If so, it returns true. Otherwise, it returns false, and if so, you'll want to return nothing from your service, as done on line 42.
Compile this service with the following command:
vbc /t:library /out:..\..\bin\TYASPNET.dll /r:System.dll /r:System.data.dll /r:System.xml. dll /r:System.Web.dll /r:System.Web.Services.dll Database.vb ..\day15\user.vb
Build an .asmx file on the server that simply specifies the class name, as follows:
<%@ WebService %>
Save this file as SecureDatabaseService.asmx. Next, you need to build the proxy class from the client (in this case, the client and server are the same computer). Use the following command to generate the proxy:
wsdl /language:VB /namespace:TYASPNET.Clients http://localhost/tyaspnet21days/day17/ SecureDatabaseService.asmx?WSDL
Again notice that you place this proxy in a new namespace, TYASPNET.Clients. Since all your work is being done on the same machine, this is to prevent class names from overlapping in one namespace.
The generated proxy class will be called SecureDatabaseService.vb by default. If you take a look at the generated code, you'll notice that the proxy has retrieved two things from the service: the authenticator and the secure database class. You'll need to use both of these in your client application to access the service.
Finally, compile this proxy with the following command:
vbc /t:library /out:..\..\bin\TYASPNET.Clients.dll /r:System.dll /r:System.data.dll /r: System.XML.dll /r:System.Web.dll /r:System.Web.Services.dll SecureDatabaseService.vb
All that's left is to build the ASP.NET page that will access this service. This page should allow the user to enter a username and password, as well as the query she wants to execute. Listing 17.7 contains the code for this page.
Listing 17.7 The ASP.NET Page to Access Your Service
1: <%@ Page Language="VB" %> 2: <%@ Import Namespace="System.Data" %> 3: 4: <script runat="server"> 5: sub Submit(Sender as Object, e as EventArgs) 6: dim objService as new TYASPNET.Clients.SecureDatabaseService_ 7: dim sHeader as new TYASPNET.Clients.Authenticator 8: dim objDS as new DataSet 9: 10: sHeader.Username = tbUser.Text 11: sHeader.Password = tbPass.Text 12: objService.Authenticator = sHeader 13: 14: try 15: objDS = objService.SelectSQL(tbQuery.Text) 16: 17: if objDS is nothing then 18: Response.Write("Invalid username or password") 19: else 20: DataGrid1.DataSource = objDS 21: DataGrid1.DataMember = "tblUsers" 22: DataGrid1.Databind() 23: end if 24: catch ex as exception 25: Response.Write("Invalid username or password") 26: end try 27: end sub 28: </script> 29: 30: <html><body> 31: <form runat="server"> 32: Username: 33: <asp:Textbox runat="server"/><br> 34: Password: 35: <asp:Textbox runat="server" 36: TextMode="password" /><p> 37: Enter a query: 38: <asp:Textbox runat="server"/> 39: <asp:Button runat="server" 40: text="Submit" 41: OnClick="Submit" /> 42: <p> 43: <asp:DataGrid 44: runat="server" BorderColor="black" 45: GridLines="Vertical" cellpadding="4" 46: cellspacing="0" width="100%" 47: Font-Name="Arial" Font-Size="8pt" 48: HeaderStyle-BackColor="#cccc99" 49: ItemStyle-BackColor="#ffffff" 50: AlternatingItemStyle-Backcolor="#cccccc" /> 51: 52: </form> 53: </body></html>
| || |
The only method in this page is Submit, the event handler for the Submit button. When this method is fired, it creates two new objects from your proxy class: the authenticator and the secure database class. You set the username and password properties of the authenticator to the values entered in the text boxes on lines 11 and 12. You then set this instance of the authenticator to the secure database's Authenticator property. The proxy now knows what to send along with the commands.
Finally, wrapped in the try block, you execute the service's method and bind the data to a DataGrid. Load this page in the browser and enter some user credentials and a SQL query. If the credentials are valid (that is, if they represent an actual user in the tblUsers table), you'll see the returned data, as shown in Figure 17.9.
Figure 17.9. The results of a successful query to the secure service.
If you enter invalid credentials, however, you should see an error message stating "Invalid Username or Password." This is the reason for the if statement on line 17. Without it, supplying invalid credentials would result in returning an empty DataSet. Figure 17.10 shows an unsuccessful attempt.
Figure 17.10. The results of an unsuccessful query to the secure service.
SOAP headers are a common and effective tool for implementing secure Web Services. Although you pulled your authentication information from a database here, you can verify the supplied information any way you like.
There is a drawback to using SOAP headers, however. When you send user credentials across the Internet via SOAP headers, they're sent as plain text in XML format. This means that they're vulnerable to prying eyes. One method to get around this is to encrypt the user credentials prior to sending them and provide an equivalent decryption algorithm in your Web Service. Encryption is beyond the scope of this book, however. For other ways to secure your application, see Day 21, "Securing Your ASP.NET Application."