Professional ASP.NET Security - Jeff Ferguson
.pdf
Extending Forms Authentication
We first check that the request has been authenticated - we don't want to add any roles for anonymous users:
if(Request.IsAuthenticated) We then extract
the roles from the authentication ticket:
string!] roles =
((Forms Identity(Context.User.Identity) .Ticket.UserData.Split(';');
Next, we create a new GenericPrincipal object. We want to use the existing identity and add our roles, so we specify these in the constructor:
GenericPrincipal newPrincipal =
new GenericPrincipal(Context.User.Identity, roles);
Finally, we attach the new principal object to the HTTP context:
Context.User = newPrincipal;
Let's take a quick recap of what we have done. We added a method to our XML credentials store class to extract roles for a user. We then added code to our login method to get the roles for the user and persist them in an authentication ticket cookie. Finally, we added an event handler that will execute with each page request, to populate the Context .User with the roles from the authentication ticket.
To find out how to make use of this roles information in authorization, see Chapter 12. For now, we can run a simple test to see whether the roles have been successfully applied by using the IsInRole method:
if(Request.IsAuthenticated)
Response. Write (User. Identity. Name) ; if(User.IsInRole("goodFolk"))
Response.Write(" is a member of goodFolk"); else Response.Write(" is not a member of goodFolk"); }
When we successfully log in 'dan', we will receive confirmation that he is a member of the 'goodFolk1 role:
Protecting Against Cookie Theft
As we mentioned in the last chapter, a user who steals the authentication cookie from another user can pose as that user. The authentication cookie carries the encrypted authentication ticket that identifies the user. This is a weakness in the forms authentication system - a weakness that it would be nice to provide extra precautions against.
One way to prevent cookie theft is to monitor the IP addresses of clients that access our web application. If we store the IP address that a new ticket is issued to and ensure that only that IP address is allowed to use that ticket subsequently, users at different IP addresses will not be able to steal the cookie.
We have already seen that we can add information to the encrypted authentication ticket so it should be an easy matter to store the IP address of the client when the authentication ticket is issued and check it with each subsequent request.
Here is the now very familiar login method, with code to store the client's IP address in the ticket:
private void LoginButton_Click(object |
sender, |
System. EventArgs |
|||||||
e) { |
|
|
|
|
|
|
|
|
|
//check |
the |
credentials |
|
|
|
|
|
||
if (FormsAuthenticat ion. Authenticate |
|
|
|
|
|||||
(UsernameTextBox.Text, |
PasswordTextBox. Value) ) { |
|
|||||||
//create |
a |
new authentication ticket |
with |
the users |
IP in the |
||||
userData FormsAuthenticationTicket |
ticket = |
new |
|
||||||
FormsAuthenticationTicket ( 1, |
|
|
|
|
|||||
FormsAuthentication.FormsCookieName, |
|
|
|
||||||
DateTime.Now, DateTime.Now.AddHours |
|
|
|
||||||
(3),false, Request. |
|
|
|
|
|
|
|||
UserHostAddress .ToString ( ) ) ; |
|
|
|
|
|||||
//encrypt |
the |
ticket |
|
|
|
|
|
||
string |
encryptedTicket |
= |
FormsAuthentication. Encrypt (ticket) ; |
||||||
//create |
a |
cookie |
|
|
|
|
|
|
|
HttpCookie |
authenticationCookie |
= |
|
|
|
||||
new HttpCookie (FormsAuthenticat ion. FormsCookieName, encryptedTicket) ; |
|||||||||
//write the cookie to the response |
|
|
|
||||||
Response. Cookies .Add (authenticationCookie) ; |
|
|
|||||||
//redirect |
the |
user |
back |
to |
their |
original |
|
||
URL Response . Redirect
(FormsAuthentication. GetRedirectUrl (UsernameTextBox.Text, false) ) ;
We now need to add code to check the IP address in the authentication ticket against the IP address associated with each request.
230
Extending Forms Authentication
We can put the following event handler in the code behind for global . asax to do this:
protected void Application_AuthenticateRequest (Object sender, |
EventArgs |
e) { |
|
if (Request . IsAuthenticated) |
|
{ |
|
//check for mismatch in ticket and request |
|
if ( ( (Formsldentity) User .Identity) .Ticket .UserData != |
|
Request .UserHostAddress .ToString ( ) ) { |
|
Genericldentity i = new Genericldentity ( " " , " " ) ; |
|
string [] roles = { } ; |
|
Context .User= new GenericPrincipal (i, roles) ; |
|
We only want to compare the IP addresses for requests that are not anonymous as anonymous requests will have no authentication ticket, so we check Request . IsAuthenticated first:
if (Request . IsAuthenticated)
If we are dealing with an authenticated request, check the IP address stored in the authentication ticket against the IP address that is making this request:
if ( ( (Forms Identity) User .Identity) .Ticket .UserData != Request .UserHostAddress. ToString ( ) )
If they do not match, we replace the principal object in User with a principal that has a new, anonymous identity - canceling the user's authentication:
Genericldentity i = new Genericldentity ("",""); string [] roles = {};
Context .User= new GenericPrincipal (i, roles) ;
We have a number of options about what to do when the IP address fails. In the example above, we simply canceled the user's authentication, reverting them to an anonymous state. We may also want to keep a record of such failed checks - they provide an indication that someone may be attempting to compromise our application. This record could involve writing to the Windows event log or simply to a text file. We may want to raise a security exception when the check is failed, ending the user's request and possibly taking further actions.
Limitations of Comparing IP Addresses
This technique of comparing the IP address to which the authentication ticket was issued and the IP that makes each request does have some weaknesses.
The first problem is that clients that share a web proxy will appear to have the same IP address. This means that users that share a proxy can steal each other's cookies and avoid our protection.
If a user's IP address changes, they will fall foul of our protection. A user's IP address could change if their Internet Service Provider uses Dynamic IP address allocation and they disconnect and reconnect their connection. This is more likely to be a problem if we want to use persistent authentication cookies as the likelihood of the user's IP address changing before their authentication cookie naturally expires is much greater.
Despite these problems, this method of protecting the authentication cookies is often a useful solution to the prime weakness of forms authentication.
Maintaining a List of Logged On Users
Sometimes we want to keep a list of which users are currently active on our application. This may be for administrative purposes, for display to other users, or both. It is useful for administrators to know which users are actively using the application at a particular time. For example, they may need to inform them of downtime. A displayed list of logged in users can be a great feature for community web sites such as forum systems - allowing users to see at a glance which other users are active on the system.
Creating a user listing system requires us to do a few things:
Q Create a data structure to store the list of logged in users when the application starts Q Add each user to the user list when they log in
Q Remove each user from the user list when they log out
In our example, we will also store the number of anonymous users currently using our application. To do this, we will need to do some additional things:
Q Create a variable to hold the number of anonymous users when the application
starts G |
Add one to the value when a new user session begins Q |
Deduct one |
form the value when a user session ends
The final thing we will need is a means of displaying the list of users.
We create the places to store our user information in the Application_Start event handler in the code behind for global . asax:
protected void Application_Start (Object |
sender, |
EventArgs |
|
e) { |
|
|
|
Application! "CurrentUsers" ] = |
new Hashtable ( ) ; |
||
Application! "AnonymousUsers" ] |
= |
(Intl6)0; |
|
We have created a Hashtable and an integer in the Application collection. Both will be maintained for the lifetime of our application. We have chosen a Hashtable because it provides an efficient way to link keys with values. We will be using the Session IDs for keys and the usernames for values. Each Session ID is a unique integer so they are ideal for use as keys.
232
Extending Forms Authentication
In order to increment the anonymous users count, we use the Session_Start event:
protected void Session_Start (Object sender, |
EventArgs |
|
e) { |
|
|
Application [ " AnonymousUsers " ] |
= Convert .Tolntl6 |
|
(Application! "AnonymousUsers" ]) + |
1; |
|
We have to use Convert . Tolntl6 because Application [ "AnonymousUsers " ] is an Obj ect - we need it to be treated as an integer.
To deal with users logging in, we need to add some code to the login method of our login page:
private void LoginButton_Click(object sender, System. EventArgs e) {
//check the credentials
if (FormsAuthenticat ion. Authenticate (UsernameTextBox.Text, PasswordTextBox. Value) )
{
//Update the list of logged in users
( (Hashtable)Application[ "CurrentUsers" ] ) .Add Session. SessionID, UsernameTextBox.Text) ;
Application [ "AnonymousUsers" ] = Convert . ToIntlS (Application! "AnonymousUsers" ]) -1; //set up the authentication cookie and redirect
FormsAuthenticat ion. RedirectFromLoginPage (UsernameTextBox.Text, false) ;
}
else
{
//make the error message visible ErrorMessageLabel .Visible = true;
First we add an entry to our user list:
( (Hashtable) Application! "CurrentUsers" ] ) .Add
(Session. SessionID, UsernameTextBox.Text) ;
We need to explicitly cast Application [ "CurrentUsers" ] with (Hashtable) because it is otherwise treated as an Object - we need it to access the Add method of Hashtable. We use Session . SessionID to get the Session ID for the user.
We also need to decrement our anonymous users count since a user has now moved from being anonymous to being authenticated:
Application [ "AnonymousUsers " ] = |
|
Convert .ToIntlS (Application [ "AnonymousUsers " ] ) |
- 1; |
Our code to handle a user clicking the log out button is similar:
private void LogoutButton_Click(object sender, System.EventArgs e) { if(((Hashtable)Application!"CurrentUsers"]).Contains(Session.SessionID)) { ((Hashtable)Application!"CurrentUsers"]).Remove(Session.SessionID); Application!"AnonymousUsers"] =
Convert.Tolntl6(Application!"AnonymousUsers"]) + 1; } FormsAuthentication.SignOut(); Response.Redirect("default.aspx"); }
First we check that the user is in the user list (we don't want to act unless the user is actually in the list already):
if(((Hashtable)Application!"CurrentUsers"1).Contains(Session.SessionID))
If the user is in the list, we remove them:
((Hashtable)Application!"CurrentUsers"]).Remove(Session.SessionID);
We also increment the number of anonymous users by one:
Application!"AnonymousUsers"] =
Convert.Tolntl6(Application["AnonymousUsers"]) + 1;
The final thing we need to deal with is the end of a user session. This happens when the session times out through inactivity. This event is not as simple as just decrementing the number of anonymous users - the user may be logged in when their session times out. We have to make sure that we deal with these situations appropriately.
Here is the event handler for the Session_End event:
protected void Session_End(Object |
sender, |
EventArgs |
|
|
e) { |
|
|
|
|
if(((Hashtable)Application!"CurrentUsers"]).Contains(Session.SessionID)) |
|
|||
((Hashtable)Application!"CurrentUsers"]).Remove(Session.SessionID); |
|
|||
else |
|
|
|
|
Application!"AnonymousUsers"] |
= |
(Intl6)Application!"AnonymousUsers"] |
- |
|
1; } |
|
|
|
|
If the user list contains the user involved, we remove them from the list. If there is not an entry in the user list associated with the session that is ending, we decrement the number of anonymous users by one instead.
With these methods, we now have a system that will store the number of active anonymous users and the usernames of logged in users. All we need now is a means to display this information. We will create a user control to display the list.
Here is the code for our . ascx file:
234
Extending Forms Authentication
<%@ Control Language="c#" AutoEventWireup="false" Codebehind="UserList.txt.ascx.cs" Inherits="UserList.UserList" Targe tSchema="http: //schemas .microsoft. com/Intel 11 sense / ie5 "%>
<asp:DataList id="UsersDataList" runat="server" BorderColor="#999999" BorderStyle="Solid" ForeColor="Black" BackColor="White" CellPadding="3" GridLines="Vertical" BorderWidth="Ipx">
<SelectedItemStyle Font-Bold="True" ForeColor="White"
BackColor="#000099"></SelectedItemStyle>
<HeaderTemplate>
Currently Logged In Users </HeaderTemplate>
<FooterTemplate> Anonymous Users:
<%# Application["AnonymousUsers"]%> </FooterTemplate>
<ItemTemplate>
<%#DataBinder.Eval(Container, "Dataltem")%> </ItemTemplate>
<FooterStyle BackColor="#CCCCCC"></FooterStyle>
<HeaderStyle Font-Bold="True" ForeColor= "White" BackColor="Black"x/HeaderStyle> </asp:DataList>
As you can see, our control is based on a DataList control. The interesting parts are the footer template and the item template. In the footer, we display the value from
Application["AnonymousUsers"]:
<%# Application["AnonymousUsers"]%> The item
template is set up to display a databound value:
<%#DataBinder.Eval(Container, "Dataltem")%>
In our code behind for this control, we need to bind the list of users to the DataList. We will do this in the Page_Load event handler:
private void Page_Load(object sender, |
System.EventArgs e) |
if(((Hashtable)Application!"CurrentUsers"]).Count > 0) UsersDataList.DataSource =
(Hashtable)Application!"CurrentUsers"]).Values; else {
string!] noUsers = {"No users logged in"}; UsersDataList.DataSource = noUsers;
UsersDataList. DataBind () ,-}
We do not want to bind to the user list if there are no users logged in, so the first thing we do is to check that there is at least one user in our user list:
if(((Hashtable)Application!"CurrentUsers"])-Count > 0)
If there is a user in the user list, we set the DataSource of the DataList to the Values property of our user list. This property of Hashtable returns a collection containing the values from the Hashtable (in this case, the usernames):
UsersDataList.DataSource = (Hashtable)Application["CurrentUsers"]).Values;
If there are no users logged in, we want to display a message to that effect. We do that by creating a string array with a single element - our message. We set this as the DataSource for the DataList:
string[] noUsers = {"No users logged in"}; UsersDataList.DataSource = noUsers;
Finally, whichever DataSource we are using, we need to perform the binding:
UsersDataList.DataBind(); This code will give a user control that looks something like this
when there are no users logged in:
|
1 H default - Microsoft Internet Explorer provided by Freeserve |
| lnjx |
|
|
File Edft |
View Favorites Tools Help |
*^ |
|
|
|
,Lrts *j |
|
|
|
|
|
|
|
S |
|
(I Currently Logged In Us ers ^^^^^^^^^^| |
*> , |
|
|
|
||
|
|
|
|
|
; No users logged in |
|
|
|
j Anonymous Users: 1 |
'y |
|
|
°fll |
|
|
|
JMHS |
|
T*»& |
|
HQPP^e |
|
|
|
, |
:^_J local intranet |
•r |
|
|
|
|
and like this when there is a user logged in:
236
Extending Forms Authentication
Fite |
Edit |
View |
Favorites |
Toote |
Help |
v- |
Address |^J http://localhost/security/forms/userlist/defaLlt.a5p_^j
default - Microsoft Internet Explorer provided by Freeserv
Currently Logged In Users
dan
Anonymous Users: 0
Log Out I Log In |
local ir^rartet
Note that the Log Out and Log In buttons are not part of the control - they are on the page along with the user list control.
Providing Multiple Login Pages
It is quite common for us to want to display different login pages for different parts of our application so that users are redirected to different login pages depending on the location of the file they originally requested. ASP.NET does not provide this functionality by default - each application has a single configured login page that users are redirected to upon failed authorization. We could set up multiple web applications each with its own configured login page, but then we will have difficulties if we want to share data between the parts of the overall application. We really need to find a way to provide the multiple login pages within a single web application.
There are two main types of solution to this problem:
Q Using a single login page but customizing it so that it appears to be multiple pages Q Using the configured login page to redirect users to one of a selection of login pages
The first solution is really just a case of using the return path of the original request to customize the login page. This is suitable for situations where the differences between the pages for different folders are not great.
We will look in some more detail at the second solution. This is more suitable when we want each login page to be very different.
We will build a login redirection page to handle failed authorization. If a login page exists in the folder of the original request, we will redirect to that page. If a suitable login page does not exist, we will redirect to a default login page. It is this logion redirection page that is set as the login page for forms authentication in the web.config.
Here is the code for the Page_Load event of the login redirection page:
private void Page_Load( object sender, System. EventArgs e) {
//get the original URL
string url = FormsAuthentication.GetRedirectUrl ("", false) ;
//create a regular expression to match the filename part of a url Regex reg = new Regex( "/( ["/ ]*)$");
//use the regular expression to remove the filename and query string string path = reg. Replace (url, "");
//create the path to a matching login page
string file = Server .MapPath (path + " /login. aspx" );
string encodedUrl = Server .UrlEncode (url) ;
//does a login page exist?
if (System. 10. File. Exists (file) )
{
//redirect to the login page
Response. Redirect (path + " /login. aspx" + " ?ReturnUrl=" + encodedUrl);
}
else
{
//redirect to the dfeualt login page
Response. Redirect ( "defaultlogin. aspx" + " ?ReturnUrl=" + encodedUrl);
First we get the original URL of the request, with FormsAuthentication.GetRedirectURl.
We now need to extract the path from the URL (remove the filename and query string) so that we can check for a login page at that path. To do this, we create a regular expression that matches the filename and query string:
Regex reg = new Regex ("/([ ^ /]*)$");
The ' / ' matches the ' / ' at the end of the path, and the ' [ * / ] * ' matches any number of characters that are not ' / ' (this ensures that the regular expression does not match a substring that includes part of the path). The ' $ ' matches the end of the string. So, what we are saying is "match everything from the final backslash to the end of the string" - this will be the filename and the query string.
We now use the regular expression to remove the filename and query string by replacing them with an empty string:
string path = reg. Replace (url, " " ) ; Next, we use the path we have extracted to create a
physical path to a login page that might exist at that path:
string file = Server .MapPath (path + " /login. aspx" ) ;
238
