Every now and then I want to do a simple authentication scheme in an ASP.NET web application, without calling on various providers or other components that bring too much weight & functionality to the table. Implementing a custom IIdentity and IPrincipal is a straightforward way of achieving this.
The steps involved:
- create a custom implementation of these two interfaces
- set the custom principal as the user for
HttpContext.Current - cache the principal in session state (not actually required, but a simple optimization)
Implementing the interfaces
A simple skeleton for the IIdentity implementation:
public class MyIdentity : IIdentity
{
private string passedInName;
// IIdentity
public string AuthenticationType { get { return "MyIdentityAuthenticationType"; } }
public string Name { get { return passedInName; } }
public bool IsAuthenticated
{
get
{
// TODO: add whatever logic we need here (for instance, AD group membership)
throw new NotImplementedException();
}
}
// /IIdentity
// Hide constructor
private MyIdentity() { }
// Static factory method
internal static IIdentity CreateIdentity(IUserNameProvider provider)
{
IIdentity identity = new MyIdentity { passedInName = provider.AccountName } ;
return identity;
}
}
And for the IPrincipal-implementing class:
public class MyPrincipal : IPrincipal
{
private IIdentity identity;
// IPrincipal
public IIdentity Identity { get { return identity; } }
public bool IsInRole(string role)
{
// TODO add logic here
throw new NotImplementedException();
}
// hide Ctor
private MyPrincipal() { }
// static factory method
public static IPrincipal CreatePrincipal(IUserNameProvider provider)
{
IIdentity identity = MyIdentity.CreateIdentity(provider);
IPrincipal principal = new MyPrincipal { identity = identity };
return principal;
}
}
A few things to note:
IUserNameProvider has only a single string property, name. The injected type is used to determine the current user's name.
- I used simple static factory methods, that are by no means necessary in this context (regular constructors would work just as well).
- you could factor out the two "business logic" methods
IsAuthenticated and IsInRole to separate concerns using the template method pattern, but this is supposed to be a simple, lean implementation.
Hooking them up
The easiest way I've found to link the custom principal is by implementing the Application_AcquireRequestState event in the global.asax:
protected void Application_AcquireRequestState(object sender, EventArgs e)
{
IPrincipal principal = null;
var current = HttpContext.Current;
// check if session is available; needed because this event can/does fire multiple times per request
if (current.Session != null)
{
// check if we have the principal in session
if (current.Session["magicstring"] != null)
{
principal = current.Session["magicstring"] as IPrincipal;
}
// catches both (1) key not in session and (2) cast not succesfull
if (principal == null)
{
// create a new one and store it in the session
principal = MyPrincipal.CreatePrincipal(new MyWebAccountNameProvider());
current.Session["magicstring"] = principal;
}
// regardless of source (session or freshly created, store in appropriate context)
current.User = principal;
}
}
Actually, this event is a bit weird, because it can fire multiple times for a single request. The Session reference is only valid once however, which is why that check is there. So, what happens:
- The existence of a session variable is checked, and, if it exists, it is cast to an IPrincipal.
- If either the cast failed or the session var was empty, a new specific principal is constructed and stored in session.
- The custom principal is linked to the HttpContext.Current.User property, allowing us to use it in the rest of the application.
Wrapup and comments
This is by no means a do-all, end-all solution, but it works well. Depending on the tiering/layering of the solution, you can either check the HttpContext.Current.User property, or you can abstract that into a simple wrapper. I usually either use a static method on my principal that returns the actual active principal, or set up a context registry to hold my principal reference transparently across layers.