Customizing Forms Authentication
Usually, you make use of some extensibility points in ASP.NET to customize the behavior of Forms authentication.
- Issuing the authentication ticket manually. This gives you more control over the ticket. You can, for example, embed user-defined data in the ticket that can be extracted at a later point.
- Adding roles to a user. You normally want to attach application-defined roles to a user. This has to be done manually after the AuthenticateRequest event in the pipeline.
Issuing the authentication ticket manually
You can create authentication tickets on your own and explicitly control their contents. Afterward, you can encrypt the ticket and issue it manually. This gives you the chance to set and add user-defined data. (That data is often used for roles caching.) If you issue the ticket manually, be careful to take all Forms authentication configuration settings into account; you also have to make some checks that would otherwise be done by the built-in APIs, such as checking for SSL if requiredSSL is set to true. After you set the ticket, you also have to redirect manually to the originally requested page. The following code constructs a FormsAuthenticationTicket object and optionally adds extra data to it.
Creating a FormsAuthenticationTicket
public static FormsAuthenticationTicket
CreateAuthenticationTicket(string username, string userData)
{
// Grab the current request context.
HttpContext context = HttpContext.Current;
// Get the ticket timeout from Forms configuration.
AuthenticationSection config = (AuthenticationSection)
context.GetSection("system.web/authentication");
int timeout = (int)config.Forms.Timeout.TotalMinutes;
if (string.IsNullOrEmpty(userData))
userData = String.Empty;
// Create the auth ticket manually and set values.
FormsAuthenticationTicket ticket = new
FormsAuthenticationTicket(
1, // version
username, // user name
DateTime.Now, // creation time
DateTime.Now.AddMinutes(timeout), // expiration time
false, // persistent
userData, // optional data
FormsAuthentication.FormsCookiePath); // path
return ticket;
}
After you have a ticket, you have to issue it manually. For cookies, construct an HttpCookie object and set the relevant properties.
Issuing a Cookie
public static void
SetAuthenticationCookie(FormsAuthenticationTicket ticket)
{
// Grab the current request context.
HttpContext context = HttpContext.Current;
// Encrypt the ticket.
string authcookie = FormsAuthentication.Encrypt(ticket);
// Create new cookie and set contents.
HttpCookie cookie = new
HttpCookie(FormsAuthentication.FormsCookieName);
cookie.Value = authcookie;
// Respect requireSSL and domain settings.
cookie.Secure = FormsAuthentication.RequireSSL;
cookie.Domain = FormsAuthentication.CookieDomain;
// Set HttpOnlycookie will not be available to client script.
cookie.HttpOnly = true;
// Check whether SSL is required.
if (!context.Request.IsSecureConnection &&
FormsAuthentication.RequireSSL)
throw new HttpException("Ticket requires SSL");
// Set the cookie.
context.Response.Cookies.Add(cookie);
}
It is recommended that you should put all extensibility code in a separate assembly and use an HttpModule for the pipeline processing. This makes it reusable across applications.
Adding roles to a user
After the user is authenticated, the user name is available through Context.User.Identity.Name. Based on the user name, you can get the roles from your data store. Afterward, you can create a new GenericPrincipal (or a custom one) based on the current user identity and your roles. The last step is to set Context.User and Thread.CurrentPrincipal to this new principal to make it available for the remainder of the request.
As stated earlier, you can handle this event by adding an Application_PostAuthenticateRequest method to global.asax or, if you want to reuse the module across different applications, in an HttpModule. Following is the code for a module.
Forms Authentication Postprocessing Module
using System;
using System.Web;
using System.Security.Principal;
namespace LeastPrivilege
{
class AuthenticationModule : IHttpModule
{
public void Init(HttpApplication context)
{
// Register for the PostAuthenticateRequest event.
context.PostAuthenticateRequest +=
new EventHandler(OnPostAuthenticateRequest);
}
void OnPostAuthenticateRequest(object sender, EventArgs e)
{
HttpContext context = HttpContext.Current;
// Only run when the user has already authenticated.
if (context.Request.IsAuthenticated)
{
// Grab the roles for the user from some data store.
string[] roles = AuthenticationHelper.GetRolesForUser
(context.User.Identity.Name);
// Create a new GenericPrincipal based on
// the user identity and the roles.
GenericPrincipal p =
new GenericPrincipal(context.User.Identity, roles);
// Set the new principal to make it available for the rest
// of the request.
context.User = Thread.CurrentPrincipal = p;
}
}
public void Dispose()
{
// Cleanup code would go here (not necessary here).
}
}
}
The next step is to register your module in web.config.
<httpModules>
<add
name="AuthenticationModule"
type="LeastPrivilege.AuthenticationModule" />
</httpModules>
ASP.NET synchronizes Context.User and Thread.CurrentPrincipal only after the AuthenticateRequest event. If you change Context.User in PostAuthenticateRequest, you have to sync both identities yourself.
The preceding approach has one problem: you have to hit your data store on every request. This can decrease performance, and perhaps you want to cache the roles rather than re-fetch them every time. The better options are to use the cache or to store the roles in the authentication ticket.
Using the Cache
public static string[] GetRolesForUserCached(string username)
{
string[] roles = null;
HttpContext context = HttpContext.Current;
// Check whether roles are already cached or expired.
if (context.Cache[username] == null)
{
// Throw if user is deleted or locked out.
// Gives you the chance to take action.
try
{
roles = GetRolesForUser(username);
}
catch
{
throw;
}
// Cache the roles.
context.Cache.Insert(
username,
roles,
null,
DateTime.Now.AddMinutes(30),
Cache.NoSlidingExpiration);
}
return (string[])context.Cache[username];
}
Caching Roles in the Ticket
protected void _btnLogin_Click(object sender, EventArgs e)
{
if (AuthenticationHelper.ValidateUser(
_txtUsername.Text, _txtPassword.Text))
{
// Get roles.
string[] roles = AuthenticationHelper.GetRolesForUser
(_txtUsername.Text);
// Encode roles in delimited string.
string rolesString = string.Join("|", roles);
// Create authentication ticket.
FormsAuthenticationTicket ticket =
FormsAuthHelper.CreateAuthenticationTicket
(_txtUsername.Text, rolesString, FormsAuthHelper.FormsAuthTimeout);
// Issue authentication ticket.
FormsAuthHelper.SetAuthenticationCookie(ticket);
// Redirect back to originally requested resource.
Response.Redirect
(FormsAuthentication.GetRedirectUrl(string.Empty, false));
}
else
_litMessage.Text = "Login failed, please try again";
}
public static FormsAuthenticationTicket CreateAuthenticationTicket
(string username, string[] roles, int timeout)
{
HttpContext context = HttpContext.Current;
// Set the role-caching timeout.
DateTime rolesExpiration = DateTime.Now.AddMinutes(timeout);
string rolesExpirationString =
rolesExpiration.ToString("yyyy.MM.dd.HH.mm.ss");
// Encode roles and timeout in the ticket.
string rolesString = string.Join("|", roles);
string userData = rolesExpirationString + "|" + rolesString;
return CreateAuthenticationTicket(username, userData);
}
public static FormsAuthenticationTicket
CreateAuthenticationTicket(string username, string userData)
{
// Grab the current request context.
HttpContext context = HttpContext.Current;
// Get the ticket timeout from Forms configuration.
AuthenticationSection config = (AuthenticationSection)
context.GetSection("system.web/authentication");
int timeout = (int)config.Forms.Timeout.TotalMinutes;
if (string.IsNullOrEmpty(userData))
userData = String.Empty;
// Create the auth ticket manually and set values.
FormsAuthenticationTicket ticket = new
FormsAuthenticationTicket(
1, // version
username, // user name
DateTime.Now, // creation time
DateTime.Now.AddMinutes(timeout), // expiration time
false, // persistent
userData, // optional data
FormsAuthentication.FormsCookiePath); // path
return ticket;
}
// Get the timeout value from Forms configuration.
public static int FormsAuthTimeout
{
get
{
HttpContext ctx = HttpContext.Current;
AuthenticationSection config =
(AuthenticationSection)ctx.GetSection(
"system.web/authentication");
return = (int)config.Forms.Timeout.TotalMinutes;
}
}
Instead of querying the database, you can now extract the roles from the ticket and check the expiration date in PostAuthenticateRequest. If the data is expired, it will be refetched from the data store. If an error occurs here (for example, because the user has been deleted in the meantime), you can force the user to reauthenticate by calling FormsAuthentication.Signout and FormsAuthentication .RedirectToLoginPage.
public class AuthenticationModule : IHttpModule
{
public void Init(HttpApplication context)
{
// register for the PostAuthenticateRequest event
context.PostAuthenticateRequest += new EventHandler(OnPostAuthenticate);
}
void OnPostAuthenticate(object sender, EventArgs e)
{
HttpContext context = HttpContext.Current;
string[] roles = null;
if (context.Request.IsAuthenticated)
{
try
{
// extract roles from ticket
roles = ExtractRolesWithTimeout();
}
catch
{
// clear ticket
FormsAuthentication.SignOut();
// redirect to login page
FormsAuthentication.RedirectToLoginPage();
return;
}
GenericPrincipal p = new GenericPrincipal(context.User.Identity, roles);
context.User = Thread.CurrentPrincipal = p;
}
}
public void Dispose()
{
// clean up code would go here (not necessary here)
}
// extracts roles from the current Context.User, taking checking and updating of the timeout into account
public static string[] ExtractRolesWithTimeout()
{
// grab the current request context
HttpContext context = HttpContext.Current;
if (context.User.Identity is FormsIdentity)
{
return ExtractRolesWithTimeout((FormsIdentity)context.User.Identity);
}
else
throw new ArgumentException("Not of type FormsIdentity", "Context.User.Identity");
}
// extracts roles from a ticket, taking checking and updating of the timeout into account
public static string[] ExtractRolesWithTimeout(FormsIdentity id)
{
// extract roles from ticket
return FormsAuthHelper.ExtractRolesWithTimeout(
id.Ticket.UserData,
id.Name);
}
// extracts roles from a userData string, taking checking and updating of the timeout into account
public static string[] ExtractRolesWithTimeout(string userdata, string username)
{
// extract expiration date
string[] data = userdata.Split('|');
string[] expiration = data[0].Split('.');
DateTime expirationDate = DateTime.Parse(String.Format("{0}.{1}.{2} {3}:{4}:{5}",
expiration[0],
expiration[1],
expiration[2],
expiration[3],
expiration[4],
expiration[5]));
// check if roles information has expired
if (DateTime.Now > expirationDate)
{
// check if user is still valid, if yes issue a new ticket
// if no, throw an exception
string[] roles = AuthenticationHelper.GetRolesForUser(username);
FormsAuthenticationTicket ticket = FormsAuthHelper.CreateAuthenticationTicket(username, roles, 30);
FormsAuthHelper.SetAuthenticationCookie(ticket);
return roles;
}
else
{
// extract roles from userData string and return them as an array
string[] roles = new string[data.Length - 1];
for (int i = 0; i < roles.Length; i++)
{
roles[i] = data[i + 1];
}
return roles;
}
}
}