ASP.NET Identity and Windows Authorization in ASP.NET MVC

To begin with, I’ll tell you that the application that I developed existed for a long time on a small “desktop” server in the form of a prototype, which was used by a small number of employees at work. After some time, the management decided to replicate this application in the prom – with the transfer to the prom server and the organization of access to it for employees Total structural unit.

Naturally, as always happens, the support gave us a list of requirements that applications hosted on prom servers must meet. One of these requirements was the implementation of Windows account authorization, and the old login/password authorization could not be used. About what pitfalls we encountered during the implementation of such a seemingly simple feature, and how we solved them, will be discussed in this post. As I mentioned earlier, at the starting point of this story, we had a classic MVC application. Information about users, their roles (Admin, Common) and access to certain actions and procedures was stored in the MS SQL database. Simplified, the structure of this database segment can be represented as follows:

By the name of the tables, you can guess that in the application itself, this bunch of tables was captured by Entity Framework 6, and then used by the ASP.NET Identity subsystem. At the beginning of the session, the user was presented with a login form in which he entered his credentials, after which he was redirected to the application home page. Further, based on what accesses this user has in the database, and what privileges he has, the system adjusted the UI to this data.

Authorization was implemented using HTML forms by using the standard Html.BeginForm helper, which sends the entered data when the Submit button is pressed. Here’s what it looked like in terms of code:

@using (Html.BeginForm("Login", "Auth", FormMethod.Post, new { @class = "form-signin" }))
{
    @Html.AntiForgeryToken()
    <div class="form-group form-ie">
        <span class="oi oi-person"></span>
        @Html.TextBoxFor(x => x.Login, new { @class = "form-control", @placeholder = "Логин", @id = "username" })
        @Html.ValidationMessageFor(x => x.Login)
    </div>

    <div class="form-group form-ie">
        <span class="oi oi-lock-locked"></span>
        @Html.PasswordFor(x => x.Password, new { @class = "form-control", @placeholder = "Пароль", @id = "inputPassword" })
        @Html.ValidationMessageFor(x => x.Password)
    </div>
    <input type="submit" class="btn btn-mybtn-lg btn-my btn-block text-uppercase" value="Войти" />
}

Next, the login and password were passed to the AuthController authorization controller, which stored the UserManager, SignInManager and AppDbContext (inherited from IdentityDBContext) from ASP.NET Identity. Here’s what the controller code looked like.

[AllowAnonymous]
[RoutePrefix("Auth")]
public class AuthController : Controller
{
	private AppDbContext _dbContext;
	private ApplicationSignInManager _signInManager;
	private ApplicationUserManager _userManager;
	public ApplicationSignInManager SignInManager
	{
		get
		{
			return _signInManager ?? HttpContext.GetOwinContext().Get<ApplicationSignInManager>();
		}
		private set
		{
			_signInManager = value;
		}
	}

	public ApplicationUserManager UserManager
	{
		get
		{
			return _userManager ?? HttpContext.GetOwinContext().GetUserManager<ApplicationUserManager>();
		}
		private set
		{
			_userManager = value;
		}
	}

	public AppDbContext DbContext
	{
		get
		{
			return _dbContext ?? HttpContext.GetOwinContext().Get<AppDbContext>();
		}
		private set
		{
			_dbContext = value;
		}
	}

	public AuthController()
	{
	}

	[HttpGet]
	public ActionResult Index()
	{
		return View(new AuthViewModel());
	}

	[HttpPost]
	[ValidateAntiForgeryToken]
	public async Task<ActionResult> Login(AuthViewModel model)
	{
		var result = await SignInManager.PasswordSignInAsync(model.Login, model.Password, false, false);
		if (result == SignInStatus.Success)
		{
			return RedirectToAction("Index", "Home");
		}
		Log.Warning("Ошибка авторизации: Неправильный логин или пароль");
		ModelState.AddModelError("Password", "Неправильный логин или пароль");
		return View("Index", model);
	}

	private IAuthenticationManager AuthenticationManager
	{
		get
		{
			return HttpContext.GetOwinContext().Authentication;
		}
	}

	[HttpGet]
	[ValidateAntiForgeryToken]
	public ActionResult LogOff()
	{
		AuthenticationManager.SignOut(DefaultAuthenticationTypes.ApplicationCookie);
		return RedirectToAction("Index", "Auth");
	}
}

The very fact of authorization in the system in other controllers was checked by applying a notation filter [Authorize]and belonging to the role – through the use of [Authorize(Roles = “role1”)].

[Authorize]
public class HomeController : Controller
{
	private AppDbContext _dbContext;

	public AppDbContext DbContext
	{
		get
		{
			return _dbContext ?? HttpContext.GetOwinContext().Get<AppDbContext>();
		}
		private set
		{
			_dbContext = value;
		}
	}

	public HomeController()
	{
	}

	[Authorize(Roles = "Common, Admin")]
	public ActionResult Index()
	{
		///something is happening
		return View();
	}
}

As someone familiar with the above stack will note, there is nothing out of the ordinary going on – these are basic elements that are familiar to every ASP.NET developer.

So, after receiving a request to change the order of authorization, we began to change it. For those who are not familiar with this, there are the following authorization types in ASP.NET, which can be set either from the config or using the Visual Studio template when creating a project:

  1. Without authorization;

  2. Authorization based on individual accounts (login + password, classic)

  3. Authorization using Active Directory, Microsoft Azure, or Office 365.

  4. Authorization using a Windows account.

Since we are not able to use Active Directory due to maintenance requirements, the only option left is authorization using Windows KM.

After playing around with changing the authorization method in empty applications a little and making sure that everything works in them, I did the same with our application, replacing the authentication mode with “Windows” in the web.config.

So, it’s time to run. Initially, I assumed that after changing the authorization, it would be possible to customize the user login in SignInManager, and then carry out authorization in the old way (only without a password) – i.e., that SignInManager will map the login with the AspNetUsers table and add the corresponding AspNetIdentity. For the purity of the experiment, I removed myself from the table with users. Iii … I still calmly logged in. After digging into the variables, I realized that when changing the authentication mode to “Windows”, a different type of Identity is used: not AspNetIdentity, but WindowsIdentity. When using WindowsIdentity, any user who logged into Windows is a priori authorized, and offline – no connection with the database and EF was observed. This meant that if nothing was fixed, then …

Well, you understand 🙂

Since we could not use Active Directory, the current version did not work, and I had no experience in writing and modifying authorization systems – plus, little time was devoted to this feature – I dug into the documentation on ASP.NET Identity and Windows Identity. As it turned out, it was the right decision.

So, how can ASP.NET Identity + EF and Windows Identity be friends:

  1. Make another class – let’s call it CustomAuthenticationFilter – and extend it from ActionFilterAttribute and IAuthenticationFilter.

The AuthorizeAttribute contains an OnAuthentication method that can be overridden in a child class. In it, we capture the user login from Windows Identity attached to the AuthenticationContext context – then, using the Entity Framework context, we access the table with users and check if the user is in the list. If it doesn’t exist, return false in the method.

Then, from the AuthorizeAttribute in our class, you need to override the OnAuthenticationChallenge event handler, which allows you to set the system’s reaction if the OnAuthentication method, overridden earlier, returns false. In our case, we will redirect the user to a page where we tell them that the application needs to be accessed (401).

public class CustomAuthenticationFilter : ActionFilterAttribute, IAuthenticationFilter
{
	public void OnAuthentication(AuthenticationContext filterContext)
	{
		var dbContext = filterContext.HttpContext.GetOwinContext().Get<AppDbContext>();

		var username = filterContext.HttpContext.User.Identity.Name;

		var userMatches = dbContext.Users.Where(x => x.UserName == username);

		if (string.IsNullOrEmpty(username) || userMatches.Count() != 1)
		{ 
			filterContext.Result = new HttpUnauthorizedResult();
		}
	}

	public void OnAuthenticationChallenge(AuthenticationChallengeContext filterContext)
	{
		if (filterContext.Result == null || filterContext.Result is HttpUnauthorizedResult)
		{
			filterContext.Result = new RedirectToRouteResult(
				 new RouteValueDictionary{
					 { "controller", "Error" },
					 { "action", "NotAuthorized" }
			});
		}
	}
}
  1. In order to make a variant that involves an additional check of the role, in addition to checking the fact of the user’s existence, it is necessary in the same or a new class to inherit from AuthorizeAttribute. To make it easier to read, I made a new class.

The ideology here is as follows:

We make a constructor, into which we pass a list of allowed roles from the outside, for example, { “Admin”, “Common”}.

We override the AuthorizeCore method, in which we implement a user search using the model of the previous class, and then, through the same EF context, we get the list of user roles and match it with the list that arrives through the constructor. If there is a match, the user is “worthy”.

public class CustomAuthorizeAttribute : AuthorizeAttribute
{
	private readonly string[] allowedRoles;
	public CustomAuthorizeAttribute(params string[] roles)
	{
		allowedRoles = roles;
	}
	protected override bool AuthorizeCore(HttpContextBase httpContext)
	{
		var dbContext = httpContext.GetOwinContext().Get<AppDbContext>();

		var username = httpContext.User.Identity.Name;
		var userMatches = dbContext.Users.Where(x => x.Name == username);

		if (!string.IsNullOrEmpty(username) && userMatches.Count() == 1)
		{
			var userId = userMatches.First().Id;
			var userRole = (from u in dbContext.Users
							join r in dbContext.Roles on u.Roles.FirstOrDefault().RoleId equals r.Id
							where u.Id == userId
							select new
							{
								r.Name
							}).FirstOrDefault();

			foreach(var role in allowedRoles)
			{
				if (role == userRole.Name) return true;
			}
		}

		return false;
	}

	protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
	{
		filterContext.Result = new RedirectToRouteResult(
			new RouteValueDictionary
			{
				{ "controller", "Home" },
				{ "action", "AccessDenied" }
			});
	}
}

And now the magic – I think you guessed it already, with the help of these two classes, we have developed filters similar to [Authorize] and [Authorize(Roles = “role1”)].

Thus, initially faced with the inability of ASP.NET Identity and Windows Identity to work together out of the box, I redefined the filters themselves, editing their logic to what I needed. I hope the information in this post helps you if you run into a similar situation. Good luck!

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *