Step 2 - Extracting the User's Role Information
Extracting the user's role information involves a bit of reflection magic. Bascially, we use reflection to look inside the principal object that is provided to us by Windows/IIS when the user is authenticated. Please note that this involves reflecting over specific properties of the principal object and is not guranteed to work in future versions of .NET. It does work and has been tested in production environments using .NET Version 1.1 (v1.1.4322).
The most logical place to extract the role information from the principal is the 'Application_AuthenticateRequest' event. Briefly, we look at a particular string array within the principal to extract any role information from the principal. There are some tricks to watch out for, which we will cover shortly. Listed below is some code that will extract the role from a principal object.
private static string[] GetRoles(IPrincipal princ)
{
Type type = princ.GetType();
// Note: This code sets the 'MAGIC_NUMBER' field of the principal object.
FieldInfo field2 = type.GetField("MAGIC_NUMBER",
BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Static);
field2.SetValue(princ,40); // This value can be any number but defaults to 23.
princ.IsInRole("DummyRole"); // This call is required so that the subsystem goes
// and retrieves a list of roles.
// Without this call, the principal object does not contain any roles in its internal
// variables, and thus the code below that uses reflection to get the value of this variable
// will fail and return NULL.
FieldInfo field = type.GetField("m_roles", BindingFlags.Instance | BindingFlags.NonPublic);
String[] roles = (String[]) field.GetValue(princ);
return roles;
}
And we obviously need to call this routine from the 'Application_AuthenticateRequest' event as in the code shown below:
protected void Application_AuthenticateRequest(Object sender, EventArgs e)
{
WindowsIdentity ident = WindowsIdentity.GetCurrent();
WindowsPrincipal wind_princ = new WindowsPrincipal(ident);
string[] roles = GetRoles(wind_princ);
......
......
}
You will notice in the 'GetRoles' method above, we set a 'MAGIC_NUMBER'. This number determines how many roles the user can have before the roles are stored in a hashtable in the principal object, instead of a string array. This is done for performance reasons, but the important part is that if your intranet users are on a domain and happen to have more than this 'MAGIC_NUMBER' number of roles, then the string array you are getting the users roles from will be empty. By default, this 'MAGIC_NUMBER' is set to 23. The code in the method above provides the ability to change this 'MAGIC_NUMBER' to any value you like. For our purposes, we will change it to something higher to make sure we get all the roles in our string array.
Step 3 - Simulating a Valid Forms Authentication Ticket and Supplying the User's Roles
Now we need to put all this together. We need to take the role information we have extracted from the principal and pass it over to our forms authentication entry point application, telling that entry point we have a valid autenticated user.
The Windows authentication entry point site has extracted the role information. Now it only needs to create a forms authentication ticket, store the roles in the user data, and issue the ticket. The ticket must have the same path and name across both the forms authentication site and the windows authentication site. The code below shows the code that should be placed in the 'Global.asax - Application_AuthenticateRequest' event for the storage of the roles and the redirection.
protected void Application_AuthenticateRequest(Object sender, EventArgs e)
{
// NOTE: Because we are encrypting some information and passing it to another site,
// the MACHINE.CONFIG file needs to have the <machinekey> element set to a static
// value. By default, this value is autogenerated for each web application on the
// machine and will therefore fail if you try and pass encrypted tickets/data between
// web apps.
WindowsIdentity ident = WindowsIdentity.GetCurrent();
WindowsPrincipal wind_princ = new WindowsPrincipal(ident);
string[] roles = GetRoles(wind_princ); // See method above for
// implementation of this method
string roleData = String.Join(";",roles);
FormsAuthenticationTicket ticket =
new FormsAuthenticationTicket(1,
"your_ticket_name",DateTime.Now,
DateTime.Now.AddMinutes(30),false,roleData,"/");
string encTicket = FormsAuthentication.Encrypt(ticket);
// NOTE: The name of the HttpCookie must match what the FormsAuth site expects.
Response.Cookies.Add(new HttpCookie("YourCookieName",encTicket));
// Ticket and cookie issued, now go to the FormsAuth site and all should be well.
Response.Redirect("http://localhost/TestFormsAuthSite/IntoSite.aspx");
}
As you can see, we encrypt this ticket using the forms authentication standard routines. For this to work across applications, modifications must be made to the machine.config file to ensure encryption keys are not auto-generated for each application. The modification required is shown below:
<!-- Your old machine.config entry will
resemble the line below -->
<!-- <machineKey validationKey="AutoGenerate,IsolateApps"
decryptionKey="AutoGenerate,IsolateApps" validation="SHA1" /> -->
<!-- You NEED to change that entry to resemble
something like the line below. Note: The validationKey/decriptionKey
should be your own -->
<machineKey
validationKey="357356792679345184568256876535689056434617489465"
decryptionKey="357356792679345184568256876535689056434617489465"
validation="SHA1"/>
Lastly, the 'Global.asax' file in the forms authentication entry point site must be modified so that the 'Application_AuthenticateRequest' event knows how to decipher a valid ticket issued by the Windows authentication entry point site and assign these roles to the current principal. The web.config of this site must also be setup to have matching attributes (i.e., name) as what the Windows authentication site is issuing. The code below demonstrates this :
Web.Config
<authentication mode="Forms">
<forms name="YourCookieName" <!-- This cookie name matches what
the Windows Auth site creates -->
loginUrl="forms_login.aspx"
protection="All"
timeout="30"
path="/" <!-- This same path as what the Windows auth site uses
when creating the cookie/auth ticket -->
requireSSL="false"
slidingExpiration="true">
</forms>
</authentication>
<authorization>
<deny users="?" /> <!-- Only allow authenticated users. Add your
application and domain specific access roles here -->
</authorization>
Application_Authenticate Event (Forms Auth site)
protected void Application_AuthenticateRequest(Object sender, EventArgs e)
{
bool cookieFound = false;
HttpCookie authCookie = null;
HttpCookie cookie;
for(int i=0; i < Request.Cookies.Count; i++)
{
cookie = Request.Cookies[i];
if (cookie.Name == FormsAuthentication.FormsCookieName)
{
cookieFound = true;
authCookie = cookie;
break;
}
}
// If the cookie has been found, it means it has been issued from either
// the windows authorisation site, is this forms auth site.
if (cookieFound)
{
// Extract the roles from the cookie, and assign to our current principal,
// which is attached to the HttpContext.
FormsAuthenticationTicket winAuthTicket =
FormsAuthentication.Decrypt(authCookie.Value);
string[] roles = winAuthTicket.UserData.Split(';');
FormsIdentity formsId = new FormsIdentity(winAuthTicket);
GenericPrincipal princ = new GenericPrincipal(formsId,roles);
HttpContext.Current.User = princ;
}
else
{
// No cookie found, we can redirect to the Windows auth site if we want,
// or let it pass through so that the forms auth system redirects to
// the logon page for us.
}
}
Final Considerations
The forms authentication site must also contain code for users who are logging in via the standard logon page. Typically, you would assign a single role of low privilege to identify the user as a non-intranet user (e.g., external to the organization) and assign that role to a principal and attach it to the HttpContext in similar fashion to the code above.
At this point, your application can make role checks against the principal attached to the HttpContext in a standard manner. The application does not have to worry about where the user came from--standard role checks are all that is needed--, and it can therefore tailor the user experience based on the different roles.
Conclusion
The technique I have described here is not trivial and requires reasonable knowledge of the forms authentication system that .NET uses. The advantage is great though. You can now let both intranet users and external parties access the same application in a consistent manner with the standard role-based access checks being applied to determine a user's functionality and origin. The code to do this is not great and the setup can be tricky, but this technique does work and is currently being used in a number of ways within our organization to great effect.
If you are having difficulty with the concept or its implementation, I can be contacted at glav@aspalliance.com.
Happy Coding...