ASP.NET MVC - Alternative to Role Provider? - asp.net
I'm trying to avoid the use of the Role Provider and Membership Provider since its way too clumsy in my opinion, and therefore I'm trying to making my own "version" which is less clumsy and more manageable/flexible. Now is my question.. is there an alternative to the Role Provider which is decent? (I know that I can do custom Role provier, membership provider etc.)
By more manageable/flexible I mean that I'm limited to use the Roles static class and not implement directly into my service layer which interact with the database context, instead I'm bound to use the Roles static class which has its own database context etc, also the table names is awful..
Thanks in advance.
I'm in the same boat as you - I've always hated the RoleProviders. Yeah, they're great if you want to get things up and running for a small website, but they're not very realistic. The major downside I've always found is that they tie you directly to ASP.NET.
The way I went for a recent project was defining a couple of interfaces that are part of the service layer (NOTE: I simplified these quite a bit - but you could easily add to them):
public interface IAuthenticationService
{
bool Login(string username, string password);
void Logout(User user);
}
public interface IAuthorizationService
{
bool Authorize(User user, Roles requiredRoles);
}
Then your users could have a Roles enum:
public enum Roles
{
Accounting = 1,
Scheduling = 2,
Prescriptions = 4
// What ever else you need to define here.
// Notice all powers of 2 so we can OR them to combine role permissions.
}
public class User
{
bool IsAdministrator { get; set; }
Roles Permissions { get; set; }
}
For your IAuthenticationService, you could have a base implementation that does standard password checking and then you could have a FormsAuthenticationService that does a little bit more such as setting the cookie etc. For your AuthorizationService, you'd need something like this:
public class AuthorizationService : IAuthorizationService
{
public bool Authorize(User userSession, Roles requiredRoles)
{
if (userSession.IsAdministrator)
{
return true;
}
else
{
// Check if the roles enum has the specific role bit set.
return (requiredRoles & user.Roles) == requiredRoles;
}
}
}
On top of these base services, you could easily add services to reset passwords etc.
Since you're using MVC, you could do authorization at the action level using an ActionFilter:
public class RequirePermissionFilter : IAuthorizationFilter
{
private readonly IAuthorizationService authorizationService;
private readonly Roles permissions;
public RequirePermissionFilter(IAuthorizationService authorizationService, Roles requiredRoles)
{
this.authorizationService = authorizationService;
this.permissions = requiredRoles;
this.isAdministrator = isAdministrator;
}
private IAuthorizationService CreateAuthorizationService(HttpContextBase httpContext)
{
return this.authorizationService ?? new FormsAuthorizationService(httpContext);
}
public void OnAuthorization(AuthorizationContext filterContext)
{
var authSvc = this.CreateAuthorizationService(filterContext.HttpContext);
// Get the current user... you could store in session or the HttpContext if you want too. It would be set inside the FormsAuthenticationService.
var userSession = (User)filterContext.HttpContext.Session["CurrentUser"];
var success = authSvc.Authorize(userSession, this.permissions);
if (success)
{
// Since authorization is performed at the action level, the authorization code runs
// after the output caching module. In the worst case this could allow an authorized user
// to cause the page to be cached, then an unauthorized user would later be served the
// cached page. We work around this by telling proxies not to cache the sensitive page,
// then we hook our custom authorization code into the caching mechanism so that we have
// the final say on whether or not a page should be served from the cache.
var cache = filterContext.HttpContext.Response.Cache;
cache.SetProxyMaxAge(new TimeSpan(0));
cache.AddValidationCallback((HttpContext context, object data, ref HttpValidationStatus validationStatus) =>
{
validationStatus = this.OnCacheAuthorization(new HttpContextWrapper(context));
}, null);
}
else
{
this.HandleUnauthorizedRequest(filterContext);
}
}
private void HandleUnauthorizedRequest(AuthorizationContext filterContext)
{
// Ajax requests will return status code 500 because we don't want to return the result of the
// redirect to the login page.
if (filterContext.RequestContext.HttpContext.Request.IsAjaxRequest())
{
filterContext.Result = new HttpStatusCodeResult(500);
}
else
{
filterContext.Result = new HttpUnauthorizedResult();
}
}
public HttpValidationStatus OnCacheAuthorization(HttpContextBase httpContext)
{
var authSvc = this.CreateAuthorizationService(httpContext);
var userSession = (User)httpContext.Session["CurrentUser"];
var success = authSvc.Authorize(userSession, this.permissions);
if (success)
{
return HttpValidationStatus.Valid;
}
else
{
return HttpValidationStatus.IgnoreThisRequest;
}
}
}
Which you can then decorate on your controller actions:
[RequirePermission(Roles.Accounting)]
public ViewResult Index()
{
// ...
}
The advantage of this approach is you can also use dependency injection and an IoC container to wire things up. Also, you can use it across multiple applications (not just your ASP.NET one). You would use your ORM to define the appropriate schema.
If you need more details around the FormsAuthorization/Authentication services or where to go from here, let me know.
EDIT: To add "security trimming", you could do it with an HtmlHelper. This probably needs a little more... but you get the idea.
public static bool SecurityTrim<TModel>(this HtmlHelper<TModel> source, Roles requiredRoles)
{
var authorizationService = new FormsAuthorizationService();
var user = (User)HttpContext.Current.Session["CurrentUser"];
return authorizationService.Authorize(user, requiredRoles);
}
And then inside your view (using Razor syntax here):
#if(Html.SecurityTrim(Roles.Accounting))
{
<span>Only for accounting</span>
}
EDIT: The UserSession would look something like this:
public class UserSession
{
public int UserId { get; set; }
public string UserName { get; set; }
public bool IsAdministrator { get; set; }
public Roles GetRoles()
{
// make the call to the database or whatever here.
// or just turn this into a property.
}
}
This way, we don't expose the password hash and all other details inside the session of the current user since they're really not needed for the user's session lifetime.
I have implemented a role provider based on #TheCloudlessSky post here. There are few things that I thought I can add and share what I have done.
First if you want to use the RequirepPermission class for your action filters as an attribute you need to implement ActionFilterAttribute class for RequirepPermission class.
Interface classes IAuthenticationService and IAuthorizationService
public interface IAuthenticationService
{
void SignIn(string userName, bool createPersistentCookie);
void SignOut();
}
public interface IAuthorizationService
{
bool Authorize(UserSession user, string[] requiredRoles);
}
FormsAuthenticationService class
/// <summary>
/// This class is for Form Authentication
/// </summary>
public class FormsAuthenticationService : IAuthenticationService
{
public void SignIn(string userName, bool createPersistentCookie)
{
if (String.IsNullOrEmpty(userName)) throw new ArgumentException(#"Value cannot be null or empty.", "userName");
FormsAuthentication.SetAuthCookie(userName, createPersistentCookie);
}
public void SignOut()
{
FormsAuthentication.SignOut();
}
}
UserSession calss
public class UserSession
{
public string UserName { get; set; }
public IEnumerable<string> UserRoles { get; set; }
}
Another point is FormsAuthorizationServiceclass and how we can assign a user to the httpContext.Session["CurrentUser"]. My Approach in this situation is to create a new instance of userSession class and directly assign the user from httpContext.User.Identity.Name to the userSession variable as you can see in FormsAuthorizationService class.
[AttributeUsageAttribute(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Constructor | AttributeTargets.Method, Inherited = false)]
public class RequirePermissionAttribute : ActionFilterAttribute, IAuthorizationFilter
{
#region Fields
private readonly IAuthorizationService _authorizationService;
private readonly string[] _permissions;
#endregion
#region Constructors
public RequirePermissionAttribute(string requiredRoles)
{
_permissions = requiredRoles.Trim().Split(',').ToArray();
_authorizationService = null;
}
#endregion
#region Methods
private IAuthorizationService CreateAuthorizationService(HttpContextBase httpContext)
{
return _authorizationService ?? new FormsAuthorizationService(httpContext);
}
public void OnAuthorization(AuthorizationContext filterContext)
{
var authSvc = CreateAuthorizationService(filterContext.HttpContext);
// Get the current user... you could store in session or the HttpContext if you want too. It would be set inside the FormsAuthenticationService.
if (filterContext.HttpContext.Session == null) return;
if (filterContext.HttpContext.Request == null) return;
var success = false;
if (filterContext.HttpContext.Session["__Roles"] != null)
{
var rolesSession = filterContext.HttpContext.Session["__Roles"];
var roles = rolesSession.ToString().Trim().Split(',').ToList();
var userSession = new UserSession
{
UserName = filterContext.HttpContext.User.Identity.Name,
UserRoles = roles
};
success = authSvc.Authorize(userSession, _permissions);
}
if (success)
{
// Since authorization is performed at the action level, the authorization code runs
// after the output caching module. In the worst case this could allow an authorized user
// to cause the page to be cached, then an unauthorized user would later be served the
// cached page. We work around this by telling proxies not to cache the sensitive page,
// then we hook our custom authorization code into the caching mechanism so that we have
// the final say on whether or not a page should be served from the cache.
var cache = filterContext.HttpContext.Response.Cache;
cache.SetProxyMaxAge(new TimeSpan(0));
cache.AddValidationCallback((HttpContext context, object data, ref HttpValidationStatus validationStatus) =>
{
validationStatus = OnCacheAuthorization(new HttpContextWrapper(context));
}, null);
}
else
{
HandleUnauthorizedRequest(filterContext);
}
}
private static void HandleUnauthorizedRequest(AuthorizationContext filterContext)
{
// Ajax requests will return status code 500 because we don't want to return the result of the
// redirect to the login page.
if (filterContext.RequestContext.HttpContext.Request.IsAjaxRequest())
{
filterContext.Result = new HttpStatusCodeResult(500);
}
else
{
filterContext.Result = new HttpUnauthorizedResult();
}
}
private HttpValidationStatus OnCacheAuthorization(HttpContextBase httpContext)
{
var authSvc = CreateAuthorizationService(httpContext);
if (httpContext.Session != null)
{
var success = false;
if (httpContext.Session["__Roles"] != null)
{
var rolesSession = httpContext.Session["__Roles"];
var roles = rolesSession.ToString().Trim().Split(',').ToList();
var userSession = new UserSession
{
UserName = httpContext.User.Identity.Name,
UserRoles = roles
};
success = authSvc.Authorize(userSession, _permissions);
}
return success ? HttpValidationStatus.Valid : HttpValidationStatus.IgnoreThisRequest;
}
return 0;
}
#endregion
}
internal class FormsAuthorizationService : IAuthorizationService
{
private readonly HttpContextBase _httpContext;
public FormsAuthorizationService(HttpContextBase httpContext)
{
_httpContext = httpContext;
}
public bool Authorize(UserSession userSession, string[] requiredRoles)
{
return userSession.UserRoles.Any(role => requiredRoles.Any(item => item == role));
}
}
then in your controller after the user is authenticated you can get roles from the database and assign it to the roles session:
var roles = Repository.GetRolesByUserId(Id);
if (ControllerContext.HttpContext.Session != null)
ControllerContext.HttpContext.Session.Add("__Roles",roles);
FormsService.SignIn(collection.Name, true);
After the user is logged out of the system you can clear the session
FormsService.SignOut();
Session.Abandon();
return RedirectToAction("Index", "Account");
The caveat in this model is that, when the user is signed into the system, if a role is assigned to the user, authorization doesn't work unless he logs out and logs back in the system.
Another thing is that there is no need to have a separate class for roles, since we can get roles directly from database and set it into roles session in a controller.
After you are done with implementing all these codes one last step is to bind this attribute to your methods in your controller:
[RequirePermission("Admin,DM")]
public ActionResult Create()
{
return View();
}
If you use Castle Windsor Dependency Injection you can inject lists of RoleProviders that can be used to ascertain user rights from any source you choose to implement.
http://ivida.co.uk/2011/05/18/mvc-getting-user-roles-from-multiple-sources-register-and-resolve-arrays-of-dependencis-using-the-fluent-api/
You don't need to use a static class for roles. For instance, the SqlRoleProvider allows you to define the roles in a database.
Of course, if you want to retrieve roles from your own service layer, it's not that hard to create your own role provider - there really aren't that many methods to implement.
You can implement your own membership and role providers by overriding the appropriate interfaces.
If you want to start from scratch, typically these types of things are implemented as a custom http module which stores the users credentials either in the httpcontext or the session. Either way you'll probably want to set a cookie with some sort of authentication token.
Related
How are ASP.NET Roles used with Authorization?
I'm using ASP.NET Core and hosting what is basically the default template with Windows Authentication enabled. I'm hosting this on a dedicated IIS server, and have verified the app is receiving correct information from AD and it correctly authenticates my session. I feel like I'm trying to do something very simple. If the user is in the security group (from AD) "Admin" they are able to access a specific function. If they aren't in that group they do not get access. I slapped on the [Authorize] attribute to the service (in ConfigureServices) services.AddAuthentication(IISDefaults.AuthenticationScheme); (in Configure) app.UseAuthorization(); (in service) [Authorize] public class SiteService { private readonly string _route; private readonly HttpClient _httpClient; public SiteService(HttpClient httpClient) { _httpClient = httpClient; _route = httpClient.BaseAddress.AbsoluteUri; } public async Task<IEnumerable<Site>> GetSites() { } } I can see in the logs that accessing the service gives me Domain/User. I then looked up the MS Docs here: https://learn.microsoft.com/en-us/aspnet/core/security/authorization/roles?view=aspnetcore-3.1 And slapped on [Authorize(Roles = "Admin"). That worked. I then switched "Admin" with "sldkfjslksdlfkj". Nothing changed...I can still access the service. Why is the Roles="x" check not working? How can I enable a relatively simple check to AD for a Security Group?
You could write a custom Policy Authorization handlers to check all of the users' ADGroups and check if they contain the desired group name. Refer to the following: 1.Create CheckADGroupRequirement(accept a parameter) public class CheckADGroupRequirement : IAuthorizationRequirement { public string GroupName { get; private set; } public CheckADGroupRequirement(string groupName) { GroupName = groupName; } } 2.Create CheckADGroupHandler public class CheckADGroupHandler : AuthorizationHandler<CheckADGroupRequirement> { protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, CheckADGroupRequirement requirement) { //var isAuthorized = context.User.IsInRole(requirement.GroupName); var groups = new List<string>();//save all your groups' name var wi = (WindowsIdentity)context.User.Identity; if (wi.Groups != null) { foreach (var group in wi.Groups) { try { groups.Add(group.Translate(typeof(NTAccount)).ToString()); } catch (Exception e) { // ignored } } if(groups.Contains(requirement.GroupName))//do the check { context.Succeed(requirement); } } return Task.CompletedTask; } } 3.Register Handler in ConfigureServices services.AddAuthorization(options => { options.AddPolicy("AdminOnly", policy => policy.Requirements.Add(new CheckADGroupRequirement("DOMAIN\\Domain Admin")));//set your desired group name //other policies }); services.AddSingleton<IAuthorizationHandler, CheckADGroupHandler>(); 4.Use on controller/service [Authorize(Policy = "AdminOnly")] public class SiteService
How to force update custom user claims?
I've got a custom claim added to my ApplicationUser class as follows: public class ApplicationUser : IdentityUser { public async Task<ClaimsIdentity> GenerateUserIdentityAsync(UserManager<ApplicationUser> manager) { var userIdentity = await manager.CreateIdentityAsync(this, DefaultAuthenticationTypes.ApplicationCookie); // Add custom user claims here if(Theme != null) userIdentity.AddClaim(new Claim("ThemeBundle", Theme.Bundle)); return userIdentity; } public int? ThemeId { get; set; } [ForeignKey("ThemeId")] public virtual Theme Theme { get; set; } } I extended Identity like this: public static class IdentityExtensions { public static string GetThemeBundle(this IIdentity identity) { var claim = ((ClaimsIdentity) identity).FindFirst("ThemeBundle"); // Test for null to avoid issues during testing return (claim != null) ? claim.Value : string.Empty; } } I update the model behind the claim from the following Action Method: public ActionResult ChangeTheme(int id) { var theme = _db.Themes.Find(id); if (theme == null) return HttpNotFound(); var userId = User.Identity.GetUserId(); var user = _db.Users.Find(userId); user.Theme = theme; _db.SaveChanges(); return RedirectToAction("Index", "Home"); } I access it in a view (or elsewhere) like this: User.Identity.GetThemeBundle() When the user updates their "Theme" property with the "ChangeTheme" action, nothing happens until they log off and log back in. I've spent all day mulling over more than the following QA's with no good result: Update User Claim not Taking Effect. Why? MVC 5 AddToRole requires logout before it works? And thanks to #Brendan Green: ASP.NET Identity - Forcing a re-login with security stamp So far the best I've got is that the page will refresh and the claim returns an empty string instead of the desired result, OR I redirect the user to the login screen. At least those are less ambiguous than nothing happening. How on earth can I get the claim to update globally as soon as the user changes their "Theme" property? I'd settle for a good way to fully log the user off and back on if needed. Using the AuthenticationManager.Signout and .Signin methods doesn't quite do the trick.
As of Asp.Net MVC 6 and Asp.Identity 3.0.0-rc1-final you could use Task SignInManager<TUser>.RefreshSignInAsync(TUser user); in order to do that.
How to handle session information in an Asp.net MVC app with dependency injection
I have a multitenant application where I want to keep track of two things across the app: The user and The tenant. I have this three cases: Anonymous Users: In this case user is null, and tenant is tracked by querystring. Authenticated Users: user comes from cookie, tenant is saved in session. Jobs: I have jobs in my app to do some work, the user in this case is null and the tenant is set manually. In all my services I use the user and the tenant. I tought at first of using an object per http session in my DI container, but this wouldn't work with the Jobs. Any toughts on how can I handle this information, I'm currently using it in a Session variable, but I have many problems with this implementation, and I need to have a lot of special cases to handle jobs and unauthenticated users.
The least you should do is abstracting access to user context. For instance: public interface IUserContext { IPrincipal User { get; } Tenant Tenant { get; } } You might want to have a separate ITenantContext abstraction, but let's stick to one for now. Your system will probably have two applications: windows service that runs the jobs, and a web application that handles user interaction. Both applications have their own entry point, their own Composition Root, and their own unique DI configuration. For the web application, I imagine the implementation to look as follows: public AspNetUserContext : IUserContext { private readonly ITenantRepository tenantRepository; public AspNetUserContext(ITenantRepository tenantRepository) { this.tenantRepository = tenantRepository; } public IPrincipal User { get { return HttpContext.Current.User; } } public Tenant Tenant { get { if (this.User.Identity.IsAuthenticated) { return this.tenantRepository.GetByName( HttpContext.Current.Session["tenant"]); } else { return this.tenantRepository.GetByName( HttpContext.Current.Request.QueryString["tenant"]); } } } } For the job service, things might look completely diffetent at first, but with the service, the processing of one job can be considered a request. So that means that when the request start, you need to set the context (something that ASP.NET does for us in the backgroudn). The IUserContext might look as follows: public JobServiceUserContext : IUserContext { [ThreadStatic] private static IPrinciple user; [ThreadStatic] private static Tenant tenant; public IPrincipal User { get { return this.user; } set { this.user = user; } } public IPrincipal User { get { return this.tenant; } set { this.tenant = tenant; } } } Now the execution of a job can be wrapped with some logic that sets the proper context, for instance: public class JobRunner { private readonly JobServiceUserContext context; private readonly IJobDactory jobFactory; public JobServiceUserContext(JobServiceUserContext context, IJobDactory jobFactory) { this.context = context; this.jobFactory = jobFactory; } public void RunJob(JobDetails job) { try { this.context.User = job.User; this.context.Tenant = job.Tenant; IJob job = this.jobFactory.Create(job.Type); job.Execute(job.Data); Activator.CreateInsta } finally { // Reset this.context.Tenant = null; this.context.User = null; } } } UPDATE In case they are both running within the same application, and the jobs are running on a background thread, you can introduce a proxy implementation for IUserContext that transparently switches to the right implementation. For instance: public SelectingUserContextProxy : IUserContext { private readonly Func<bool> selector; private readonly IUserContext trueContext; private readonly IUserContext falseContext; public SelectingUserContextProxy(Func<bool> selector, IUserContext trueContext, IUserContext falseContext) { this.selector = selector; this.trueContext = trueContext; this.falseContext = falseContext; } public IPrincipal User { get { return this.Context.User; } } public Tenant Tenant { get { return this.Context.Tenant; } } private IUserContext Context { get { return selector() ? trueContext : falseContext; } } } And you can register this as follows: var jobContext = new JobServiceUserContext(); container.RegisterSingle<IUserContext>( new SelectingUserContextProxy( () => HttpContext.Current != null, trueContext: new AspNetUserContext(), falseContext: jobContext));
ASP.NET MVC3: custom [authorise] attribute
In my database, the system user has a list of modules he/she can access. I would like to be able to add an authorise attribute which checks that this is the case. E.g. [authorise(UserID, ControllerName)] Which goes to some code, ensures that the User with UserID specified, has the controller name in his/her list. At the moment you can simply bypass the fact the tabs aren't visible, by using the URL. (I have code which already checks if the user has specified access and hides/shows tabs)
public class MyAuthorizeAttribute : AuthorizeAttribute { protected override bool AuthorizeCore(HttpContextBase httpContext) { var isAuthorized = base.AuthorizeCore(httpContext); if (!isAuthorized) { return false; } string currentUser = httpContext.User.Identity.Name; string currentController = httpContext.Request.RequestContext.RouteData.GetRequiredString("controller"); // TODO: go hit your database and see if currentUser can access // currentController and return true/false from here ... } } then decorate your controllers or actions: [MyAuthorize] public class FooController: Controller { ... } This being said I suspect that you might have gone the wrong way in your database design by storing a list of which user has access to access which controller action. Probably you should have used roles for that. Having the database know about controllers just feels wrong. So: [Authorize(Roles = "Foo,Bar")] public class FooController: Controller { ... } Only users that have the Foo or Bar role can access the FooController.
You can create a new attribute. [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)] public class CustomAuthorizeAttribute : FilterAttribute, IAuthorizationFilter { public void OnAuthorization(AuthorizationContext filterContext) { if (filterContext == null) { throw new ArgumentNullException("filterContext"); } if (!filterContext.HttpContext.User.Identity.IsAuthenticated) { filterContext.Result = new HttpUnauthorizedResult(); } bool authorized = // Perform custom logic If(!authorized) { filterContext.Result = new RedirectResult(/* Your access denied url */); } } }
When you create the model, check the permission DisplayAdminLink = _permissionService.Authorize(StandardPermissionProvider.AccessAdminPanel), and in the view #if (Model.DisplayAdminLink) { <li>#T("Account.Administration") </li> }
ASP.NET MVC - Set custom IIdentity or IPrincipal
I need to do something fairly simple: in my ASP.NET MVC application, I want to set a custom IIdentity / IPrincipal. Whichever is easier / more suitable. I want to extend the default so that I can call something like User.Identity.Id and User.Identity.Role. Nothing fancy, just some extra properties. I've read tons of articles and questions but I feel like I'm making it harder than it actually is. I thought it would be easy. If a user logs on, I want to set a custom IIdentity. So I thought, I will implement Application_PostAuthenticateRequest in my global.asax. However, that is called on every request, and I don't want to do a call to the database on every request which would request all the data from the database and put in a custom IPrincipal object. That also seems very unnecessary, slow, and in the wrong place (doing database calls there) but I could be wrong. Or where else would that data come from? So I thought, whenever a user logs in, I can add some necessary variables in my session, which I add to the custom IIdentity in the Application_PostAuthenticateRequest event handler. However, my Context.Session is null there, so that is also not the way to go. I've been working on this for a day now and I feel I'm missing something. This shouldn't be too hard to do, right? I'm also a bit confused by all the (semi)related stuff that comes with this. MembershipProvider, MembershipUser, RoleProvider, ProfileProvider, IPrincipal, IIdentity, FormsAuthentication.... Am I the only one who finds all this very confusing? If someone could tell me a simple, elegant, and efficient solution to store some extra data on a IIdentity without all the extra fuzz.. that would be great! I know there are similar questions on SO but if the answer I need is in there, I must've overlooked.
Here's how I do it. I decided to use IPrincipal instead of IIdentity because it means I don't have to implement both IIdentity and IPrincipal. Create the interface interface ICustomPrincipal : IPrincipal { int Id { get; set; } string FirstName { get; set; } string LastName { get; set; } } CustomPrincipal public class CustomPrincipal : ICustomPrincipal { public IIdentity Identity { get; private set; } public bool IsInRole(string role) { return false; } public CustomPrincipal(string email) { this.Identity = new GenericIdentity(email); } public int Id { get; set; } public string FirstName { get; set; } public string LastName { get; set; } } CustomPrincipalSerializeModel - for serializing custom information into userdata field in FormsAuthenticationTicket object. public class CustomPrincipalSerializeModel { public int Id { get; set; } public string FirstName { get; set; } public string LastName { get; set; } } LogIn method - setting up a cookie with custom information if (Membership.ValidateUser(viewModel.Email, viewModel.Password)) { var user = userRepository.Users.Where(u => u.Email == viewModel.Email).First(); CustomPrincipalSerializeModel serializeModel = new CustomPrincipalSerializeModel(); serializeModel.Id = user.Id; serializeModel.FirstName = user.FirstName; serializeModel.LastName = user.LastName; JavaScriptSerializer serializer = new JavaScriptSerializer(); string userData = serializer.Serialize(serializeModel); FormsAuthenticationTicket authTicket = new FormsAuthenticationTicket( 1, viewModel.Email, DateTime.Now, DateTime.Now.AddMinutes(15), false, userData); string encTicket = FormsAuthentication.Encrypt(authTicket); HttpCookie faCookie = new HttpCookie(FormsAuthentication.FormsCookieName, encTicket); Response.Cookies.Add(faCookie); return RedirectToAction("Index", "Home"); } Global.asax.cs - Reading cookie and replacing HttpContext.User object, this is done by overriding PostAuthenticateRequest 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(); CustomPrincipalSerializeModel serializeModel = serializer.Deserialize<CustomPrincipalSerializeModel>(authTicket.UserData); CustomPrincipal newUser = new CustomPrincipal(authTicket.Name); newUser.Id = serializeModel.Id; newUser.FirstName = serializeModel.FirstName; newUser.LastName = serializeModel.LastName; HttpContext.Current.User = newUser; } } Access in Razor views #((User as CustomPrincipal).Id) #((User as CustomPrincipal).FirstName) #((User as CustomPrincipal).LastName) and in code: (User as CustomPrincipal).Id (User as CustomPrincipal).FirstName (User as CustomPrincipal).LastName I think the code is self-explanatory. If it isn't, let me know. Additionally to make the access even easier you can create a base controller and override the returned User object (HttpContext.User): public class BaseController : Controller { protected virtual new CustomPrincipal User { get { return HttpContext.User as CustomPrincipal; } } } and then, for each controller: public class AccountController : BaseController { // ... } which will allow you to access custom fields in code like this: User.Id User.FirstName User.LastName But this will not work inside views. For that you would need to create a custom WebViewPage implementation: public abstract class BaseViewPage : WebViewPage { public virtual new CustomPrincipal User { get { return base.User as CustomPrincipal; } } } public abstract class BaseViewPage<TModel> : WebViewPage<TModel> { public virtual new CustomPrincipal User { get { return base.User as CustomPrincipal; } } } Make it a default page type in Views/web.config: <pages pageBaseType="Your.Namespace.BaseViewPage"> <namespaces> <add namespace="System.Web.Mvc" /> <add namespace="System.Web.Mvc.Ajax" /> <add namespace="System.Web.Mvc.Html" /> <add namespace="System.Web.Routing" /> </namespaces> </pages> and in views, you can access it like this: #User.FirstName #User.LastName
I can't speak directly for ASP.NET MVC, but for ASP.NET Web Forms, the trick is to create a FormsAuthenticationTicket and encrypt it into a cookie once the user has been authenticated. This way, you only have to call the database once (or AD or whatever you are using to perform your authentication), and each subsequent request will authenticate based on the ticket stored in the cookie. A good article on this: http://www.ondotnet.com/pub/a/dotnet/2004/02/02/effectiveformsauth.html (broken link) Edit: Since the link above is broken, I would recommend LukeP's solution in his answer above: https://stackoverflow.com/a/10524305 - I would also suggest that the accepted answer be changed to that one. Edit 2: An alternative for the broken link: https://web.archive.org/web/20120422011422/http://ondotnet.com/pub/a/dotnet/2004/02/02/effectiveformsauth.html
Here is an example to get the job done. bool isValid is set by looking at some data store (lets say your user data base). UserID is just an ID i am maintaining. You can add aditional information like email address to user data. protected void btnLogin_Click(object sender, EventArgs e) { //Hard Coded for the moment bool isValid=true; if (isValid) { string userData = String.Empty; userData = userData + "UserID=" + userID; FormsAuthenticationTicket ticket = new FormsAuthenticationTicket(1, username, DateTime.Now, DateTime.Now.AddMinutes(30), true, userData); string encTicket = FormsAuthentication.Encrypt(ticket); HttpCookie faCookie = new HttpCookie(FormsAuthentication.FormsCookieName, encTicket); Response.Cookies.Add(faCookie); //And send the user where they were heading string redirectUrl = FormsAuthentication.GetRedirectUrl(username, false); Response.Redirect(redirectUrl); } } in the golbal asax add the following code to retrive your information protected void Application_AuthenticateRequest(Object sender, EventArgs e) { HttpCookie authCookie = Request.Cookies[ FormsAuthentication.FormsCookieName]; if(authCookie != null) { //Extract the forms authentication cookie FormsAuthenticationTicket authTicket = FormsAuthentication.Decrypt(authCookie.Value); // Create an Identity object //CustomIdentity implements System.Web.Security.IIdentity CustomIdentity id = GetUserIdentity(authTicket.Name); //CustomPrincipal implements System.Web.Security.IPrincipal CustomPrincipal newUser = new CustomPrincipal(); Context.User = newUser; } } When you are going to use the information later, you can access your custom principal as follows. (CustomPrincipal)this.User or (CustomPrincipal)this.Context.User this will allow you to access custom user information.
MVC provides you with the OnAuthorize method that hangs from your controller classes. Or, you could use a custom action filter to perform authorization. MVC makes it pretty easy to do. I posted a blog post about this here. http://www.bradygaster.com/post/custom-authentication-with-mvc-3.0
Here is a solution if you need to hook up some methods to #User for use in your views. No solution for any serious membership customization, but if the original question was needed for views alone then this perhaps would be enough. The below was used for checking a variable returned from a authorizefilter, used to verify if some links wehere to be presented or not(not for any kind of authorization logic or access granting). using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Security.Principal; namespace SomeSite.Web.Helpers { public static class UserHelpers { public static bool IsEditor(this IPrincipal user) { return null; //Do some stuff } } } Then just add a reference in the areas web.config, and call it like below in the view. #User.IsEditor()
Based on LukeP's answer, and add some methods to setup timeout and requireSSL cooperated with Web.config. The references links MSDN, Explained: Forms Authentication in ASP.NET 2.0 MSDN, FormsAuthentication Class SO, .net Access Forms authentication “timeout” value in code Modified Codes of LukeP 1, Set timeout based on Web.Config. The FormsAuthentication.Timeout will get the timeout value, which is defined in web.config. I wrapped the followings to be a function, which return a ticket back. int version = 1; DateTime now = DateTime.Now; // respect to the `timeout` in Web.config. TimeSpan timeout = FormsAuthentication.Timeout; DateTime expire = now.Add(timeout); bool isPersist = false; FormsAuthenticationTicket ticket = new FormsAuthenticationTicket( version, name, now, expire, isPersist, userData); 2, Configure the cookie to be secure or not, based on the RequireSSL configuration. HttpCookie faCookie = new HttpCookie(FormsAuthentication.FormsCookieName, encTicket); // respect to `RequreSSL` in `Web.Config` bool bSSL = FormsAuthentication.RequireSSL; faCookie.Secure = bSSL;
As an addition to LukeP code for Web Forms users (not MVC) if you want to simplify the access in the code behind of your pages, just add the code below to a base page and derive the base page in all your pages: Public Overridable Shadows ReadOnly Property User() As CustomPrincipal Get Return DirectCast(MyBase.User, CustomPrincipal) End Get End Property So in your code behind you can simply access: User.FirstName or User.LastName What I'm missing in a Web Form scenario, is how to obtain the same behaviour in code not tied to the page, for example in httpmodules should I always add a cast in each class or is there a smarter way to obtain this? Thanks for your answers and thank to LukeP since I used your examples as a base for my custom user (which now has User.Roles, User.Tasks, User.HasPath(int) , User.Settings.Timeout and many other nice things)
All right, so i'm a serious cryptkeeper here by dragging up this very old question, but there is a much simpler approach to this, which was touched on by #Baserz above. And that is to use a combination of C# Extension methods and caching (Do NOT use session). In fact, Microsoft has already provided a number of such extensions in the Microsoft.AspNet.Identity.IdentityExtensions namespace. For instance, GetUserId() is an extension method that returns the user Id. There is also GetUserName() and FindFirstValue(), which returns claims based on the IPrincipal. So you need only include the namespace, and then call User.Identity.GetUserName() to get the users name as configured by ASP.NET Identity. I'm not certain if this is cached, since the older ASP.NET Identity is not open sourced, and I haven't bothered to reverse engineer it. However, if it's not then you can write your own extension method, that will cache this result for a specific amount of time.
I tried the solution suggested by LukeP and found that it doesn't support the Authorize attribute. So, I modified it a bit. public class UserExBusinessInfo { public int BusinessID { get; set; } public string Name { get; set; } } public class UserExInfo { public IEnumerable<UserExBusinessInfo> BusinessInfo { get; set; } public int? CurrentBusinessID { get; set; } } public class PrincipalEx : ClaimsPrincipal { private readonly UserExInfo userExInfo; public UserExInfo UserExInfo => userExInfo; public PrincipalEx(IPrincipal baseModel, UserExInfo userExInfo) : base(baseModel) { this.userExInfo = userExInfo; } } public class PrincipalExSerializeModel { public UserExInfo UserExInfo { get; set; } } public static class IPrincipalHelpers { public static UserExInfo ExInfo(this IPrincipal #this) => (#this as PrincipalEx)?.UserExInfo; } [HttpPost] [AllowAnonymous] [ValidateAntiForgeryToken] public async Task<ActionResult> Login(LoginModel details, string returnUrl) { if (ModelState.IsValid) { AppUser user = await UserManager.FindAsync(details.Name, details.Password); if (user == null) { ModelState.AddModelError("", "Invalid name or password."); } else { ClaimsIdentity ident = await UserManager.CreateIdentityAsync(user, DefaultAuthenticationTypes.ApplicationCookie); AuthManager.SignOut(); AuthManager.SignIn(new AuthenticationProperties { IsPersistent = false }, ident); user.LastLoginDate = DateTime.UtcNow; await UserManager.UpdateAsync(user); PrincipalExSerializeModel serializeModel = new PrincipalExSerializeModel(); serializeModel.UserExInfo = new UserExInfo() { BusinessInfo = await db.Businesses .Where(b => user.Id.Equals(b.AspNetUserID)) .Select(b => new UserExBusinessInfo { BusinessID = b.BusinessID, Name = b.Name }) .ToListAsync() }; JavaScriptSerializer serializer = new JavaScriptSerializer(); string userData = serializer.Serialize(serializeModel); FormsAuthenticationTicket authTicket = new FormsAuthenticationTicket( 1, details.Name, DateTime.Now, DateTime.Now.AddMinutes(15), false, userData); string encTicket = FormsAuthentication.Encrypt(authTicket); HttpCookie faCookie = new HttpCookie(FormsAuthentication.FormsCookieName, encTicket); Response.Cookies.Add(faCookie); return RedirectToLocal(returnUrl); } } return View(details); } And finally in 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(); PrincipalExSerializeModel serializeModel = serializer.Deserialize<PrincipalExSerializeModel>(authTicket.UserData); PrincipalEx newUser = new PrincipalEx(HttpContext.Current.User, serializeModel.UserExInfo); HttpContext.Current.User = newUser; } } Now I can access the data in views and controllers simply by calling User.ExInfo() To log out I just call AuthManager.SignOut(); where AuthManager is HttpContext.GetOwinContext().Authentication