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 theFormsAuthentication 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 theObject 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.
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"
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>"); } } |
No comments:
Post a Comment