Introduction
This article explains a simple tip on how to customized the IPrincipal
used in ASP.NET MVC4 internet application project template. You can try this tip if you want
to attach additional information on the IPrincipal
(Controller.User) for some purposes.
Background
This tip is based from the solution I used in implementing custom identity in my ASP.NET MVC 3 project which I got from this
thread: http://stackoverflow.com/questions/1064271/asp-net-mvc-set-custom-iidentity-or-iprincipal.
The main solution is almost the same from the said thread but with just a few tweaks required to set data to additional IPrincipal
properties when OAuthWebSecurity
is used as authentication method.
Using the code
Initially ASP.NET MVC 4 internet project template is configured to use both
WebMatrix.WebSecurity
(for local accounts) and OAuthWebSecurity
(for external site accounts) for authentication. Also accounts data
are getting saved in a UserProfile
table which only have properties for user ID and username, and some predefined webpages_TABLES.
This initial setup is not enough for us to achieve our goal: that is to attach additional information in the IPrincipal
. In this example we will going to need to add the first
name and last name info of the user but you can add any data to suit your needs.
We will need first a storage of the additional data we want to attach. To do this you can just simply add properties on the UserProfile
class
defined in AccountModels.cs. Or use any table then modify the InitializeSimpleMembershipAttribute.cs from the Filters folder and set your DBContext
and table name:
public SimpleMembershipInitializer()
{
Database.SetInitializer<YourDBContext>(null);
try
{
using (var context = new UsersContext())
{
if (!context.Database.Exists())
{
((IObjectContextAdapter)context).ObjectContext.CreateDatabase();
}
}
WebSecurity.InitializeDatabaseConnection("DefaultConnection", "YourDesiredTable",
"UserId", "UserName", autoCreateTables: true);
}
catch (Exception ex)
{
throw new InvalidOperationException("The ASP.NET Simple Membership database could " +
"not be initialized. For more information, please see " +
"http://go.microsoft.com/fwlink/?LinkId=256588", ex);
}
}
Another way is to leave the SimpleMembershipInitializer
as it is and check this
tutorial: http://www.asp.net/mvc/tutorials/mvc-4/using-oauth-providers-with-mvc
If your data storage is now ready we can now start creating custom IPrincipal
:
public interface ICustomPrincipal : System.Security.Principal.IPrincipal
{
string FirstName { get; set; }
string LastName { get; set; }
}
public class CustomPrincipal : ICustomPrincipal
{
public IIdentity Identity { get; private set; }
public CustomPrincipal(string username)
{
this.Identity = new GenericIdentity(username);
}
public bool IsInRole(string role)
{
return Identity != null && Identity.IsAuthenticated &&
!string.IsNullOrWhiteSpace(role) && Roles.IsUserInRole(Identity.Name, role);
}
public string FirstName { get; set; }
public string LastName { get; set; }
public string FullName { get { return FirstName + " " + LastName; } }
}
public class CustomPrincipalSerializedModel
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
}
Then in the AccountController
class, add this method. We will need this method to serialize the user data and attach it in a cookie:
public void CreateAuthenticationTicket(string username) {
var authUser = Repository.Find(u => u.Username == username);
CustomPrincipalSerializedModel serializeModel = new CustomPrincipalSerializedModel();
serializeModel.FirstName = authUser.FirstName;
serializeModel.LastName = authUser.LastName;
JavaScriptSerializer serializer = new JavaScriptSerializer();
string userData = serializer.Serialize(serializeModel);
FormsAuthenticationTicket authTicket = new FormsAuthenticationTicket(
1,username,DateTime.Now,DateTime.Now.AddHours(8),false,userData);
string encTicket = FormsAuthentication.Encrypt(authTicket);
HttpCookie faCookie = new HttpCookie(FormsAuthentication.FormsCookieName, encTicket);
Response.Cookies.Add(faCookie);
}
Call the above method: From the ExternalLoginCallback
method:
public ActionResult ExternalLoginCallback(string returnUrl)
{
AuthenticationResult result = OAuthWebSecurity.VerifyAuthentication(
Url.Action("ExternalLoginCallback", new { ReturnUrl = returnUrl }));
if (!result.IsSuccessful)
{
return RedirectToAction("ExternalLoginFailure");
}
if (OAuthWebSecurity.Login(result.Provider, result.ProviderUserId, createPersistentCookie: true))
{
CreateAuthenticationTicket(OAuthWebSecurity.GetUserName(
result.Provider, result.ProviderUserId));
return RedirectToLocal(returnUrl);
}
if (User.Identity.IsAuthenticated)
{
OAuthWebSecurity.CreateOrUpdateAccount(result.Provider, result.ProviderUserId, User.Identity.Name);
CreateAuthenticationTicket(User.Identity.Name);
return RedirectToLocal(returnUrl);
}
else
{
string loginData = OAuthWebSecurity.SerializeProviderUserId(result.Provider, result.ProviderUserId);
ViewBag.ProviderDisplayName = OAuthWebSecurity.GetOAuthClientData(result.Provider).DisplayName;
ViewBag.ReturnUrl = returnUrl;
return View("ExternalLoginConfirmation",
new RegisterExternalLoginModel { UserName = result.UserName, ExternalLoginData = loginData });
}
}
In Register
method:
public ActionResult Register(RegisterModel model)
{
if (ModelState.IsValid)
{
try
{
WebSecurity.CreateUserAndAccount(
model.UserName,
model.Password,
new {
UpdatedBy = 0,
UpdatedDate = DateTime.Today,
CreatedBy = 0,
CreatedDate = DateTime.Today
}
);
WebSecurity.Login(model.UserName, model.Password);
CreateAuthenticationTicket(model.UserName);
return RedirectToAction("Index", "Home");
}
catch (MembershipCreateUserException e)
{
ModelState.AddModelError("", ErrorCodeToString(e.StatusCode));
}
}
In ExternalLoginConfirmation
method:
...
OAuthWebSecurity.CreateOrUpdateAccount(provider, providerUserId, model.UserName);
OAuthWebSecurity.Login(provider, providerUserId, createPersistentCookie: false);
CreateAuthenticationTicket(model.UserName);
return RedirectToLocal(returnUrl);
...
And in the Login method:
public ActionResult Login(LoginModel model, string returnUrl)
{
if (ModelState.IsValid && WebSecurity.Login(model.UserName,
model.Password, persistCookie: model.RememberMe))
{
CreateAuthenticationTicket(model.UserName);
return RedirectToLocal(returnUrl);
}
ModelState.AddModelError("", "The user name or password provided is incorrect.");
return View(model);
}
It's now time to read the serialized data from our cookie and replace the HttpContext.Current.User
.
Do this by overriding the Application_PostAuthenticateRequest
method in project's Global.asax.cs
.
protected void Application_PostAuthenticateRequest(Object sender, EventArgs e)
{
HttpCookie authCookie = Request.Cookies[FormsAuthentication.FormsCookieName];
if (authCookie != null)
{
FormsAuthenticationTicket authTicket = FormsAuthentication.Decrypt(authCookie.Value);
JavaScriptSerializer serializer = new JavaScriptSerializer();
if (authTicket.UserData == "OAuth") return;
CustomPrincipalSerializedModel serializeModel =
serializer.Deserialize<CustomPrincipalSerializedModel>(authTicket.UserData);
CustomPrincipal newUser = new CustomPrincipal(authTicket.Name);
newUser.Id = serializeModel.Id;
newUser.FirstName = serializeModel.FirstName;
newUser.LastName = serializeModel.LastName;
HttpContext.Current.User = newUser;
}
}
To access the attached data from pages:
@(User as CustomPrincipal).FullName
And from server:
@(User as CustomPrincipal).FullName
Points of Interest
Nothing clever from what I did actually, just a simple tip.