Custom Role-Based Authentication

With custom authentication, your code performs the work that IIS accomplishes automatically: investigating the supplied account information and verifying it with a database lookup. As with IIS, you need to perform this authentication at the beginning of every request or develop a ticket-issuing system, as described in the next section.

Most developers have used some form of custom authentication in their applications. Often, custom authentication just verifies that a user exists in the database and then allows access to the rest of the application. Chapter 18 presents one such example of a security system that can easily be integrated into any distributed application.

A more sophisticated system will use multiple tables, as shown in Figure 13-4. Here, tables track users, roles, and the permissions granted to each role. Two other tables are used to create many-to-many relationships between these three entities.

Figure 13-4. Database tables for custom authentication

graphics/f13dp04.jpg

It helps to consider a simple example. Figure 13-5 shows sample information: a single user, a role, and a permission record. Figure 13-6 shows the data used to link these records together, effectively giving the administrator the ability to delete records and assigning the testuser account to the Administrator role. If the Allowed column of the Permissions table has a value of 1, the permission is granted. If the Allowed column contains a 0, the permission is denied.

Figure 13-5. Creating a user, a role, and a permission

graphics/f13dp05.jpg

Figure 13-6. Linking users, roles, and permissions

graphics/f13dp06.jpg

Note

For tighter security, you should encrypt the password in the database. When the user accesses your XML Web service, you encrypt the supplied password and then see whether it matches the value in the database. Encryption is discussed later in this chapter.


To bring this all together in a simple, maintainable way, you can include in your remote components a private method that checks for a requested permission. To support this method, you need to add a dedicated stored procedure, as shown in Listing 13-8.

Listing 13-8 A stored procedure for verifying a permission
 CREATE Procedure CheckPermission  (     @UserName    varchar(25),     @Permission    varchar(25)  )  AS SELECT MIN(Allowed) FROM RolesPermissions     INNER JOIN Permissions ON Permissions.ID = PermissionID     INNER JOIN Roles ON Roles.ID = RoleID     INNER JOIN UsersRoles ON Roles.ID = Roles.ID     INNER JOIN Users ON Users.ID = UsersRoles.UserID WHERE Users.UserName=@UserName AND      Permissions.Name=@Permission 

The CheckPermission stored procedure joins together all the tables to find out whether a given permission is provided to a given user. The MIN function returns the lowest value from the Allowed column of all matching records. This handy trick allows permissions to be both granted and denied. This logic springs into action when a user matches more than one group. Consider, for instance, the case in which testuser is a member of both a Programmers and a Contractors group. Table 13-2 summarizes the possible outcomes.

Table 13-2. Authorization with Multiple Group Membership

Permission in Programmers Group

Permission in Contractors Group

Authorization Outcome

Granted (Allowed = 1)

Granted (Allowed = 1)

Granted (Returns 1)

Granted (Allowed = 1)

Denied (Allowed = 0)

Denied (Returns 0)

Granted (Allowed = 1)

Not specified (no record for this permission)

Granted (Returns 1)

Listing 13-9 shows the private .NET function you can use to test a permission. Note that this method assumes that the user's password information has already been verified. Alternatively, you can alter the stored procedure to also accept and use password information.

Listing 13-9 Authorizing a permission
 Private Function PermissionDemand(user As String, _   permission As String) As Boolean     Dim con As New SqlConnection(ConnectionString)     Dim cmd As New SqlCommand("CheckPermission", con)     Dim r As SqlDataReader     cmd.CommandType = CommandType.StoredProcedure     Dim Param As SqlParameter     Param = cmd.Parameters.Add( _      "@UserName", SqlDbType.NVarChar, 25)     Param.Value = user     Param = cmd.Parameters.Add( _      "@Permission", SqlDbType.NVarChar, 25)     Param.Value = permission     Dim Allowed As Integer = 0     Try         con.Open()         r = cmd.ExecuteReader()         If r.Read() Then             Allowed = r(0)         Else 
             ' No matching permission record.             Allowed = 0         End If         r.Close()     Finally         con.Close()     End Try     If Allowed = 1 Then         Return True     Else         Return False     End If End Function 

The client uses the following pattern:

 If PermissionDemand(userName, "DeleteRecord")     ' (Allow record deletion.) Else     Throw New SecurityException("Insufficient permissions.") End If 

Optionally, you can change the PermissionDemand function into a subroutine and give it the responsibility of throwing an exception if the permission is denied. The client would then use this pattern:

 ' Attempt to validate the user. PermissionDemand(userName, "DeleteRecord") ' If the code reaches this point, no exception was thrown. ' (Allow record deletion.) 

Ticket Systems

This custom authorization system suffers from one flaw. The user's authentication information (username and password) must be submitted with each method call. This leads to the cumbersome coding style shown here, in which two unrelated parameters are tacked onto every method:

 Public Function GetProductInfo(productID As Integer, _   username As String, password As String) As ProductDetails     ' (Authenticate user.)     ' (Perform task.) End Function 

Worse yet, the XML Web service has to validate this information every time. This can lead to a significant scalability bottleneck, especially if you store user information using the same data source as the rest of the application data (which is more than likely).

To counter these problems, you need to implement some sort of ticket system. With a ticket system, the client begins a session by calling a remote Login method with user account information. The Login method authenticates the user and issues a new ticket. Typically, this ticket is a globally unique identifier (GUID). You can use random numbers or some other ticket scheme, but this raises the possibility that a hacker might guess a ticket value based on previous values.

The client then submits this ticket on subsequent method calls. Every other server-side method performs its authentication by validating the ticket.

If properly implemented, a ticket system provides a number of benefits:

  • It can drastically improve performance when combined with caching.

    If you store tickets in an in-memory cache, ticket validation is almost instantaneous and doesn't require a trip to the database.

  • It limits the effects of a security breach.

    If hackers intercept a Web method request, they can (at worst) steal the session ticket and hijack a user session. However, they won't have the account information to log in to the system on their own.

  • It enables you to use SSL effectively.

    SSL encrypts communication between a client and an XML Web service. However, it imposes additional overhead. With a ticket system, you can use SSL to encrypt requests to the Login method and protect user account information, but you won't need to use it for other methods.

The remainder of this section focuses on the essentials of a ticket system that uses ASP.NET application state. This removes some of the need for database access but can hamper performance for large systems because of the amount of information that needs to be stored in memory. Chapter 18 shows a case study with a complete example of a ticket system that incorporates caching rather than application state.

The first step is to create a class that represents the ticket information you need to retain, as shown in Listing 13-10. In this example, the ticket class stores a UserID, which would allow you to look up application-specific permission information from the database. Alternatively, you might want to store this type of information in the ticket class itself so that you could fill it once and then access it easily in any method.

Listing 13-10 A ticket class
 Public Class TicketIdentity     ' The unique value (e.g. 382c74c3-721d-4f34-80e5-57657b6cbc27).     Public Ticket As Guid     ' The IP address of the client.     Public HostAddress As String     ' The user identity information.     Public UserID As Integer     Public Sub New()         ' Create a new GUID when the class is created.         Me.Ticket = Guid.NewGuid()     End Sub End Class 

Then you create the remote Login Web method (as shown in Listing 13-11). This method issues a new ticket, stores the ticket information in server memory, and returns the ticket to the user.

Listing 13-11 The Login Web method
 Public Function Login(userName As String, password As String) _   As Guid     ' Verify that the user name and password are in the database.     ' (Database code omitted.)     ' If they are, create a new ticket.     Dim Ticket As New TicketIdentity()     ' Store the user address information.     Ticket.HostAddress = Context.Request.UserHostAddress     ' Add this ticket to Application state.     Application(TicketIdentity.Ticket.ToString()) = TicketIdentity     ' Return the ticket GUID.     Return TicketIdentity.Ticket End If 

The third ingredient is the private server-side method that performs the authentication (as shown in Listing 13-12). It accepts the ticket, looks up the full ticket information, and then performs any additional authorization steps that are necessary.

Listing 13-12 The authentication logic
 Private Sub AuthenticateSession(ByVal ticket As Guid)     Dim Ticket As TicketIdentity     Ticket = Application(ticket.ToString())     Dim Success As Boolean = False     If Not Ticket Is Nothing                  ' Matching ticket was found.         Success = True         ' You could also verify that the IP address is unchanged by         ' uncommenting the following code.         ' If Ticket.HostAddress <> Context.Request.UserHostAddress         '     Success = False         ' End If     End If     If Not Success Then         Throw New Security.SecurityException("Session not valid.")     End If End Sub 

Note that the Login method stores the user's IP address to prevent hijacked sessions. When the ticket is authenticated, the XML Web service can verify that the IP address hasn't changed (in other words, that the request is being made from the same computer). However, this system won't always work, particularly if the user is behind a proxy server that uses a dynamic IP address that changes unpredictably. Therefore, you should uncomment the preceding code only if you're working with a known (probably internal) environment in which this technique is practical.

Now that these ingredients are in place, every method can make use of the AuthenticateSession method. Listing 13-13 presents an example.

Listing 13-13 Authenticating a ticket
 <WebMethod()> _ Private Function GetProductName(ID As Integer, ticket As Guid) _   As String     ' Attempt to validate the ticket.     AuthenticateSession(ticket)     ' Now perform the task.     ' (Method-specific code omitted.) End Function 

The client follows the pattern shown in Listing 13-14.

Listing 13-14 Using a ticket on the client side
 Dim Proxy As New localhost.TicketServiceTest() ' Get the ticket. Dim Ticket As Guid = Proxy.Login("testuser", "secret") ' Perform some work using the ticket. Dim Product As String Product = Proxy.GetProductName(234, Ticket) 

Passing Tickets Automatically

The ticket-based system we've considered so far doesn't solve the problem of additional method parameters. The client still needs to hold onto the ticket and submit it with every request. An easier approach is possible one that automatically tracks and submits the ticket transparently.

With an XML Web service, the secret is to develop a custom SOAP header. The SOAP header (which contains the ticket GUID) is transmitted automatically with every Web method request. The client doesn't need to perform any additional actions. Listing 13-15 shows a sample SOAP header class.

Listing 13-15 A SOAP ticket header
 Public Class TicketHeader     Inherits SoapHeader     Public Ticket As Guid End Class 

This technique is demonstrated in detail with the case study in Chapter 18, and so we won't delve into it in any more detail here.

With .NET Remoting, a similar technique is available with the CallContext class. The first step is to create a custom object that encapsulates the information you need to transmit automatically. (This plays the same role as a SOAP header in ASP.NET.) This object must be serializable and must implement the ILogicalThreadAffinative interface (in the System.Runtime.Remoting.Messaging namespace) for it to make the jump to other application domains. Note that both the client and the server need a copy of this class definition, so it should be placed in a separate assembly. If you're using interface-based programming (always a good idea), you can place this class in the same assembly that's used to define the required interfaces. Listing 13-16 uses this technique.

Listing 13-16 An ILogicalThreadAffinative object
 <Serializable()> _ Public Class TicketData    Implements ILogicalThreadAffinative     Public Ticket As Guid End Sub 

Your client can create this object and use the CallContext.SetData method to attach it to the current context, as shown in Listing 13-17. It is automatically transmitted to the remote component with every request.

Listing 13-17 Using an automatic ticket on the client side
 Dim RemoteObject As New RemoteClass() ' Get the ticket. Dim Ticket As Guid = RemoteObject.Login("testuser", "secret") ' Set the ticket. Dim TicketObj As New TicketData() TicketObj.Ticket = Ticket CallContext.SetData("ticket", TicketObj) ' Perform some work. ' The ticket is transmitted automatically (and seamlessly) ' with each call. Dim Product As String Product = Proxy.GetProductName(234) 

The remote component can use CallContext.GetData to retrieve this information and validate it, as shown in Listing 13-18.

Listing 13-18 Retrieving an automatic ticket on the server side
 Dim TicketObj As TicketData TicketObj = CType(CallContext.GetData("ticket"), TicketData) 


Microsoft. NET Distributed Applications(c) Integrating XML Web Services and. NET Remoting
MicrosoftВ® .NET Distributed Applications: Integrating XML Web Services and .NET Remoting (Pro-Developer)
ISBN: 0735619336
EAN: 2147483647
Year: 2005
Pages: 174

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