Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

From Custom Authentication to ASP.NET Forms Authentication

0.00/5 (No votes)
5 Nov 2002 1  
Illustrates replacing custom authentication with ASP.NET Forms Authentication.

Introduction

One of the first projects I tackled with .NET, after doing the customary "Hello World" example, was converting a commercial ASP application into ASP.NET. 

The application tasks were to process, store and acknowledge (via email) customers' answers to a competition question and to provide a secure area for company officials to view customer entries and send out bulk mail.

Apart from learning how to implement each step in ASP.NET, I also restructured the application to make it more object-oriented. For the secure area of the site I initially more-or-less faithfully reproduced the original functionality. Then I discovered and investigated ASP.NET's built-in Forms Authentication.

What is authentication?

Authentication is the process of obtaining identification credentials, such as name and password, from a user and validating those credentials against some authority. If the credentials are valid, the entity that submitted the credentials is considered an authenticated identity. Once an identity has been authenticated, the authorization process determines whether that identity has access to a given resource. 

ASP.NET provides two other methods of authentication that are platform-specific with respect to the client, whereas Forms Authentication isn't. A couple of other articles on this site provide more in-depth insight into Forms Authentication. Here, I just provide the basics and discuss the issues I needed to address in my authentication process.

The Problem

A company official (also referred to as an administrator) wants to view the list of names and email addresses of the people who have entered the competition and the answers they've provided. The official may then perform other tasks, such as running queries or sending bulk mail.

The security requirements are:

  1. Access to the pages in the secure area requires the official to log in with a valid user name and password.
  2. Any attempt to navigate to a page in the secure area should redirect a user to the Login page.
  3. It should not be possible to  view any page when the browser is in offline mode, thereby bypassing security.
  4. There should be a limit on the number of login attempts within any browser session. 

Now, this isn't an e-commerce application. No credit card details are being processed. It's not necessary to have rock-solid security. Nevertheless it's worth exploring how security can be breached. 

There is no direct navigation from the customer pages to the secure area but suppose somehow a customer or other user discovers the URL to one of the pages in the secure area. Then our security mechanism will force them to login. It will throw them out after a specified number of invalid attempts (say 3). Though they can shut down the browser and try again, but they don't know that. Hopefully they'll be discouraged. But if not, they'll still have a hard time discovering the correct user name and password. An administrator will be aware that they can restart the browser though. So if they forget their login details they can try again to their heart's content. 

A more serious breach would be a malicious user's hacking the web site, downloading the database and extracting the login details. For this application we are just using a simple Microsoft Access database. The database is password-protected so it can't be opened in Access. But you can open the database in a text editor and perhaps have a poke around (it's mostly gibberish but it does contain the odd English word fragment). We could encrypt the database but we haven't. 

The last possibility (I think) is a network sniffer's intercepting and extracting the user name and password as they are transported across the network. I have not catered for this. But it can be addressed by using Secure Sockets Layer (SSL) to encrypt the user name and password as they are passed over the network. If there is a security breach then a hacker would have access to the names and email addresses of our customers and could send them junk mail. That's it. In the initial design, at least, company officials cannot directly update the database via the web. All operations are read-only. So these restrictions would apply to a hacker too.

Initial Solution

We roll our own authentication functionality. First, define some Session objects in Global.asax.

protected void Session_Start(Object sender, EventArgs e)
{
  // Administrator will only be allowed a certain number of login attempts

  Session["MaxLoginAttempts"] = 3;
  Session["LoginCount"] = 0;
  
  // Track whether they're logged in or not

  Session["LoggedIn"] = "No";
}

The login code looks like this.

// Note: here we are just faithfully reproducing the original ASP behaviour.

// Otherwise we would use ASP.NET authentication.


// Check number of login attempts not exceeded. If it is redirect to failed 

// login page.

int maxLoginAttempts = (int)Session["MaxLoginAttempts"];

if (Session["LoginCount"].Equals(maxLoginAttempts))
{
  Response.Redirect("LoginFail.aspx?reason=maxloginattempts");
}

// Attempt login

if (Request.Form["txtUserName"].Trim() == AdministratorLogin.UserName &&
  Request.Form["txtPassword"].Trim() == AdministratorLogin.Password)
{
  // Success, so we can access customer details.

  Session["LoggedIn"] = "Yes";
  Response.Redirect("CustomerDetails.aspx");
}
else // Fail to login

{
  // Report failure

  string invalidLogin = "Invalid Login.";
  lblMessage.Text = invalidLogin;
  
  // Track the number of login attempts

  int loginCount = (int)Session["LoginCount"];
  loginCount += 1;
  Session["LoginCount"] = loginCount;
}

When the login page is loaded it first checks to see whether the maximum number of login attempts has been exceeded. If it has the user is redirected to the "failed login" page. 

If the user has not exceeded the maximum number of login attempts the user name and password are validated against those returned by the AdministratorLogin object. Here I have just provided a couple of read-only properties which retrieve the user name and password from a persistent store (in this case, a database). If all is OK the user can access the customer details page. If not, an invalid login message is displayed to the user and they can try again up until the allowable number of attempts. 

Once the allowable number of login attempts has been exceeded the user will be unable to attempt a login again without being redirected to the "failed login" page. 

If the user tries to access any other page in the secure area they are automatically directed to the login page. This is because the Page_Load event of each page calls a custom authentication function that looks like this.

/// <summary>

/// Authenticates user for access to administration pages.

/// Ensures that page can't be navigated to

/// without user's being online and logged in.

/// </summary>

protected void AuthenticateUser()
{
  // Prevent caching, so can't be viewed offline

  Response.Cache.SetCacheability(HttpCacheability.NoCache);

  // Can't navigate to the page unless already logged in.

  // If not already logged in go to login page.

  if (Session["LoggedIn"].Equals("No"))
  {
    Response.Redirect("Login.aspx");
  }
}

Without the first line users can navigate to a secure page when the browser is offline, if the page is in the history list, which is not what we want!

Forms Authentication Solution

The principal effect of using ASP.NET's Forms Authentication mechanism is that we no longer need to track the login state. The AuthenticateUser function above disappears. Nor do we have to write our own code to retrieve the user name and password from the database. But in order to use the mechanism we must add some sections to the web.config file in the application root directory. In the authentication section we replace the default settings with the following: 

<!-- Can also use encryption for passwords. -->
<authentication mode="Forms">
  <forms name="FwLoginCookie" loginUrl="Login.aspx">
    <credentials passwordFormat="Clear">
      <user name="4th Wall" password="abc" />
      <user name="Kevin" password="maestro" />
    </credentials>
  </forms>
</authentication>

<!-- Deny access to anonymous (unauthenticated) users. -->
<authorization>
  <deny users="?" />
</authorization>

Then, after the closing system.web tag:

<!-- Allow all users access to LoginFail.aspx page. -->
<location path="LoginFail.aspx">
  <system.web>
    <authorization>
      <allow users="*" />
    </authorization>
  </system.web>
</location>

The effect of these settings is that all pages in the directory are protected from access except through the login mechanism. Any files in sub-directories are also protected unless they contain their own web.config files with different settings. 

In the authentication section, "FwLoginCookie" is the name of the cookie created by the authentication mechanism. Sometimes we may not want to use cookies. But for the present purposes these pages are for access only by company officials. They won't mind having cookies from themselves so to speak! 

"Login.aspx" is the page to be redirected to if a user accesses any other page in the directory. The credentials section contains a list of valid user names and passwords in clear format. An alternative is to encrypt them. (There is a framework function that can do this.) Instead of putting the user name and password in the web.config file they could be placed in an external XML Users file (or a database). This is the solution we would go for if we wanted to add new users to the system.

The authorization section's settings deny anonymous (i.e., unauthenticated) users access to our pages. 

The location section allows us to override the authentication and authorization checks for the LoginFail.aspx page. We need to do this so that an unauthenticated user can be redirected here when their login fails (i.e., after exceeding the allowable number of login attempts). An alternative is to put the LoginFail.aspx page in another directory or in a sub-directory with its own web.config file.

The revised code looks like this. The Session["LoggedIn"] object is no longer required:

protected void Session_Start(Object sender, EventArgs e)
{
  // Administrator will only be allowed a certain number of login attempts

  Session["MaxLoginAttempts"] = 3;
  Session["LoginCount"] = 0;
}

The Login code now just uses ASP.NET's Forms Authentication methods instead of the custom user name and password checking functionality implemented in the initial solution:

// Check number of login attempts not exceeded. If it is redirect to 

// failed login page.

int maxLoginAttempts = (int)Session["MaxLoginAttempts"];

if (Session["LoginCount"].Equals(maxLoginAttempts))
{
  Response.Redirect("LoginFail.aspx?reason=maxloginattempts");
}

// Attempt login

if (FormsAuthentication.Authenticate(txtUserName.Text.Trim(), 
                                     txtPassword.Text.Trim()))
{
  // Success, create non-persistent authentication cookie.

  FormsAuthentication.SetAuthCookie(txtUserName.Text, false);
  
  // Navigate to Customer Details

  Response.Redirect("CustomerDetails.aspx");
}
else // Fail to login

{
  // Report failure

  string invalidLogin = "Invalid Login.";
  lblMessage.Text = invalidLogin;
  
  // Track the number of login attempts

  int loginCount = (int)Session["LoginCount"];
  loginCount += 1;
  Session["LoginCount"] = loginCount;
}

In the Page_Load event in each protected page we still need to prevent offline viewing.

// Prevent caching, so can't be viewed offline

Response.Cache.SetCacheability(HttpCacheability.NoCache);

That's it. Again, to make it solid, we should also apply SSL to prevent user name and password interception.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here