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:
- Access to the pages in the secure area requires the official to log in
with a valid user name and password.
- Any attempt to navigate to a page in the secure area should redirect a
user to the Login page.
- It should not be possible to view any page when the browser is in
offline mode, thereby bypassing security.
- 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)
{
Session["MaxLoginAttempts"] = 3;
Session["LoginCount"] = 0;
Session["LoggedIn"] = "No";
}
The login code looks like this.
int maxLoginAttempts = (int)Session["MaxLoginAttempts"];
if (Session["LoginCount"].Equals(maxLoginAttempts))
{
Response.Redirect("LoginFail.aspx?reason=maxloginattempts");
}
if (Request.Form["txtUserName"].Trim() == AdministratorLogin.UserName &&
Request.Form["txtPassword"].Trim() == AdministratorLogin.Password)
{
Session["LoggedIn"] = "Yes";
Response.Redirect("CustomerDetails.aspx");
}
else
{
string invalidLogin = "Invalid Login.";
lblMessage.Text = invalidLogin;
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.
protected void AuthenticateUser()
{
Response.Cache.SetCacheability(HttpCacheability.NoCache);
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:
<!---->
<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>
<!---->
<authorization>
<deny users="?" />
</authorization>
Then, after the closing system.web tag:
<!---->
<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)
{
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:
int maxLoginAttempts = (int)Session["MaxLoginAttempts"];
if (Session["LoginCount"].Equals(maxLoginAttempts))
{
Response.Redirect("LoginFail.aspx?reason=maxloginattempts");
}
if (FormsAuthentication.Authenticate(txtUserName.Text.Trim(),
txtPassword.Text.Trim()))
{
FormsAuthentication.SetAuthCookie(txtUserName.Text, false);
Response.Redirect("CustomerDetails.aspx");
}
else
{
string invalidLogin = "Invalid Login.";
lblMessage.Text = invalidLogin;
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.
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.