ASP.NET 3.5 Unleashed Errata: ASP.NET AJAX Authentication
Well, I guess it is too much to hope that there would not be any errors in an almost 2,000 page book. Bertrand Le Roy sent me an email pointing out a security hole in one of my code samples in ASP.NET 3.5 Unleashed. The problem is in Chapter 33, Using Client-Side ASP.NET AJAX.
The code sample demonstrates how to authenticate users from client-side code against the ASP.NET Membership system:
Listing 33.21 – ShowLogin.aspx
1: <%@ Page Language="C#" %>
2:
3: <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
4:
5: <script runat="server">
6:
7: [System.Web.Services.WebMethod]
8:
9: public static string GetSecretMessage()
10:
11: {
12:
13: return "Time is a fish";
14:
15: }
16:
17: </script>
18:
19: <html xmlns="http://www.w3.org/1999/xhtml">
20:
21: <head runat="server">
22:
23: <title>Show Login</title>
24:
25: <script type="text/javascript">
26:
27: function pageLoad()
28:
29: {
30:
31: $addHandler( $get("btnLogin"), "click", login);
32:
33: }
34:
35: function login()
36:
37: {
38:
39: Sys.Services.AuthenticationService.login
40:
41: (
42:
43: $get("txtUserName").value,
44:
45: $get("txtPassword").value,
46:
47: false,
48:
49: null,
50:
51: null,
52:
53: loginSuccess,
54:
55: loginFail
56:
57: );
58:
59: }
60:
61: function loginSuccess(isAuthenticated)
62:
63: {
64:
65: if (isAuthenticated)
66:
67: PageMethods.GetSecretMessage(getSecretMessageSuccess);
68:
69: else
70:
71: alert( "Log in failed" );
72:
73: }
74:
75: function loginFail()
76:
77: {
78:
79: alert( "Log in failed" );
80:
81: }
82:
83: function getSecretMessageSuccess(message)
84:
85: {
86:
87: $get("spanMessage").innerHTML = message;
88:
89: }
90:
91: </script>
92:
93: </head>
94:
95: <body>
96:
97: <form id="form1" runat="server">
98:
99: <asp:ScriptManager
100:
101: ID="ScriptManager1"
102:
103: EnablePageMethods="true"
104:
105: runat="server" />
106:
107: <fieldset>
108:
109: <legend>Login</legend>
110:
111: <label for="txtUserName">User Name:</label>
112:
113: <input id="txtUserName" />
114:
115: <br /><br />
116:
117: <label for="txtUserName">Password:</label>
118:
119: <input id="txtPassword" type="password" />
120:
121: <br /><br />
122:
123: <input id="btnLogin" type="button" value="Login" />
124:
125: </fieldset>
126:
127: The secret message is:
128:
129: <span id="spanMessage"></span>
130:
131: </form>
132:
133: </body>
134:
135: </html>
The page displays a form that enables you to enter your user name and password. If you enter a valid user name and password, then the secret message is displayed.
Here’s how the code above works. When you click the button to submit your user name and password, the login() method is called. If the user name and password are valid, the loginSuccess() method is called. This method calls a second web service method named GetSecretMessage() to retrieve the secret message.
Now, I correctly warn the reader that you should never put any secret information in your JavaScript code since anyone can select the menu option View Source in a browser to see all of your JavaScript code (or download the JavaScript files from the server). For this reason, I placed the secret message in server-side code within the GetSecretMessage() web method.
Here’s what I did not get right. Anyone can call the GetSecretMessage() web method at any time to grab the secret message. In fact, if you type the following code in your browser’s address bar after requesting the ShowLogin.aspx page, then you can bypass the validation step and view the secret message:
javascript:window.PageMethods.GetSecretMessage(getSecretMessageSuccess);
Drats! This was a stupid mistake on my part. Fortunately, there is an easy way to fix the code sample. The GetSecretMessage() method should be modified to perform a server-side authentication check. Here is the fixed version of the page:
Listing 33.21 (fixed)
1: <%@ Page Language="C#" %>
2:
3: <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
4:
5: <script runat="server">
6:
7: [System.Web.Services.WebMethod]
8:
9: public static string GetSecretMessage()
10:
11: {
12:
13: if (!HttpContext.Current.User.Identity.IsAuthenticated)
14:
15: throw new Exception("Not Authenticated!");
16:
17: return "Time is a fish";
18:
19: }
20:
21: </script>
22:
23: <html xmlns="http://www.w3.org/1999/xhtml">
24:
25: <head id="Head1" runat="server">
26:
27: <title>Show Login</title>
28:
29: <script type="text/javascript">
30:
31: function pageLoad()
32:
33: {
34:
35: $addHandler( $get("btnLogin"), "click", login);
36:
37: }
38:
39: function login()
40:
41: {
42:
43: Sys.Services.AuthenticationService.login
44:
45: (
46:
47: $get("txtUserName").value,
48:
49: $get("txtPassword").value,
50:
51: false,
52:
53: null,
54:
55: null,
56:
57: loginSuccess,
58:
59: loginFail
60:
61: );
62:
63: }
64:
65: function loginSuccess(isAuthenticated)
66:
67: {
68:
69: if (isAuthenticated)
70:
71: PageMethods.GetSecretMessage(getSecretMessageSuccess, getSecretMessageFail);
72:
73: else
74:
75: alert( "Log in failed" );
76:
77: }
78:
79: function loginFail()
80:
81: {
82:
83: alert( "Log in failed" );
84:
85: }
86:
87: function getSecretMessageSuccess(message)
88:
89: {
90:
91: $get("spanMessage").innerHTML = message;
92:
93: }
94:
95: function getSecretMessageFail(err)
96:
97: {
98:
99: alert( "Could not retrieve secret message: " + err.get_message() );
100:
101: }
102:
103: </script>
104:
105: </head>
106:
107: <body>
108:
109: <form id="form1" runat="server">
110:
111: <asp:ScriptManager
112:
113: ID="ScriptManager1"
114:
115: EnablePageMethods="true"
116:
117: runat="server" />
118:
119: <fieldset>
120:
121: <legend>Login</legend>
122:
123: <label for="txtUserName">User Name:</label>
124:
125: <input id="txtUserName" />
126:
127: <br /><br />
128:
129: <label for="txtUserName">Password:</label>
130:
131: <input id="txtPassword" type="password" />
132:
133: <br /><br />
134:
135: <input id="btnLogin" type="button" value="Login" />
136:
137: </fieldset>
138:
139: The secret message is:
140:
141: <span id="spanMessage"></span>
142:
143: </form>
144:
145: </body>
146:
147: </html>
In the code above, a server-side authentication check is performed in the GetSecretMessage() web method. Since this authentication check happens on the server, there is no way to bypass it from the client.
I’ve discussed this broken code sample with my editor and he says that it can be fixed in the next printing of the book.