📜 ⬆️ ⬇️

Configurable authorization in Asp.Net MVC

Hi Habrayuzer! I would like to share with the community my little development experience on the ASP.NET MVC framework. Namely, a very important part of user authentication in the application. As well as the implementation of a security system based on roles.
This article will most likely be useful to novice programmers using ASP.NET MVC as a development platform. But it is also possible for “experienced” (experienced) users to find some ideas for themselves. Criticism, suggestions and the like are welcome. All interested in asking under the cat.

So, our goal is to write custom (custom) authentication, with a small role-based security system. It will not require changes or additions to the application domain model. The main requirement is any hint at users and the role system. We will not consider and even use the standard security system based on membership providers. It seems to me not at all convenient. Its main disadvantage is the need for a specific data scheme, which is often difficult to associate with the application domain.

A bit of theory.


Let's start with a small theoretical part. In the ASP.NET MVC platform, there are several types of authentication provided out of the box.


In a client-side form authentication, it stores an encrypted cookie set. The cookie is transmitted in requests to the server, indicating that the user is authorized. To create such an encrypted set, in standard form authentication, use the Encript method in the System.Web.Security.FormAuthentication class. Decrypt is used for decoding. What is the FormAuthentication class good for? In that its encoding and decoding methods encrypt and sign data using the server's server keys. Without these keys, the information from the cookie cannot be read or modified, and we do not need to reinvent the wheel.
')

Project implementation


Let's start by creating a user identification class that will be available through the security information of the current Http HttpContext.User.Identity request.
Implementation
[Serializable] //TAccount -     . //TRole -  . public abstract class AbstractIdentity<TAccount, TRole>: MarshalByRefObject, IIdentity { protected AbstractIdentity() { Id = long.MinValue; } private bool _isInitialized = false; public long Id { get; set; } public string Name { get; set; } public string AuthenticationType { get { return String.Format("CustomizeAuthentication_{0}", typeof(TAccount).Name); } } public string[] Role { get; set; } public TRole[] Roles { get; set; } public bool IsAuthenticated { get { return Id != long.MinValue; } } public bool CheckRole(TRole role) { return Role.All(r => r.Equals(role.ToString())); } public void SetAccount(TAccount account) { Id = GetId(account); Name = GetName(account); Roles = GetRole(account); Role = Roles.Select(c=>c.ToString()).ToArray(); InitializeMoreFields(account); _isInitialized = true; } protected virtual void InitializeMoreFields(TAccount account) { } protected abstract long GetId(TAccount account); protected abstract string GetName(TAccount account); protected abstract TRole[] GetRole(TAccount account); public string Serialize() { if (!_isInitialized) throw new AccountNotSetException(); using (var stream = new MemoryStream()) { var formatter = new XmlSerializer(GetType()); formatter.Serialize(stream, this); return Encoding.UTF8.GetString(stream.ToArray()); } } public static TIdenty Deserialize<TIdenty>(string value) where TIdenty : AbstractIdentity<TAccount, TRole> { using (var stream = new MemoryStream(Encoding.UTF8.GetBytes(value))) { var formatter = new XmlSerializer(typeof(TIdenty)); return (TIdenty)formatter.Deserialize(stream); } } } 


Next, we implement an abstract HTTP authentication module. In which we subscribe to the AuthenticateRequest event, which occurs after passing the user's authentication. In the implementation of the signed method, we decade the cookie by creating and writing the user to the current HTTP request.
Implementation
 //TIdenty -      //TAccount -     . //TRole -  . public abstract class AbstractAutentificationModule<TIdenty, TAccount, TRole> : IHttpModule where TIdenty : AbstractIdentity<TAccount, TRole> { public void Init(HttpApplication context) { context.AuthenticateRequest += OnAuthenticateRequest; } private static void OnAuthenticateRequest(object sender, EventArgs e) { var application = (HttpApplication)sender; var context = application.Context; if (context.User != null && context.User.Identity.IsAuthenticated) return; var cookieName = FormsAuthentication.FormsCookieName; var cookie = application.Request.Cookies[cookieName.ToUpper()]; if (cookie == null) return; try { var ticket = FormsAuthentication.Decrypt(cookie.Value); var identity = AbstractIdentity<TAccount, TRole>.Deserialize<TIdenty>(ticket.UserData); var principal = new GenericPrincipal(identity, identity.Role); context.User = principal; Thread.CurrentPrincipal = principal; } catch {} } public void Dispose() {} } 


Implementing an abstract authorization service. Records data of the authorized user in the cookie. Add it to the HTTP request.
Implementation
 //TAccount -     . public interface IAuthorizeService<in TAccount> { void SignIn(TAccount account, bool createPersistentCookie); void SignOut(); } //TIdenty -     //TAccount -     . //TRole -  . public abstract class AbstractAuthorizeService<TIdentity, TAccount, TRole> : IAuthorizeService<TAccount> where TIdentity : AbstractIdentity<TAccount, TRole>, new() { private const int TICKET_VERSION = 1; private const int EXPIRATION_MINUTE = 60; public void SignIn(TAccount account, bool createPersistentCookie) { var accountIdentity = CreateIdentity(account); var authTicket = new FormsAuthenticationTicket(TICKET_VERSION, accountIdentity.Name, DateTime.Now, DateTime.Now.AddMinutes(EXPIRATION_MINUTE), createPersistentCookie, accountIdentity.Serialize()); CreateCookie(authTicket); HttpContext.Current.User = new GenericPrincipal(accountIdentity, accountIdentity.Role); } private TIdentity CreateIdentity(TAccount account) { var accountIdentity = new TIdentity(); accountIdentity.SetAccount(account); return accountIdentity; } private void CreateCookie(FormsAuthenticationTicket ticket) { var authCookie = new HttpCookie(FormsAuthentication.FormsCookieName, FormsAuthentication.Encrypt(ticket)) { Expires = DateTime.Now.Add(FormsAuthentication.Timeout), }; HttpContext.Current.Response.Cookies.Add(authCookie); } public void SignOut() { FormsAuthentication.SignOut(); } } 


Do not forget about the security system. To do this, we implement the authentication attribute, to which we will transfer the roles and rules necessary for accessing the controller method. Since attributes do not support Generic, we delegate the creation of rules to another class (RuleFactory).
The IRule interface describes an authentication verification rule.
Implementation
 public interface IRule { bool Check(IIdentity user); } //     //TIdenty -     //TAccount -     . //TRole -  . internal class Rule<TIdentity, TAccount, TRole> : IRule where TIdentity : AbstractIdentity<TAccount, TRole> { private readonly Func<TIdentity, bool> _check; public Rule(Func<TIdentity, bool> check) { if (check == null) throw new ArgumentNullException("check"); _check = check; } public bool Check (IIdentity user) { return _check((TIdentity) user); } } //  //TIdenty -     //TAccount -     . //TRole -  . public class RuleFactory<TIdentity, TAccount, TRole> where TIdentity : AbstractIdentity<TAccount, TRole> { public IRule Create(Func<TIdentity, bool> rule) { return new Rule<TIdentity, TAccount, TRole> (rule); } } //  public abstract class AbstractAutintificateAttribute : AuthorizeAttribute { private readonly ICollection<IRule> _rules = new List<IRule> (); private readonly bool _isNotSimpleAuthentication; protected AbstractAutintificateAttribute(bool isNotSimpleAuthentication) { _isNotSimpleAuthentication = isNotSimpleAuthentication; } protected void AddRule(IRule rule) { if (rule == null) throw new ArgumentNullException ("rule"); _rules.Add (rule); } protected override bool AuthorizeCore(HttpContextBase httpContext) { if (httpContext == null) throw new ArgumentNullException("httpContext"); if (httpContext.User == null || !httpContext.User.Identity.IsAuthenticated) return false; var isAuthorize = false; isAuthorize |= _rules.Any(rule => rule.Check(httpContext.User.Identity)); isAuthorize |= httpContext.Request.IsAuthenticated && !_isNotSimpleAuthentication; return isAuthorize; } protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext) { var context = filterContext.HttpContext; var appPath = context.Request.ApplicationPath == "/" ? string.Empty : context.Request.ApplicationPath; var loginUrl = FormsAuthentication.LoginUrl; var path = HttpUtility.UrlEncode(context.Request.Url.PathAndQuery); var url = String.Format("{0}{1}?ReturnUrl={2}", appPath, loginUrl, path); if (!filterContext.IsChildAction) filterContext.Result = new RedirectResult(url); } } 


For ease of use in their projects, we implement a basic abstract controller. Which will provide convenient access to the current user.
Implementation
 //TIdenty -     //TAccount -     . //TRole -  . public abstract class AbstractController<TIdenty, TAccount, TRole> : Controller where TIdenty : AbstractIdentity<TAccount, TRole> { protected AbstractController() { _user = new Lazy<TIdenty>(() => HttpContext.User.Identity as TIdenty); } private readonly Lazy<TIdenty> _user; protected TIdenty CurrentUser { get { return _user.Value; } } } 


Use example


Basically, all the implementation for a specific project is to prescribe the necessary generic types.
The first thing we will start with in all projects is the implementation of the identification class.
Implementation
 public class ExampleIdentity : AbstractIdentity<Account, Role> { public string Email { get; set; } //  .     protected override long GetId(Account account) { return account.Id; } //  .   () protected override string GetName(Account account) { return account.Login; } //  .     protected override Role[] GetRole(Account account) { return new []{ account.Role }; } // .        .     protected override void InitializeMoreFields(Account account) { Email = account.Email; } } 


Implementation of the Authentication Module
Implementation
 public class ExampleAutintificateModule : AbstractAutentificationModule<ExampleIdentity, Account, Role> { } 

We are not forgetting to add your authorization module to Web.config, and change the authentication type there.
 <?xml version="1.0" encoding="utf-8"?> <configuration> .... <system.web> .... <httpModules> <remove name="FormsAuthentication" /> <add name="FormsAuthentication" type="Example.Infrostructure.ExampleAutintificateModule" /> </httpModules> <authentication mode="Forms"> <!--loginUrl     --> <forms loginUrl="~/User/Login" timeout="2880" /> </authentication> </system.web> </configuration> <system.webServer> .... <modules runAllManagedModulesForAllRequests="true"> <remove name="FormsAuthentication" /> <add name="FormsAuthentication" type="Example.Infrostructure.ExampleAutintificateModule" /> </modules> </system.webServer> 


An example of an authorization service.
Implementation
 public class ExampleAuthorizeService : AbstractAuthorizeService<ExampleIdentity, Account, Role> { } 


Basic controller for the current project
Implementation
 public abstract class ExampleController : AbstractController<ExampleIdentity, Account, Role> { } 


Well, we implement the attribute with the rules of authentication
Implementation
 //  public class ExampleRuleFactory : RuleFactory<ExampleIdentity, Account, Role> { } //     . public class ExampleAuthintificationAtribute : AbstractAutintificateAttribute { private readonly ExampleRuleFactory _ruleFactory = new ExampleRuleFactory(); public ExampleAuthintificationAtribute(params Role[] allowedRole) : base(allowedRole.Any()) { //    ,        AddRule(_ruleFactory.Create(account => allowedRole.Intersect(account.Roles).Any())); //  ,      AddRule(_ruleFactory.Create(account => account.Roles.Any(c=>c == Role.Admin))); } } 


Conclusion


For convenience, I created a nugget-package library, which can be installed with the following command:
 Install-Package MvcCustomizableFormAuthentication 

The repository with the project is on github , there is an example of a working application.
Thank you all for your time. Waiting for advice, criticism, suggestions.
PS Unforgettable about unit testing.

Source: https://habr.com/ru/post/174097/


All Articles