深瞳

夜如深瞳,瞳深如夜

  :: :: 博问 :: 闪存 :: :: 联系 :: 订阅 订阅 :: 管理 ::

Chapter 11. Security

ASP.NET provides authentication and authorization services in conjunction with IIS, supporting Basic, Digest, and Windows authentication. Impersonation using client credentials is also supported on a per-request basis. Role-based security is provided in much the same way as it is with COM+, and allows customized content based on role membership. Finally, forms-based authentication allows applications to provide their own login UI and perform their own credential verification, greatly simplifying a technique already used by many Web sites.

11.1 Web Security

Security is often one of the last issues addressed by developers. It requires thinking about your applications in a different light from what you are accustomed to. As you build your applications, your goal is to make them as useful and easy to use as possible. When you consider security, however, the goal is often quite the opposite. You find yourself asking questions such as "How can I be sure that these people cannot access this portion of my application?" or "How can I validate that the request coming into my application is from who I think it is, and not some person pretending to be someone else?"

With Web applications, the issues of security are magnified because almost all communication to and from the application is performed across long connections. For this reason, one of the most important security issues in Web applications is authentication. Clients want to know whether the site they are looking at is indeed produced by the company it purports to be from, and servers often need to know the identity of a particular client, especially if the application is modifying data on behalf of a client (as with a bank account). The level of security required is very application dependent. Web applications that are simply informative may not care about client credentials and are happy to deal with all clients as anonymous users. Web applications that provide customized content may want to identify clients only if they want to be identified (for customization), and are otherwise happy to let anonymous clients browse the application at their leisure. Web applications that provide client services (stock portfolios, bank accounts, and so on) typically need to be much more careful about client identity and often require client authentication before any pages on the site can be displayed.

As a Web application developer, you must be aware of the level of authentication required by your application and add only the security services necessary. Each additional security requirement of your application makes it more complex and may have significant impact on its design.

11.1.1 Server Authentication

Server authentication is often very important to clients. They want to be sure that the Web site they are viewing is in fact being published and run by the authority it claims to be. This is especially true if a client is sending sensitive information, such as credit card or bank account numbers, to the server. If the server has not somehow authenticated itself with the client, the client cannot be sure that someone is not surreptitiously pretending to be that site just to collect information from clients.

Authenticating servers in a well-controlled local network is a solved problem. A particular machine is designated as the domain controller and takes responsibility for validating that each communication is authentic. Protocols such as Kerberos work well in this environment. As soon as we try to scale these techniques to the Web, however, we quickly run into client firewall constraints that make it impossible to exchange secret keys safely.

One solution to this is to use a technology labeled digital certificates. Digital certificates rely on public key cryptography that lets servers maintain a private key and publish their public key, which clients can use to authenticate that the server is indeed who it says it is. There is still a problem, however; someone may intercept the sending of the public key and replace it with his own. The person in the middle who swapped in his own public key can then potentially intercept and modify subsequent communication between that client and the server.

The solution to this is to designate a trusted authority that can vouch for the public keys of anyone that registers them with this authority. Companies (such as Verisign) have set up such a system and, for a fee, will store and vouch for your public key. As long as everyone trusts that the third-party company is telling the truth, and clients make the effort to verify server certificates, digital certificate authentication of servers works reasonably well. The one issue remaining is how to verify that the third party is who it claims to be (how do you know that Verisign is in fact the one performing the verification of a public key?). Distributing the public keys of companies like Verisign with browser installations solves this.

11.1.2 Client Authentication

With server authentication, there is little question about the purpose of the authentication. It is always to guarantee the identity of the server to the client. Client authentication, on the other hand, can be used for several different purposes. Some Web sites need to authenticate clients so that they can customize their content based on the preferences of those clients (for example, an e-commerce site may want to display on the front page items it thinks you will purchase when you visit it). Other sites control access to all or portions of the site based on client identity. Finally, some sites require that clients have credentials on the server (Windows logins) so that operations performed through the Web site can be done with the credentials of the client (very common with intranet applications). Each of these types of client authentication requires different levels of assurance and can be implemented in several ways.

Through IIS, you can specify the level of client authentication to use for a given Web application (or a page within an application). By default, IIS prefers not to perform client authentication because it will incur more server involvement with each request, slowing down the system. If no client authentication is selected (Anonymous), every request is treated as anonymous, and a designated account (IUSR_MACHINE, where MACHINE is the name of your machine) is used for credentials. To force client authentication with IIS, you can disable anonymous access and then choose one of the available client authentication methods. Figure 11-1 shows the IIS configuration form controlling client authentication for a virtual directory.

Figure 11-1. IIS Client Authentication Settings

The options for client authentication in IIS are Basic authentication, Integrated Windows authentication, and Digest authentication. Basic authentication requires that the client pass his password as clear text, which makes sense only if it is being encrypted, thus the use of Basic authentication is typically restricted to sites using SSL as their protocol. Even with the password being encrypted, however, it is still available to scripts within the Web application through the AUTH_PASSWORD intrinsic variable, which may not be something you want.

Integrated authentication attempts to use native Windows authentication. This works by negotiating Kerberos or NTLM with proprietary extensions to HTTP, and works only for clients running IE who have a valid account on the server and who are not separated from the server by a firewall or proxy, because Kerberos is typically inhibited by a firewall, and NTLM becomes nonoperational through a proxy. This option may be useful for local intranets, but is typically much less useful for Internet servers.

Digest authentication uses a challenge/response technique to verify a client password. This means that the password must be available on the server, and thus the server must be a domain controller on the server network, which is usually not advisable, because having someone hack into your domain controller could be disastrous. It is also possible to configure IIS to accept client certificates for authentication. Certificate authentication is similar to the certificate authentication used to authenticate servers, but the server requests a certificate of the client instead of the other way around. For this to work, all clients must have valid certificates to present to the server to gain access to the site, as well as the server requiring a valid certificate.

In general, a large percentage of Windows-based Web applications do not rely on IIS to perform client authentication because it requires that clients do something special (obtain a server account or a certificate, for example), and most sites want to allow as many users access as possible. Instead, these sites typically authenticate clients at the application level and manage the details of keeping passwords and accounts themselves. This gives them much more flexibility and control in designing their client authentication system.

11.2 Security in ASP.NET

As we will see, you can also require client authentication in ASP.NET applications by adding the appropriate elements to your application's web.config file. Once the client has been authenticated, whether it was specified by IIS or by ASP.NET, information about that client is available through the User property of the Page class. This property is a pass-through property pointing to the User property of the current HttpContext class on any given request. Listings 11-1 and 11-2 show the definition of this property in each class.

Listing 11-1 User Property in the Page Class
public class Page : TemplateControl, IHttpHandler
{
     public IPrincipal User {get;}
     ...
}
Listing 11-2 User Property in the HttpContext Class
public class HttpContext :
          IObjectServiceProvider
{
     public IPrincipal User {get;set;}
     ...
}

The User property points to an implementation of the IPrincipal interface. If the client was authenticated using Windows authentication, it points to an instance of the WindowsPrincipal class. Otherwise, it may point to a custom IPrincipal implementation or, more likely, an instance of the GenericPrincipal class. (We will show an example of using the GenericPrincipal class later to implement custom roles.) The IPrincipal interface is shown in Listing 11-3 and consists of a property and a method. The property (Identity) is an interface pointer to the identity of the current client, and the method (IsInRole) can be called to query whether the client is in a particular role. If the client was authenticated using Windows authentication, IsInRole simply checks group membership in the user's access token. If the client was authenticated in some other way (forms authentication, for example), this method should be able to tell you whether the client belongs to a particular named role, which is defined by the application.

Listing 11-3 The IPrincipal Interface
public interface IPrincipal
{
     IIdentity Identity {get;}
     bool IsInRole(string role);
}

Listing 11-4 shows the IIdentity interface, consisting of three read-only properties: IsAuthenticated, Name, and AuthenticationType. You can use IsAuthenticated to distinguish between authenticated and anonymous clients, and you can use Name to query the identity of the client. If the client was authenticated, you can then use the AuthenticationType property of this interface to query the type of authentication that was used. Listing 11-5 shows an example of accessing an authenticated client's information. In this case, when the page loads, we extract the current client's name and type of authentication, and display them in labels on our page.

Listing 11-4 The IIdentity Interface
public interface IIdentity
{
     string AuthenticationType {get;}
     bool IsAuthenticated {get;}
     string Name {get;}
}
Listing 11-5 Accessing Authenticated Client Information
<!� File: AccessClientInfo.aspx �>
<%@ Page language=C# %>
<html>
<script runat=server>
  void Page_Load(Object src, EventArgs e)
  {
    AuthUser.Text = User.Identity.Name;
    AuthType.Text = User.Identity.AuthenticationType;
  }
</script>
<body>
<form runat="server">
<h1>My test security page</h1>
<h2>Can you read this?</h2>
  <h3>user:</h3><asp:label id=AuthUser runat=server />
  <h3>authType: </h3><asp:label id=AuthType runat=server />
</form> </body> </html>

11.2.1 Client Authentication and Authorization

As we have seen, you can configure client authentication through IIS, and once that is in place, it applies to ASP.NET files as well. You can also specify authentication and authorization in ASP.NET via the web.config file of your application. You specify the client authentication type by using the authentication element, as shown in Listing 11-6. There are four possible values for the mode attribute of the authentication element: Windows (default), Forms, Passport, or None. If you use Windows authentication, you are shifting responsibility for authentication to the Web server, so you must pick an authentication mode in IIS, such as Integrated, Basic, Digest, or SSL client authentication.

Listing 11-6 Setting the Authentication Mode with web.config
<!� File: web.config �>
<configuration>
  <system.web>
     <!� mode can also be Forms, Passport, or None �>
     <!� the default is Windows �>
     <authentication mode="Windows"/>
  </system.web>
</configuration>

Once you have configured the type of authentication you would like ASP.NET to use, you need to give it a reason to authenticate clients. You do this by restricting access to all or portions of your application via the authorization element. To control access to your site, you add deny and allow subelements to the authorization element, specifically denying or allowing access to users or roles. The wildcard "*" is used to represent all users, and the wildcard "?" is used to represent anonymous users. For example, to deny anonymous users access to your site, you would specify an authorization element with a single deny subelement set to"?", as shown in Listing 11-7.

Listing 11-7 Denying Anonymous Users
<!� File: web.config �>
<configuration>
  <system.web>
     <authorization>
          <deny users="?" />
     </authorization>
  </system.web>
</configuration>

Both the deny and allow elements provide three attributes�users, roles, and verbs梐nd the values of these attributes support comma-delimited lists of users, roles, or verbs. In Listing 11-7, we used the users attribute to deny anonymous users access to our application. We could also construct more complex authorization requirements by adding multiple deny and allow elements. For example, in Listing 11-8, we explicitly grant authenticated users MyDomain\bob and MyDomain\alice complete access to our application. The second allow element grants all users the right to issue GET requests to pages in our application, and the final element, deny, restricts access to unauthenticated users. When ASP.NET checks the authorization element, it traverses the list of allow and deny elements in declaration sequence, and the first element that matches the credentials of the client request determines whether access is granted or denied. In our example, if the incoming client has been authenticated as Bob or Alice, he (or she) will be granted complete access. If an anonymous user attempts to access our site with a GET request, the request will be serviced because of our second allow statement. However, if an anonymous client attempts to access a page in our application with a POST request, she will be either given a chance to authenticate or simply denied access.

Listing 11-8 A More Complex Authorization Declaration
<configuration>
  <system.web>
     <authorization>
       <allow users="MyDomain\bob, MyDomain\alice"/>
       <allow users="*" verbs="GET" />
       <deny users="?" />
     </authorization>
  </system.web>
</configuration>

One last question to ask about the example in Listing 11-8 is, What happens if Bob or Alice issues a GET request to one of our pages? Because GET requests are allowed via anonymous access by our second allow element, even the initial requests from Bob and Alice will typically be processed as anonymous requests. Only when the client attempts to do something that is prevented for anonymous users will authentication take place. In general, try to keep your authorization logic simple and obvious, because the more complex the rules become, the more likely you are to make a mistake and grant access where you should have denied it.

Authorization can be specified differently for different files and subdirectories in your application. To change the authorization settings for different directories in your application, you can add a web.config file to each subdirectory specifying authorization settings. Authorization is always applied with local web.config file settings first, and only if no match is found will it look in additional configuration files in the hierarchy above. Thus, if you grant anonymous users access to your top-level site but add a web.config file that denies anonymous users access to a particular subdirectory, clients must authenticate before they can access pages in that subdirectory. Alternatively, you can specify different authorization settings for different files (and subdirectories) from the top-level web.config file of your application using the location element. Listing 11-9 shows an example of restricting access to a top-level file called secret.aspx and a subdirectory called secret, while granting anonymous access to the remainder of the pages in the application.

Listing 11-9 Using the location Element for Fine-Grained Authorization
<!� File: web.config �>
<configuration>
  <system.web>
     <authorization>
        <!� allow all users by default �>
       <allow users="*" />
     </authorization>
  </system.web>

    <!� use location element to restrict access to a
         particular file �>
  <location path="secret.aspx">
  <system.web>
     <authorization>
       <deny users="?" />
     </authorization>
  </system.web>
  </location>

    <!� use location element to restrict access to a
         subdirectory �>
  <location path="secret">
  <system.web>
     <authorization>
       <deny users="?" />
     </authorization>
  </system.web>
  </location>
</configuration>

One final note on ASP.NET authentication to keep in mind is that ASP.NET can enforce authorization and authentication only when requests are dispatched to the ASP.NET worker process. This means that if you create an authorization scheme for your application using ASP.NET's configuration files, and a user requests a plain .htm file or .gif file, the ASP.NET authentication is not consulted, and whatever settings IIS has for your virtual directory are applied (typically granting anonymous access). The only work-around for this now is to route all file requests to the ASP.NET worker process by configuring the IIS metabase to use the aspnet_isapi.dll ISAPI extension as the handler for all Web file types (.htm, .gif, .jpg, and so on). Keep in mind, however, that this also slows down access to these file types for your application.

11.2.2 Forms Authentication

As mentioned earlier, relying on Windows authentication is rarely what you want for an Internet application with a broad user base because it would require that each client have a valid Windows account on your server. Instead, in your web.config file, you can specify one of two cookie-based authentication models (Passport or Forms), which provide many of the details of managing application-level authentication. Passport mode uses a cookie-based authentication technique that relies on Microsoft's Passport authentication technology, which lets sites and clients register with Microsoft to have a central point for client authentication. Forms authentication also uses cookies but leaves the details of authentication in your hands. You do not have to build your own cookie-based authentication scheme, as developers have had to do in the past, because ASP.NET takes care of the details of enforcing authorization and managing an authentication cookie.

The idea behind forms-based authentication is shown in Figure 11-2. When a user first requests a resource that requires authentication, the server redirects him to a designated login page. This login page collects the user's name and password, PIN, or some other bit of proof, and the application then authenticates him (through some database of users and passwords, presumably). Once the user successfully logs in, the server grants him an authentication cookie (which should be a cryptographically secure value that is infeasible to guess). The user is then redirected to the original page that he requested, but this time he presents an authentication cookie with the request and is granted access to the page. This authentication cookie lasts throughout the session, and thus the client can access all pages allowed by the authorization policy by presenting this cookie with each access. If this cookie is made persistent on the client's machine, it can be used for subsequent sessions from that same machine as well (this is manifested in the commonly seen "remember my password" check box many sites provide). Be aware, however, that persistent authentication cookies can easily be hijacked by anyone with access to the client machine. Forms-based security works well for servers that intend to service a large number of clients and want to manage client registration and authentication at the application level rather than relying on system-provided authentication requiring much more setup.

Figure 11-2. Forms Authentication

ASP.NET provides much of the infrastructure necessary to put together a cookie-based authentication Web application. First, if you specify Forms authentication in your web.config file, you can specify a loginUrl attribute, which should point to the page you want users to be redirected to if they attempt unauthenticated access to your application. If you then explicitly deny anonymous users by using the authorization element, ASP.NET takes care of routing unauthenticated clients to your designated login page for authentication. An example of a web.config file configured for forms-based authentication is shown in Listing 11-10.

Listing 11-10 Specifying Forms Authentication
<!� File: web.config �>
<configuration>
  <system.web>
    <authorization>
      <deny users="?"/>
    </authorization>

    <authentication mode="Forms">
      <forms loginUrl="login.aspx" />
    </authentication>
  </system.web>
</configuration>

Your only other major task to get cookie-based authentication working is to implement the login page to grant or deny the authentication cookie. ASP.NET provides the class FormsAuthentication, shown in Listing 11-11, so that you can do this. This class consists primarily of a number of static methods that control the authentication cookie. For example, to grant an authentication cookie to a client, you call the SetAuthCookie method, and ASP.NET makes sure that a new cookie is generated and set for the client. ASP.NET also checks incoming requests to verify that they are in fact presenting a valid authentication cookie, and if not, routes them to your designated login page. This class also gives a useful method called HashPass-wordForStoringInConfigFile, which takes a string and perform a one-way hash on it (using either SHA1 or MD5 algorithms). The next time a user presents her password to you, you can verify it by running the hash algorithm on it again and comparing the resultant string with the hash you stored earlier. This lets you avoid storing passwords in clear text in your database. We discuss password security in more detail later in this chapter.

Listing 11-11 FormsAuthentication Class
public class FormsAuthentication
{
  public static string FormsCookieName {get;}
  public static string FormsCookiePath {get;}

  public static bool Authenticate(string name,
                                  string password);
  public static FormsAuthenticationTicket
                   Decrypt(string s);
  public static string Encrypt(
                  FormsAuthenticationTicket tk);
  public static HttpCookie GetAuthCookie(string userNm,
                                           bool bPersist);
  public static string GetRedirectUrl(string userNm,
                                        bool bPersist);
  public static string
                HashPasswordForStoringInConfigFile(
                            string psswd, string format);
  public static void RedirectFromLoginPage(string userNm,
                            bool bPersist);
  public static void SetAuthCookie(string userNm,
                                     bool bPersist);
  public static void SignOut();
  //...
}

Listing 11-12 shows a sample login page implementation. Note that in this example, we are calling the FormsAuthentication.RedirectFromLoginPage method, which takes care of granting the authentication cookie to the client and then redirecting the client to the page she originally requested. In this example, we have hard-coded the user checks, but in a real application, this information would most likely live in a database.

Listing 11-12 Forms-Based Authentication Example條ogin.aspx
<!� File: Login.aspx �>
<%@ Page language=C# %>
<html>
  <script runat=server>
  public void OnClick_Login(Object src, EventArgs e)  {
     if (((_username.Text == "Bob") &&
         (_password.Text == "geek")) ||
          ((_username.Text == "Alice") &&
         (_password.Text == "geek")))
       FormsAuthentication.RedirectFromLoginPage(
                _username.Text, _persistCookie.Checked);
     else
       _message.Text = "Invalid login: Please try again";
  }
  </script>
  <body>
    <form runat=server> <h2>Login Page</h2>
       User name:
       <asp:TextBox id="_username" runat=server/><br/>
       Password:
       <asp:TextBox id="_password"
                      TextMode="password" runat=server/>
       <br/>
       Remember password?:
       <asp:CheckBox id=_persistCookie runat="server"/>
       <br/>
       <asp:Button text="Login" OnClick="OnClick_Login"
                   runat=server/><br/>
       <asp:Label id="_message" ForeColor="red"
                  runat=server />
    </form></body>
</html>

If you want further control over the authentication cookie used by the forms authentication module, there are several additional attributes that you can apply to the forms element, as listed in Table 11-1. First of all, by default the cookie will timeout after 30 minutes, so if you expect users of your application to submit requests more than 30 minutes apart, and you don't want to force them to reauthenticate, you can increase this value. On the other hand, it may often be wise to decrease this value to something closer to 5 or 10 minutes, because if someone were accessing private information from an Internet café, for example, they wouldn't want someone else to come in within 30 minutes after they leave and be able to view that private data. The protection attribute lets you specify how much care is taken to protect the cookie. By default, the cookie is both encrypted and Message Authentication Code (MAC) verified so that it cannot be easily tampered with or read. If for some reason you want to decrease the amount of protection on the cookie, you can do so with this attribute. Unless you have a really good reason to change this setting, however, you should leave it alone. Finally, the path attribute lets you specify the path that will be prepended to the cookie. This defaults to"/", which is the preferred setting, since some browsers treat cookie paths with case sensitivity and others without it; so leaving it as "/" works with the largest number of browsers.

Table 11-1. Attributes of the forms Authentication Element

Attribute

Values

Default

Description

name

String

.ASPXAUTH

Name of the cookie

loginUrl

URL

login.aspx

URL to redirect client to for authentication

protection

All, None, Encryption, Validation

All

Protection mode for data in cookie

timeout

Minutes

30

Duration for nonpersistent cookie to be valid (reset on each request)

path

Path

"/"

Path to use for cookie

11.2.3 Authentication Cookies in Web Farms

By default, authentication cookies used by the forms-based authentication module are both encrypted and MAC verified. The decryption and validation keys used to perform these tasks are automatically generated machine by machine. This means that if you are using forms-based authentication in a Web farm environment, where each request potentially can be served by a different machine in the farm, the key validation will quickly fail. To deal with this, you can use the machineKey element in your application's web.config file (or you can modify the one in the systemwide machine.config file) to use a fixed value for both keys. If all machines in a Web farm are configured to use the same pair of keys, the cookie protection in forms authentication will work properly. Listing 11-13 shows a sample web.config file that has explicitly set the machineKey element to use fixed key values for both the validation key and the decryption key.

Listing 11-13 Using Explicit Validation and Decryption keys
<configuration>
  <system.web>
  <authentication mode="Forms"/>
  <authorization>
    <deny users="?"/>
  </authorization>
<machineKey
validationKey="F18815BDA3E05869EEFA53C531A696B187DA31A0F298E0FAB869AC92E292F1008CD5EC1B5C887B39F9559C7ED6BE66242A42E028CC5B8306D0CD1F5784A4FBC9"
decryptionKey="9F9E881DFCED3092FDE726CA286B0459375E42DFD3000C20"
               validation="SHA1"/>
  </system.web>
</configuration>

This example uses a 64-byte validation key and a 24-byte decryption key, the maximum length allowed by the encryption and validation algorithms in ASP.NET. It is wise to create these keys using strong random algorithms, such as those provided by the RNGCryptoServiceProvider. Listing 11-14 shows a sample console application that you can use to generate keys of arbitrary length by passing in the desired length on the command line to the program.

Listing 11-14 Program to Generate Strong Random Keys Using RNGCryptoServiceProvider
// File: genkey.cs
using System;
using System.Text;
using System.Security;
using System.Security.Cryptography;

class App
{
  static void Main(string[] argv)
  {
    int len = 48;
    if (argv.Length > 0)
      len = int.Parse(argv[0]);

    byte[] buff = new byte[len];
    RNGCryptoServiceProvider rng =
                             new RNGCryptoServiceProvider();
    rng.GetBytes(buff);
    StringBuilder sb = new StringBuilder(len);
    for (int i=0; i<buff.Length; i++)
      sb.Append(string.Format("{0:X2}", buff[i]));

    Console.WriteLine(sb);
  }
}

11.2.4 Optional Authentication

Another scenario that comes up frequently is the need to let clients authenticate themselves if they want to or to let them simply use the site anonymously. Once a client has authenticated himself, you may elect to customize the contents of the site, or perhaps some subset of the pages of the site are available only if the client has been authenticated. In this scenario, we do not want to send every unauthenticated client to a default login form. More often, it makes sense to have a login form integrated into your main page. This way, the user can authenticate if he wishes or can remain anonymous.

ASP.NET supports this type of cookie authentication as well. Figure 11-3 shows an example of a client using an optional login form on a page. Once the client has been authenticated, the page displays additional information that is relevant to that client. The implementation is similar to the cookie authentication example discussed earlier, except that anonymous clients are not prevented from accessing the site. Instead, an integrated login form is shown on the main page. If a user logs in and authenticates successfully, she is granted an authentication cookie and can then potentially do additional things or view additional material on the site. Programmatically, our login form looks very similar to the one shown earlier, but instead of directly calling RedirectFromLoginPage, we explicitly grant the user an authentication cookie by calling SetAuthCookie and then redirect the user to the current page so that additional content may be displayed. Then, in any of the pages of our application, we can check the credentials of the current user and display elements of the page conditionally based on her login.

Figure 11-3. Forms-Based Authentication

11.2.5 Password Storage

One of the dangers of performing your own authentication, as is required by forms authentication, is that if a hacker manages to break into your database where the passwords are stored, she can then use the usernames and passwords freely, possibly at other Web sites if users established the same credentials elsewhere. To prevent this, you should avoid storing passwords in clear text altogether and instead prefer to store MD5 or SHA1 hashes of users' passwords. The FormsAuthentication class provides a method to perform MD5 and SHA1 hashes on arbitrary strings with the static method HashPasswordForStoringInConfigFile, as shown in Listing 11-15.

Listing 11-15 Hashing Passwords
<%@ Page language=C# %>
<html>
  <script runat=server>
  public void OnClick_Login(Object src, EventArgs e)
  {
    // Calculate hash of password to check against
    // database entry
    string passHash = FormsAuthentication.
         HashPasswordForStoringInConfigFile(_password.Text,
                                             "sha1");
    // use passHash in a database query here to look up
    // password hash instead of clear text password
    // then call FormsAuthentication.RedirectFromLoginPage
    // if username and password hash are correct
  }
  </script>
  <body>
    <form runat=server> <h2>Login Page</h2>
       Username:
       <asp:TextBox id="_username" runat=server/><br/>
       Password:
       <asp:TextBox id="_password"
                      TextMode="password" runat=server/>
       <br/>
       Remember password?:
       <asp:CheckBox id=_persistCookie runat="server"/>
       <br/>
       <asp:Button text="Login" OnClick="OnClick_Login"
                   runat=server/><br/>
    </form></body>
</html>

You might have noticed that the name of the function used to hash passwords is not just HashPassword but HashPasswordForStoringInConfigFile. This is not just because the Microsoft developer building this class wanted the title of "longest API function in .NET" but also because you can store user credentials directly in your web.config file. It is unlikely that you will want to take advantage of this feature in any reasonably sized application, because managing usernames and passwords is a task much better suited to a database. Listing 11-16 shows a sample web.config file that stores user credentials, and Listing 11-17 shows a sample page that uses the FormsAuthentication.Authenticate method to query those credentials.

Listing 11-16 Storing User Credentials in web.config
<configuration>
  <system.web>
    <authorization>
      <deny users="?"/>
    </authorization>

    <authentication mode="Forms">
      <forms loginUrl="login.aspx">
        <credentials passwordFormat="SHA1">
          <user name="Alice" password="9402F2262..."/>
          <user name="Bob"   password="EA9003E95..."/>
        </credentials>
      </forms>
    </authentication>
  </system.web>
</configuration>
Listing 11-17 Authenticating Users with web.config-Based Credentials
<%@ Page language=C# %>
<html>
  <script runat=server>
  public void OnClick_Login(Object src, EventArgs e)
  {
    if (FormsAuthentication.Authenticate(_username.Text,
                                         _password.Text))
      FormsAuthentication.RedirectFromLoginPage(
                _username.Text, _persistCookie.Checked);
    else
      _message.Text = "Invalid login: Please try again";
  }
  </script>
  <!� body and form not shown - see earlier examples �>
</html>

11.2.6 Salted Hashes

In the previous examples, we performed one-way hashes on passwords to prevent anyone from viewing them should they somehow fall into the wrong hands. Unfortunately, this may not be enough to protect the passwords, because someone who gains access to the hashed passwords could run a dictionary attack against them. That is, a dictionary of prehashed words could be compared with all the passwords, and if there were any matches, the attacker would know one or more passwords.

To counter this, many password storage facilities use what are called "salted" hashes to store passwords. To perform a salted hash on a string, you prefix the string with a randomly generated string of fixed length (the "salt") before performing the hash. This ensures that comparisons with hashed strings drawn from a password dictionary will never match. To verify a client's password, you must prefix the password he sends you with the salt string used when the original password was hashed. This means that you need to store both the hash and the salt in your password database.

Although there is no direct support for performing salted hashes in ASP.NET, it is relatively straightforward to do. Listing 11-18 shows a function, HashWithSalt, that performs a salted hash. It takes as parameters the hashing algorithm, the password as plain text, and the salt string by reference (for which you pass null if this is the first time you are hashing a password), and the hash is returned as an out parameter. The function then creates a 16-byte salt string if no salt is passed in, prepends it to the password, and passes the resulting string onto the FormsAuthentication class's HashPasswordForStoringInConfigFile.

Listing 11-18 HashWithSalt Routine
public static void HashWithSalt(string algName,
                                string plaintext,
                                ref string salt,
                                out string hash)
{
  const int SALT_BYTE_COUNT = 16;
  if (salt == null || salt == "")
  {
     byte[] saltBuf = new byte[SALT_BYTE_COUNT];
     RNGCryptoServiceProvider rng =
                     new RNGCryptoServiceProvider();
     rng.GetBytes(saltBuf);

     StringBuilder sb = new StringBuilder(saltBuf.Length);
     for (int i=0; i<saltBuf.Length; i++)
       sb.Append(string.Format("{0:X2}", saltBuf[i]));
     salt = sb.ToString();
  }

  hash = FormsAuthentication.
       HashPasswordForStoringInConfigFile(salt+plaintext,
                                           algName);
}

11.2.7 Role-Based Authentication

It is often useful to build Web applications in terms of roles. ASP.NET supports role-based authentication through the IsInRole() method of the IPrincipal interface. If Integrated Windows authentication is used, the IsInRole() method checks Windows group membership. If you are using cookie-based authentication and would like to use roles in your security checks, you must define those roles and create the mappings of users to roles. Fortunately, there is a convenient helper class called GenericPrincipal that implements IsInRole() for you when you give it a string of role names.

For an example of when roles might be useful in a Web application, consider the roles and code shown in Figure 11-4. This application defines five roles: Doctors, Nurses, Administrators, Patients, and Janitors. Each user that logs in to the application belongs to one or more roles, and within each page, you can specifically check to see which roles the current user belongs to, and grant access or display additional information based on role membership.

Figure 11-4. Role-Based Authentication

To implement role-based security checks, you must define the roles and the mapping of users to roles. You may have a registration form that assigns roles based on information that users check, or you may assign roles to users through some internal management page or some other technique. It is up to you how best to implement this. Once you have established the roles and user-to-role mapping, you must prepare a special IPrincipal implementation to be aware of these roles so that you can program against them in your pages.

The GenericPrincipal class is useful for this purpose. Its constructor takes an array of strings (role names) and a client Identity, and properly implements IsInRole of IPrincipal based on the array of strings. To use this class throughout the pages of your application, you need to prepare an instance of GenericPrincipal initialized with the appropriate array of role names for each request, and assign it to the User property of the current HttpContext.

The application-level event called AuthenticateRequest is your hook to perform these operations before any page requests information about the roles of the current client. Listing 11-19 shows an example global.asax file that defines a handler for the AuthenticateRequest event, prepares a GenericPrincipal class initialized with an array of role names based on the current user, and assigns the GenericPrincipal to the User property of the current HttpContext. After this event handler has set up the role-aware principal, your pages can successfully use the User.IsInRole() method to query whether a given user belongs to a particular role.

Listing 11-19 Role-Based Authentication Implementation (global.asax)
<%! File: global.asax %>
<%@ Import Namespace="System.Security" %>
<%@ Import Namespace="System.Security.Principal" %>
<script language=C# runat=server>
  void Application_AuthenticateRequest(Object src,
                                       EventArgs e)
  {
    if (Request.IsAuthenticated)
    {
      ArrayList roles = new ArrayList();
      if (Context.User.Identity.Name.ToLower() == "alice")
      {
        roles.Add("Doctors");
        roles.Add("Administrators");
      }
      else if (Context.User.Identity.Name.ToLower() ==
               "bob")
      {
          roles.Add("Doctors");
          //...
      }

      // Assign a GenericPrincipal class initialized with
      // our custom roles for this user
      String[] rgRoles =
               (String[]) roles.ToArray(typeof(String));
      Context.User =
       new GenericPrincipal(Context.User.Identity, rgRoles);
    }
  }
</script>

11.3 System Identity in ASP.NET

Throughout this chapter, we have discussed authenticating clients and how to work with the "identity" of clients. In all cases, the identity we were referring to was the managed identity of the client, recognized only by the .NET runtime. Another equally important identity to consider is the system identity, which affects the way code runs on a particular machine.

By default, the ASP.NET worker process runs under a special account created for ASP.NET called ASPNET. This account by default is granted only users rights, which means that it is quite restricted in what it can do on the machine. This is a good thing, because it prevents potential hackers who might gain access to your machine through this account from doing much damage. If you decide that you want to change the identity of the worker process, it is configurable through the machine.configprocessModel element. The options are to specify a username of machine (the default) or System (run as the LOCAL_SYSTEM account), or some hard-coded username. If you specify machine or system, you can leave the password attribute set to AutoGenerate, but if you specify a particular user, you must specify the password for that user in clear text. Listing 11-20 shows the processModel portion of a machine.config file with the default system identity setting of machine.

Listing 11-20 Using processModel to Control the Identity of aspnet_wp.exe
<!� File: machine.config �>
<configuration>
  <!� ... �>
  <system.web>
    <processModel enable="true" timeout="Infinite"
         idleTimeout="Infinite" shutdownTimeout="0:00:05"
         requestLimit="Infinite" requestQueueLimit="5000"
         restartQueueLimit="10" memoryLimit="60"
         webGarden="false" cpuMask="0xffffffff"
         userName="machine" password="AutoGenerate"
         logLevel="Errors" clientConnectedCheck="0:00:05"
         comAuthenticationLevel="Connect"
         comImpersonationLevel="Impersonate"
         responseRestartDeadlockInterval="00:09:00"
         responseDeadlockInterval="00:03:00"
         maxWorkerThreads="25" maxIoThreads="25"/>
    <!� ... �>
  </system.web>
</configuration>

If you try to do anything that requires system credentials, such as modifying files on the file system, or writing to the registry, you will find that the default privileges of the ASPNET account will stop you. The best approach to deal with this is to add the ASPNET account to the list of users allowed to perform whatever task you are trying to perform, rather than "punting" and changing the identity of the worker process altogether. For example, if you want to write data to an XML file on your system, modify the permissions of that XML file to include write permissions for the ASPNET account.

If you are using Windows authentication, you may want to take advantage of the fact that you can impersonate the client by using his Windows login credentials on the thread servicing the request within the worker process. To enable impersonation, add an identity element to your web.config file and set its impersonate attribute to true, as shown in Listing 11-21. This gives the thread used to service the request all the privileges associated with the client making the request.

Listing 11-21 Impersonating a Client
<!� File: web.config �>
<configuration>
  <system.web>
    <identity impersonate="true" />
  </system.web>
</configuration>

SUMMARY

The primary enhancement that ASP.NET makes to Web application security is its forms authentication infrastructure. Instead of relying on hand-rolled authorization and authentication techniques, ASP.NET provides a framework for building client authentication into applications without relying on operating system credentials. It takes care of granting an authentication cookie, redirecting unauthenticated users to a login page, and enforcing any authorization rules specified in your configuration files. What is left for you to do is to perform the actual authentication of clients, which typically is a simple query into a database. ASP.NET also defines a role-based authorization mechanism that is convenient for building applications in terms of roles of users instead of hard-coding checks for identities. Finally, the programmatic interface to the security settings in an application are generic enough that you can easily switch between using different authentication mechanisms, with very few changes to your application code. This means that you can build a system based on Windows authentication, and then if you decide to scale out your user base, you can switch to a forms-based authentication system relatively easily.

posted on 2006-01-25 11:01  深瞳  阅读(544)  评论(0编辑  收藏  举报