ASP.NET provides a rich set of application services that are of fundamental value when building web applications. Forms authentication makes it possible to easily authenticate users against virtually any credentials storage mechanism. There are built-in role management facilities for creating, deleting, and disabling user accounts. ASP.NET also includes the Profile feature for storing and retrieving personalization information about users. This can be used for both authenticated and anonymous users.
Before ASP.NET AJAX, these services were available only on the server. You could write code in your .NET Framework language of choice to authenticate users, manage roles, or work with the Profile personalization data. You could also make use of a set of controls for presenting the user with an interface for logging in, displaying their status, creating new users, or retrieving forgotten passwords. This could even be done without writing any code by using the default services provided by ASP.NET.
ASP.NET AJAX builds on the application services to expand the way they can be leveraged from JavaScript code running in the browser. Script classes provided as part of the Microsoft AJAX Library make it easy to authenticate users and store and retrieve Profile information. In this chap-ter, you will see how to configure the ASP.NET Application Services for use and examine the new JavaScript classes you use to access them from the client.
ASP.NET supports several schemes for authenticating users. It works with the IIS web server to coordinate the types of authentication used. In the default scheme, the authentication mode is set to Windows, which means that a WindowsPrincipal object is assigned to every request and used to check against the file and operating system restrictions in effect for that user. The WindowsPrincipal object is a type in the .NET Framework that represents a machine or domain authenticated user. Basic information identifying the user’s name and access rights is available from the WindowsPrincipal object.
<configuration>
<system.web>
<authentication mode="Windows" />
</system.web>
</configuration>
The WindowsPrincipal assigned to the request can either be a low-privileged account created for ASP.NET or the identity of the logged-in user accessing the site. IIS can be configured to require the user be authenticated, and it supports impersonation so that the user’s identity is then passed on to ASP.NET.
The authentication mode can also be set to None to skip the attachment of the WindowsPrincipal object to the request. This does not mean that IIS authentication of the user is skipped but that ASP.NET is not requiring the authentication and won’t carry the delegated credentials. In version 2.0 of ASP.NET, the authentication schemes work together to provide rich infrastructure that carries across the static and dynamic content. ASP.NET also supports using the Passport Service (also known as the Windows Live ID service) to make use of a centralized and shared login.
To authenticate users without requiring Passport accounts or Windows accounts, ASP.NET also has an authentication mode called Forms. This is the most popular authentication type for Internet-facing sites, as opposed to Intranet-facing sites that commonly use Windows authentication with Active Directory user accounts. As Internet users cannot be expected to have Windows domain user accounts, it becomes necessary to prompt users for their credentials and then validate them against a database or some other mechanism. Forms authentication gets its name from the fact that the user typically provides his or her username and password in an HTML form submitted to the server where the authentication occurs. Forms authentication allows a site to have thousands of users without ever requiring them to have Windows accounts within the host domain. There is a tradeoff, though: You can’t use Windows access control lists to restrict file and resource access, so you must be careful about what resources you access when processing a web request on behalf of a user.
First, Forms authentication must be enabled for the application. Otherwise, the content is not restricted and the request is not checked for the Forms authentication ticket issued to sessions when the user authenticates. To enable Forms authentication, set the mode attribute of the authentication element to Forms. This isn’t the only default value that needs to be changed. ASP.NET also allows anonymous users to access the site, so in order to force users to authenticate, you must also add authorization information to the web.config file to disallow anonymous users. The web.config file shown in the following code denies an anonymous user access while allowing all authenticated users access. This is done using special wildcard characters. The anonymous user is represented by the question mark, and all authenticated users are associated with the asterisk. Be careful not to confuse these symbols! The deny element excludes anonymous users indicated with the question mark, blocking all those that have not been through authentication. And the asterisk specified in the allow element indicates that all users who have been authenticated should be allowed. It’s a good practice to place the deny elements before the allow elements, since ASP.NET will stop examining these clauses when it finds a match on any one of them. You can also specify roles in the authorization elements to further control who is allowed where in a site. The roles are then checked against the ASP.NET Roles application service as shown in the follow-ing from a web.config file:
<?xml version="1.0"?>
<configuration>
<appSettings/>
<connectionStrings/>
<system.web>
<authentication mode="Forms">
<forms cookieless="UseCookies" />
</authentication>
<authorization>
<deny users="?"/>
<allow users="*"/>
</authorization>
</system.web>
</configuration>
With this configuration, ASP.NET will now look at every request to the application, and if they have not authenticated, they will be redirected to the loginURL specified in the forms element of configuration. In this example, no loginURL is given, so the default login.aspx page will be used. Figure 7-1 shows the redirect that happens as a result of an anonymous user requesting a page called SecuredPage.aspx from the application.
Notice that the error indicates that the requested URL was login.aspx. The URL in the address bar of the browser shows that the querystring contains a ReturnURL key-value pair that shows the original path requested. If the site had a page named login.aspx, the unauthenticated user would have been redirected there. Once the user provides credentials and is validated, ASP.NET will return him to the page they were originally targeting.
When authenticating users, you can leverage the built-in membership provider. Listing 7-1 is an excerpt from a custom membership provider (WroxMembershipProvider.cs). It shows that you can override the abstract MembershipProvider class to provide a custom implementation. This allows you to decide what action must be taken to validate a user.
namespace Wrox.Samples {
using System;
using System.Web;
using System.Configuration.Provider;
using System.Web.Security;
using System.Collections;
using System.Collections.Specialized;
public class WroxMembershipProvider : MembershipProvider {
public override void Initialize(string name, NameValueCollection config) {
base.Initialize(name, config);
}
public override bool ValidateUser(string username, string password) {
return ((username == "WroxUser") && (password == "passw0rd"));
}
public override bool UnlockUser(string userName) {
throw new System.NotImplementedException();
}
}
}
The code in Listing 7-1 does not override all of the members of the abstract members as you would need to in providing a custom membership provider. In reality, you would want to use some better security practices than hardcoding usernames and passwords, as we did in the ValidateUser method earlier. It’s a best practice to apply a one-way hash to the user’s password and store that in the database instead. You can’t decrypt the password and send it to the user, but you minimize risk of someone discovering the password, since you’re not going to store it directly. To use the custom provider, you add the provider and also specify that you want your provider to be used by default in the web.config file.
<membership defaultProvider="SampleProvider"> <providers> <add name="SampleProvider" type="Wrox.Samples.WroxMembershipProvider"/> </providers> </membership>
Before using the Microsoft AJAX Library to authenticate users from JavaScript code running in the browser, you have to enable it. By default, the JavaScript services are not enabled. These services are off by default to keep all unneeded services disabled, which is a best practice from a security standpoint. You have to opt in to use these services.
<system.web.extensions> <scripting> <webServices> <authenticationService enabled="true" requireSSL="false"/> </webServices> </scripting> </system.web.extensions>
In addition to enabling the Authentication service, you can specify whether or not to allow requests to be made without SSL encryption. Similarly, other ASP.NET services are not enabled for script access by default. Once enabled, the Sys.Services.AuthenticationService namespace will be available for use in the browser by all ASP.NET AJAX pages that use a ScriptManager component. JavaScript code can then call back to the ASP.NET authentication service without requiring a full postback or requiring a user to be redirected to a separate login page.
Listing 7-2 (from Membership.aspx) is a page with JavaScript code that uses the Authentication service to perform an out-of-band call to the login method to check the user’s credentials using the custom membership provider. The pageLoad function wires up an event handler for the Login button. The han-dler code then retrieves the username and password from textboxes on the page. It also declares variables for optional parameters. The isPersistent parameter is used to declare whether or not the authentication ticket issued should be persisted by the browser after it closes for use by a later session. If not set to true, the ticket is not kept beyond the current browser session. When the browser closes, the ticket is deleted.
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
<title>Membership Page</title>
</head>
<body>
<form id="form1" runat="server">
<div>
<asp:scriptmanager runat="server" id="scriptmanager" />
Username <input type="text" id="username" /><br />
Password <input type="password" id="password" /><br />
<input type="button" value="Login" id="login" /><br />
</div>
</form>
</body>
<script type="text/javascript">
function pageLoad() {
$addHandler($get('login'),
}
function loginHandler() {
var username = $get('username')
var password = $get('password')
var isPersistent = false;
var customInfo = null;
var redirectUrl = null;
Sys.Services.AuthenticationService.login(
username,
password,
isPersistent,
customInfo,
redirectUrl,
loginCompleted,
loginFailed);
}
function loginCompleted(result, context, methodName) {
if(result) {
alert("Successfuly logged in.");
}
else {
alert("Failed to login.");
}
}
function loginFailed(error, context, methodName) {
alert(error.get_message());
}
</script>
</html>
The customInfo and redirectUrl are also optional. Whatever is passed in the customInfo argument will be relayed to the callback functions after the authentication is complete. The redirectUrl, when provided, indicates the page that the user should be sent to when the authentication check is complete. The other two arguments to the login method are the completed and failed callback functions. You pass the name of functions that should be called when the login call completes or fails. It’s important to note that the completed callback is called if the login completes, even if the result is a failed login attempt. The failed callback is called only if the login fails, however.
As with other ASP.Net services, you can define default handlers instead of specifying them with each call. Once you have created JavaScript functions you want to set as the defaults, you can make them the default for the page. The default loginCompletedCallback, logoutCompletedCallback, and failedCallback can all be set in the JavaScript pageLoad function of your page:
var service = Sys.Services.AuthenticationService; service.set_defaultLoginCompletedCallback( loginComplete ); service.set_defaultLogoutCompletedCallback( logoutComplete ); service.set_defaultFailedCallback( authFailed );
You would still need to set the defaults on each page where the authentication service was going to be used, but you could standardize on a set of names as shown here and have those functions defined in a script included in every page.
Of course, performing a validation check to log in a user is only part of what is needed in a Membership service. You also need to be able to check the current status of a user. If the user is not logged in, you need to present the option to log in, and you should not try to get any other data that would apply only to an authenticated user. The get_isLoggedIn function calls the authentication service to get the user’s current status. In Listing 7-3 (Status.aspx) the user’s current status is shown as a result of clicking the button on the page.
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
<title>Membership Status Page</title>
</head>
<body>
<form id="form1" runat="server">
<div>
<asp:scriptmanager runat="server" id="scriptmanager" />
<input type="button" value="Show Status" id="status" /><br />
</div>
</form>
</body>
<script type="text/javascript">
function pageLoad() {
$addHandler($get('status'),
}
function statusHandler() {
var result = Sys.Services.AuthenticationService.get_isLoggedIn();
if(result === true) {
alert('You are logged in.');
}
else {
alert('You are NOT logged in.');
}
}
</script>
</html>
When a user chooses to log out, you may want to redirect her away from the page she is currently viewing. The logout function provides a redirectUrl parameter for this purpose. If set, the browser will be navigated to the redirectUrl when the logout call completes. Be aware that a page unload handler that might be used by the JavaScript code for a particular page that could interfere with this logout navigation. The unload code would need to be written knowing that logout could be called, and it must allow the redirect to happen. This unload issue is especially important if you have other developers working on the same application. Listing 7-4 (Logout.aspx) adds a button to the Status.aspx page for logging out.
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" > <head runat="server"> <title>Logout Page</title> </head> <body> <form id="form1" runat="server"> <div> <asp:scriptmanager runat="server" id="scriptmanager" /> <input type="button" value="Show Status" id="status" /><br /> <input type="button" value="Logout" id="logout" /><br /> </div> </form> </body> <script type="text/javascript"> function pageLoad() { $addHandler($get('status'), 'click ', statusHandler); $addHandler($get('logout'), 'click ', logoutHandler); } function statusHandler() { var result = Sys.Services.AuthenticationService.get_isLoggedIn(); if(result === true) { alert('You are logged in.'); } else { alert('You are NOT logged in.'); } } function logoutHandler() { var redirectUrl = 'Logout.aspx'; var userContext = null; Sys.Services.AuthenticationService.logout(redirectUrl, logoutCompletedCallback, logoutFailedCallback, userContext); } function logoutCompletedCallback(result, context, methodName) { alert('Logged out.') } function logoutFailedCallback(error, context, methodName) { alert(error.get_message()); } </script> </html>
The completion callbacks all have the same set of parameters. The first parameter is the result. This is the return value of the service method being called. If the method returns a Boolean, the result will be that Boolean it returned. If it returns void, as the Logout function does, the result argument will be null.
Now you can avoid sending users through a series of redirects to log in and log out. The ASP.NET AJAX Extensions make it easy to authenticate users, check their status, and log them out from any page!