When you talk about authentication in ASP.NET you will most undoubtedly hear the mention of the MembershipProvider. I'm here to tell you not to go down that road. That road will only lead to tears and suffering. This post will help you understand and implement your ASP.NET authentication built on top of FormsAuthentication. I hope you take away that building this stuff is not hard as long as you don't try to over think it.
Before I show you how to implement a kick-butt authentication system, I'll show you the simplest solution you could build.
FormsAuthentication.SetAuthCookie(user.Id, createPersistentCookie: true);
Congratulations you are now officially signed into your ASP.NET application using forms authentication. Notice I don't have any SQL migrations, crazy classes, or any other crap you didn't ask for.
The code above writes a cookie to the response of the current HttpContext. On the user's next request that cookie will be passed back to the server and used to check whether they are authenticated or not.
Password Hasher
Hashing passwords is really easy with .NET. The trick is to generate a salt with every password. The idea of the salt is not to be a secret, but to be unique every time. This makes it difficult for hackers to process all your passwords in the case your system is compromised. This is my standard password hasher:
public static class PasswordHasher {
private const int SaltSize = 64;
public static Passphrase Hash(string password) {
if (password == null) throw new ArgumentNullException("password");
byte[] passwordBytes = Encoding.Unicode.GetBytes(password);
byte[] saltBytes = CreateRandomSalt();
string hashedPassword = ComputeHash(passwordBytes, saltBytes);
return new Passphrase {
Hash = hashedPassword,
// Convert salt from byte[] to string
Salt = Convert.ToBase64String(saltBytes)
};
}
public static bool Equals(string password, string salt, string hash) {
return String.CompareOrdinal(hash, Hash(password, salt)) == 0;
}
public static string GenerateRandomSalt(int size = SaltSize) {
return Convert.ToBase64String(CreateRandomSalt(size));
}
private static string ComputeHash(byte[] password, byte[] salt) {
var passwordAndSalt = new byte[salt.Length + password.Length];
Buffer.BlockCopy(salt, 0, passwordAndSalt, 0, salt.Length);
Buffer.BlockCopy(password, 0, passwordAndSalt, salt.Length, password.Length);
byte[] computedHash;
using (HashAlgorithm algorithm = new SHA256Managed()) {
computedHash = algorithm.ComputeHash(passwordAndSalt);
}
return Convert.ToBase64String(computedHash);
}
private static string Hash(string password, string salt) {
return ComputeHash(Encoding.Unicode.GetBytes(password), Convert.FromBase64String(salt));
}
private static byte[] CreateRandomSalt(int size = SaltSize) {
if (size <= 0)
throw new ArgumentException("size must be greater than zero.");
var saltBytes = new Byte[size];
using (var rng = new RNGCryptoServiceProvider()) {
rng.GetBytes(saltBytes);
}
return saltBytes;
}
}
public class Passphrase {
public string Hash { get; set; }
public string Salt { get; set; }
}
Principal and Identity
The IPrincipal and IIdentity interfaces are crucial to authentication in .NET. If you have ever used HttpContext, WebForms, or ASP.NET MVC then you are using derivations of these interfaces. It usually comes in the guise of a User property. You don't have to implement these classes, you can always use the GenericPrincipal and GenericIdentity. I like to implement them myself, because it allows me to pass a bit more useful data around in the cookie (remember cookies have size limits).
Let's first look at the IPrinicipal implementation:
public class MuchoPrincipal : IPrincipal {
private readonly MuchoIdentity _identity;
public MuchoPrincipal(MuchoIdentity identity) {
_identity = identity;
}
#region IPrincipal Members
public bool IsInRole(string role) {
return
_identity.Roles.Any(
current => string.Compare(current, role, StringComparison.InvariantCultureIgnoreCase) == 0);
}
public IIdentity Identity {
get { return _identity; }
}
public MuchoIdentity Information {
get { return _identity; }
}
public bool IsUser {
get { return !IsGuest; }
}
public bool IsGuest {
get { return IsInRole("guest"); }
}
#endregion
}
Next up is the IIdentity, just take a look:
public class MuchoIdentity : IIdentity {
public MuchoIdentity(FormsAuthenticationTicket ticket) {
if (ticket == null) {
Name = "Guest";
Roles = new List<string> { "guest" };
return;
}
var data = JsonConvert.DeserializeObject<MuchoCookie>(ticket.UserData);
if (data == null) {
AsGuest();
return;
}
Id = data.Id;
FirstName = data.FirstName;
LastName = data.LastName;
Name = string.IsNullOrWhiteSpace(FirstName) || string.IsNullOrWhiteSpace(LastName)
? data.Email
: "{0} {1}".With(FirstName, LastName);
Email = data.Email;
Roles = data.Roles ?? new List<string> { "user" };
RememberMe = data.RememberMe;
try {
TimeZone = TimeZoneInfo.FindSystemTimeZoneById(data.TimeZone);
} catch (Exception) {
TimeZone = TimeZoneInfo.Utc;
}
}
public MuchoIdentity(User user) {
if (user == null) {
AsGuest();
return;
}
Id = user.Id;
Email = user.Email;
FirstName = user.FirstName;
LastName = user.LastName;
Name = string.IsNullOrWhiteSpace(FirstName) || string.IsNullOrWhiteSpace(LastName)
? user.Email
: "{0} {1}".With(FirstName, LastName);
try {
TimeZone = TimeZoneInfo.FindSystemTimeZoneById(user.TimeZone);
} catch (Exception) {
TimeZone = TimeZoneInfo.Utc;
}
Roles = new List<string> { user.Role ?? "user" };
}
private void AsGuest() {
Name = "Guest";
Roles = new List<string> { "guest" };
}
public int Id { get; set; }
public string Email { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public TimeZoneInfo TimeZone { get; set; }
public bool RememberMe { get; set; }
public IList<string> Roles { get; set; }
#region IIdentity Members
public string AuthenticationType {
get { return "MuchoForms"; }
}
public bool IsAuthenticated {
get { return !( Id == 0 || string.IsNullOrWhiteSpace(Email)); }
}
public string Name { get; protected set;
#endregion
}
C is for Cookie
The cookie object is really just a data transfer object. Nothing really mind blowing here. I usually create a structure to store my useful information. Again, keep in mind the size limitations of a cookie which is about 4kb (4096 bytes).
public class MuchoCookie {
public MuchoCookie() {
Roles = new List<string>();
}
public int Id { get; set; }
public string Email { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string TimeZone { get; set; }
public List<string> Roles { get; set; }
public bool RememberMe { get; set; }
}
FormsAuthenticationService
FormsAuthentication is built right into ASP.NET, but I like to write a littler wrapper around it so I can test and inject it into other classes. Also I create the cookie from the previous section when a user successfully signs in.
public class FormsAuthenticationService : IFormsAuthenticationService {
private readonly HttpContextBase _httpContext;
public FormsAuthenticationService(HttpContextBase httpContext) {
_httpContext = httpContext;
}
#region IFormsAuthenticationService Members
public void SignIn(User user, bool createPersistentCookie) {
if (user == null)
throw new ArgumentNullException("user");
var cookie = new MuchoCookie {
Id = user.Id,
Email = user.Email,
FirstName = user.FirstName,
LastName = user.LastName,
RememberMe = createPersistentCookie,
TimeZone = user.TimeZone,
Roles = new List<string> { user.Role ?? "user" }
};
string userData = JsonConvert.SerializeObject(cookie);
var ticket = new FormsAuthenticationTicket(1, cookie.Email, DateTime.Now,
DateTime.Now.Add(FormsAuthentication.Timeout),
createPersistentCookie, userData);
string encTicket = FormsAuthentication.Encrypt(ticket);
var httpCookie = new HttpCookie(FormsAuthentication.FormsCookieName, encTicket) { Expires = DateTime.Now.Add(FormsAuthentication.Timeout) };
_httpContext.Response.Cookies.Add(httpCookie);
}
public void SignOut() {
// Not worth covering, has been tested by Microsoft
FormsAuthentication.SignOut();
}
#endregion
}
Notice the SignIn method creates the cookie and just writes it to the response using FormsAuthentication. We are leveraging what ASP.NET gives us. There is no reason to reinvent the wheel when it comes to passing secure cookies to the client.
PrincipalService
The principal service helps us get the information out of a cookie and rehydrate our IPrinipal and our IIdentity to our custom implementations. The other added benefit is we can actually set the User property to a Guest principal with extra information if we need it.
public class MuchoSupportPrincipalService : IPrincipalService {
private readonly HttpContextBase _context;
public MuchoSupportPrincipalService(HttpContextBase context) {
_context = context;
}
#region IPrincipalService Members
public IPrincipal GetCurrent() {
IPrincipal user = _context.User;
// if they are already signed in, and conversion has happened
if (user != null && user is MuchoPrincipal)
return user;
// if they are signed in, but conversion has still not happened
if (user != null && user.Identity.IsAuthenticated && user.Identity is FormsIdentity) {
var id = (FormsIdentity)_context.User.Identity;
var ticket = id.Ticket;
if (FormsAuthentication.SlidingExpiration)
ticket = FormsAuthentication.RenewTicketIfOld(ticket);
var fid = new MuchoIdentity(ticket);
return new MuchoPrincipal(fid);
}
// not sure what's happening, let's just default here to a Guest
return new MuchoPrincipal(new MuchoIdentity((FormsAuthenticationTicket)null));
}
#endregion
}
The trick here is just to check the current context for the cookie already being passed in with our request. You need to call this code from your Global.asax as such (note I am using ASP.NET MVC and the dependency resolver built into it).
protected void Application_AuthenticateRequest(object sender, EventArgs e) {
var principalService = DependencyResolver.Current.GetService<IPrincipalService>();
var context = DependencyResolver.Current.GetService<HttpContextBase>();
// Set the HttpContext's User to our IPrincipal
context.User = principalService.GetCurrent();
}
Usage in Your Code
This is what my ASP.NET MVC controller looks like:
[RequireScheme(Scheme.Https)]
public class AuthenticationController : ApplicationController {
public ActionResult Login(string returnUrl) {
if (User.Identity.IsAuthenticated) {
if (!string.IsNullOrWhiteSpace(returnUrl))
return Redirect(returnUrl);
return RedirectToAction("show", "dashboard");
}
var model = new LoginModel {
ReturnUrl = returnUrl
};
return View(model);
}
[HttpPost]
public ActionResult Login(LoginModel input) {
// validation does password hash check
// you could do it more explicitly
if (ModelState.IsValid) {
Logger.Info("successful!", input.Username);
var user = Db.Users
.FirstOrDefault(u => u.Email == input.Username);
// set cookie
Forms.SignIn(user, input.RememberMe);
return input.HasReturnUrl(Url)
? Redirect(input.ReturnUrl)
: (ActionResult) RedirectToAction("show", "dashboard");
}
Flash.Error("Please try again");
return View("login", input);
}
public ActionResult Logout() {
Forms.SignOut();
return RedirectToAction("login");
}
}
Conclusion
The parts of an authentication system are all infrastructural. It is all smooth sailing once you get over the hump of setting it up. Once the infrastructure is set up you can work on creating your own tables for Users, Profiles, or any other domain model that makes sense. All access to user's is up to you, so feel free to use any data access provider you like: SQL Server, RavenDB, MongoDB, etc. Additionally, the cookie is now yours; feel free to add or remove data from it as you see fit, but always remember the 4kb size limit.