One of the most useful and perhaps most misunderstood authentication
schemes built in to the ASP.NET runtime is Forms Authentication. Useful,
because it is highly extensible and flexible (as we'll see in a moment).
Misunderstood, because most developers don't get past the default setup
described in the documentation and therefore never find out how to extend
and customize it.
The keys to a successful understanding and implementation of Forms -
based authentication are first - to become familiar with the FormsAuthentication
class, its members and properties, and second - to learn how to implement
it programmatically with a database containing usernames, passwords, and
roles - the exact same type of roles that we use for Windows Authentication.
We implement this with the use of the Application_AuthenticateRequest
method in Global.asax, like this:
protected
void Application_AuthenticateRequest(Object sender, EventArgs e)
{
if (HttpContext.Current.User != null)
{
if (HttpContext.Current.User.Identity.IsAuthenticated)
{
if (HttpContext.Current.User.Identity
is FormsIdentity)
{
//
Get Forms Identity From Current User
FormsIdentity
id = (FormsIdentity)HttpContext.Current.User.Identity;
//
Get Forms Ticket From Identity object
FormsAuthenticationTicket
ticket = id.Ticket;
//
Retrieve stored user-data (our roles from db)
string
userData = ticket.UserData;
string[]
roles = userData.Split(',');
//
Create a new Generic Principal Instance and assign to Current User
HttpContext.Current.User
= new GenericPrincipal(id, roles);
}
}
}
} |
As can be seen above, we check the current HttpContext
for its Current.User property and determine if the Identity
is Authenticated. If it is, we check to see if the identity is of type
FormsIdentity. We then assign this Identity to a local
member, "id", create a new FormsAuthenticationTicket
from the id, and grab the custom UserData from it, which
in this case is simply the comma - delimited string of roles that have
been assigned to this user in our database. Finally, we create a new GenericPrincipal
object and assign this to our Current.User HttpContext.
At this point, we have now bound a custom identity to our HttpContext
and it will travel around our site with this authenticaled user, wherever
they may go. To enforce role based security on any page -- or even for
specific controls on a page, all we need to do is query if (User.IsInRole("Administrator"))
in our Page_Load or other pertinent code area and we can control
access by using Response.Redirect back to the login page,
or run code that enables or suppresses controls on the page, or whatever
may be appropriate.
Before we review the code and web.config settings to look up each visitor
in our database and use the above code, lets take a quick pass through
a condensed version of the FormsAuthentication class:
FormsAuthentication Members
Public Constructors
FormsAuthentication Constructor |
Initializes a new instance of the FormsAuthentication
class. |
Public Properties
FormsCookieName |
Returns the configured cookie name
used for the current application. |
FormsCookiePath |
Returns the configured cookie path
used for the current application. |
Public Methods
Authenticate |
Attempts to validate the credentials
against those contained in the configured credential store, given
the supplied credentials. |
Decrypt |
Returns an instance of a FormsAuthenticationTicket
class, given an encrypted authentication ticket obtained from
an HTTP cookie. |
Encrypt |
Produces a string containing an encrypted
authentication ticket suitable for use in an HTTP cookie, given
a FormsAuthenticationTicket. |
Equals (inherited from Object) |
Overloaded. Determines whether two
Object instances are equal. |
GetAuthCookie |
Overloaded. Creates an authentication
cookie for a given user name. |
GetHashCode (inherited from Object) |
Serves as a hash function for a particular
type, suitable for use in hashing algorithms and data structures
like a hash table. |
GetRedirectUrl |
Returns the redirect URL for the original
request that caused the redirect to the logon page. |
GetType (inherited from Object) |
Gets the Type of the current instance. |
HashPasswordForStoringInConfigFile |
Given a password and a string identifying
the hash type, this routine produces a hash password suitable
for storing in a configuration file. |
Initialize |
Initializes FormsAuthentication
by reading the configuration and getting the cookie values and
encryption keys for the given application. |
RedirectFromLoginPage |
Overloaded. Redirects an authenticated
user back to the originally requested URL. |
RenewTicketIfOld |
Conditionally updates the sliding expiration
on a FormsAuthenticationTicket. |
SetAuthCookie |
Overloaded. Creates an authentication
ticket and attaches it to the cookie's collection of the outgoing
response. It does not perform a redirect. |
SignOut |
Removes the authentication ticket. |
ToString (inherited from Object) |
Returns a String that represents the
current Object. |
Protected Methods
Finalize (inherited from Object) |
Overridden. Allows an Object to attempt
to free resources and perform other cleanup operations before
the Object is reclaimed by garbage collection.
In C# and C++, finalizers are expressed using destructor syntax.
|
MemberwiseClone (inherited from Object) |
Creates a shallow copy of the current
Object. |
We will have an opportunity to use most of these members
in our example. Now, how do we handle the login situation? Let's take
a look at the web.config elements required for this setup:
<authentication mode="Forms">
<forms name="FormsAuthDB.AspxAuth"
loginUrl="default.aspx"
protection="All"
timeout ="10"
path="/"/>
</authentication>
<authorization>
<deny users="?" />
<allow users="*"/>
</authorization> |
In this example, I have set up a FormsAuthentication
block in web.config to enable Forms Authentication, provide a loginUrl
(where you get directed to automatically if you are not authenticated
when attempting to load a page), protection of "All" (recommended),
timeout of 10 minutes for the ticket (cookie), and we are denying
access to the anonymous user. The Path attribute controls the cookie
path in the site; here, it's for the entire site. Be advised however,
that you can place additional web.config files in subfolders on your
site that provide more granular control about "who can go where".
Now in our login page (in this case Default.aspx because
I want to catch everybody with a minimum of fuss), we'll need a couple
of textboxes to ask for a username and password and I guess a button
that says "LOGIN". I'll leave the UI up to you, let's look
at the codebehind:
private void Button1_Click(object
sender, System.EventArgs e)
{
// Initialize FormsAuthentication
(reads the configuration and gets
// the cookie values and encryption keys for the given
application)
FormsAuthentication.Initialize();
// Create connection and command
objects
SqlConnection conn =
new SqlConnection("Data Source=PETER;Database=Northwind;User
ID=sa;password=;");
conn.Open();
SqlCommand cmd = conn.CreateCommand();
cmd.CommandText = "SELECT roles FROM Employees
WHERE username=@username " +
"AND password=@password";
// this should really be a stored procedure, right?
// Fill our parameters
cmd.Parameters.Add("@username", SqlDbType.NVarChar,
64).Value = TextBox1.Text;
cmd.Parameters.Add("@password", SqlDbType.NVarChar,
64).Value = TextBox2.Text;
FormsAuthentication.HashPasswordForStoringInConfigFile(TextBox2.Text,"sha1");
// you can use the above method for encrypting
passwords to be stored in the database
// Execute the command
SqlDataReader reader = cmd.ExecuteReader();
if (reader.Read())
{
// Create a new ticket used
for authentication
FormsAuthenticationTicket ticket = new FormsAuthenticationTicket(
1, // Ticket version
TextBox1.Text, // Username to
be associated with this ticket
DateTime.Now, // Date/time issued
DateTime.Now.AddMinutes(30), //
Date/time to expire
true, // "true" for
a persistent user cookie (could be a checkbox on form)
reader.GetString(0), // User-data
(the roles from this user record in our database)
FormsAuthentication.FormsCookiePath); //
Path cookie is valid for
// Hash the cookie for transport
over the wire
string hash = FormsAuthentication.Encrypt(ticket);
HttpCookie cookie = new HttpCookie(
FormsAuthentication.FormsCookieName, //
Name of auth cookie (it's the name specified in web.config)
hash); //
Hashed ticket
// Add the cookie to the list
for outbound response
Response.Cookies.Add(cookie);
// Redirect to requested URL,
or homepage if no previous page requested
string returnUrl = Request.QueryString["ReturnUrl"];
if (returnUrl == null) returnUrl = "LoggedIn.aspx";
// Don't call the FormsAuthentication.RedirectFromLoginPage
since it could
// replace the authentication ticket we just added...
Response.Redirect(returnUrl);
}
else
{
// Username and or password
not found in our database...
ErrorLabel.Text = "Username / password incorrect.
Please login again.";
ErrorLabel.Visible = true;
}
}
|
The Parameters for the FormsAuthenticationTicket constructor
are:
version --The
version number.
name --
User name associated with the ticket.
issueDate
-- Time at which the cookie was issued.
expiration --
Expiration date for the cookie.
isPersistent
-- True if the cookie is persistent.
userData -- User-defined
data to be stored in the cookie.
At this point, we're about 100% wired up for custom
Forms Authentication. What I did here for this example is simply re-used
the ever-popular Northwind database. I took the Employees table (hey,
why reinvent the wheel...) and added three columns, all nvarchar:
username - the username they will login
with
password - the password they will use
roles - a comma - delimited list of roles attached
to this user e.g. "Administrator,Manager,Accounting, User"
So if you want to use the solution "out of the
box" you'll only need to modify your Employees table in the Northwind
database as above. Put in several users with different roles so you
can experiment. Leave Nancy Davolio alone.
To summarize how custom FormsAuthentication works: a
user is authenticated, then an authentication cookie ("ticket")
is attached to the validating Response when you call one of the appropriate
static methods of the FormsAuthentication provider. It is at that
moment that the Application_AuthenticateRequest handler
shown in the very first block of code in this article is called. When
dynamically assigning custom roles (the UserData parameter) to a user
you need to create a new instance of GenericPrincipal and assign it
to the current thread context, and the Application_AuthenticateRequest
handler in global.asax is the ideal place to do this.
The only thing that remains is to check users' identity
credentials for roles wherever you need to in your code. This could
be in the Page_Load event handler to control page access, or it could
be around code that controls the visibility of particular controls on
the page. For example, right now I'm coding a special utility page that
allows users to view the status of some running services for one of
our apps. However, only certain restricted users are actually allowed
to start or stop these services. So with this arrangement, all I need
to do is check the role of the authenticated user and if they aren't
allowed to, I set the visibility property of the buttons that start
or stop the services appropriately. if they don't have the credentials,
they still get to see the services - just no stop/start buttons.
Here's an example of how you might check credentials
on a given page:
private void
Page_Load(object sender, System.EventArgs e)
{
try
{
// if
they haven't logged in this will fail and we can send them to
// the login page
FormsIdentity id = (FormsIdentity)HttpContext.Current.User.Identity;
}
// whatever
bad happened, let's just send them back to login page for now...
catch(Exception ex )
{
Response.Redirect("Default.aspx");
// whatever your login page is
}
//
is this an Administrator role?
if (User.IsInRole("Administrator"))
{
Response.Write("Welcome
Big Admin!");
// ok
let's enumerate their roles for them...
FormsIdentity id = (FormsIdentity)HttpContext.Current.User.Identity;
FormsAuthenticationTicket
ticket = id.Ticket;
string userData = ticket.UserData;
string[] roles = userData.Split(',');
foreach(string role in roles)
{
Response.Write("You
are: " + role.ToString()+"<BR>");
}
Response.Write ("You get to see the Admin link:<BR><A
href=\"Admin/Adminstuff.aspx\">Admin Only</a>");
}
else
{
// ok, they
got in but we know they aren't an Administrator...
Response.Write("Ya got logged
in, but you ain't an Administrator!");
FormsIdentity id = (FormsIdentity)HttpContext.Current.User.Identity;
FormsAuthenticationTicket ticket
= id.Ticket;
string userData = ticket.UserData;
string[] roles = userData.Split(',');
foreach(string role
in roles)
{
Response.Write("You
are: " +role.ToString()+"<BR>");
}
}
|
There is plenty more cool stuff you can do with this
that I haven't shown here. For example, although I haven't had the time
to completely look into it, you might be able to have a Webservice with
a Login webmethod that returns the FormsAuth ticket (encrypted of course
with the encrption method) back to the user in response to a login SOAP
method providing their username and password. Then on subsequent calls
to other methods they would pass their ticket either as a SOAP header
or as an element in the SOAP body. Your code would grab this ticket
and basically do the same thing we are doing here above without having
to hit the database again.
Another interesting idea would be to return an XML
document (as a string from a database column) into the userData parameter.
This would allow you to load the string into an XmlDocument instance
and use XPath to handle complex hierarchical user memberships and relationships
which could be more complicated than a simple array of roles.
You can download the complete solution which also
includes a subfolder "Admin" along with the web.config for
it that controls access to the page therein, here:
| Peter Bromberg is a C# MVP, MCP, and .NET consultant who has worked in the banking and financial industry for 20 years. He has architected and developed web - based corporate distributed application solutions since 1995, and focuses exclusively on the .NET Platform. |
|