This is the first part of the series of articles I'll be covering about ASP.NET Core Security. We're going to start off with cookie based authentication and build our way up to configuring policy based authorization.
As part of the ASP.NET Core security, there is a new richer policy based authorization that we can use to authorize against the claims in user's possession.
Let's build an example MVC application to demonstrate the concepts. In our scenario, we'll demand users to be authenticated and have Read claim to view the home page of our application.
I am using Visual Studio 2015 Pro Edition w/Update 3 (you should also be able to use the free community edition).
1. Create a new ASP.NET Core Web Application
2. Select the Empty Template
3. We need to add the required nuget packages to configure authorization, cookie authentication, and the mvc middleware. Bring up the project.jsonfile, add the following under the dependencies section.
"Microsoft.AspNetCore.Authorization": "1.0.0"
"Microsoft.AspNetCore.Authentication.Cookies": "1.0.0"
"Microsoft.AspNetCore.Mvc": "1.0.0"
4. Once you save the project.json file, Notice Visual Studio installs the missing nuget packages automatically. Next, bring up the Startup.cs where we'll configure the middleware we just included in our project.
5. In Configure method, add the following authentication middleware configuration;
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationScheme = "Cookies",
LoginPath = new StringPath("/Account/Login"),
AccessDeniedPath = new StringPath("/Home/Forbidden"),
AutomaticAuthenticate = true,
AutomaticChallenge = true
});
Here we're using the Cookie authentication, defining our LoginPath, where users will be redirected for authentication, and AccessDeniedPath when the user is not authorized. AutomaticAuthenticate flag indicates that the middleware should run on every request and attempt to validate and reconstruct any serialized principal it created. AutomaticChallenge flag indicates that the middleware should redirect the browser to the LoginPath or the AccessDeniedPath when the authorization fails (there are various other configuration options, however this the bare minimum we need for this example).
Next, we'll configure the requirements for the ReadPolicy. The policy will demand the user to be authenticated and have the Read claim in order access the required resource(s). Depending on your authorization logic, you can setup your policy to require additional claims.
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthorization(options =>
{
options.AddPolicy("ReadPolicy", policyBuilder =>
{
policyBuilder.RequireAuthenticatedUser()
.RequireAssertion(context => context.User.HasClaim("Read", "true"))
.Build();
});
});
}
6. Finally we need to add the mvc middleware configuration.
app.UseMvc(builder =>
{
builder.MapRoute("default", "{controller=Home}/{action=index}/{id?}");
});
Let's add couple of controllers so that we can test the login and the policy we've created. Create AccountController for user login and HomeController where we'll apply the ReadPolicy.
7. In the AccountController.cs add the following actions to login user;
[HttpGet]
public IActionResult Login(string returnUrl)
{
ViewData["ReturnUrl"] = returnUrl;
return View();
}
8. Add a simple Login.cshtml view under the Views/Account folder (create the folder structure if it doesn't exists) where the user can login to the application.
<form asp-action="Account/Login" method="post"
asp-route-returnUrl="@ViewData["ReturnUrl"]">
<div>
<label>Username</label>
<input type="text" name="username" />
</div>
<div>
<label>Password</label>
<input type="password" name="password" />
</div>
<div><button>Login</button></div>
</form>
In the POST login action, we have a simple verification; The username and the password must match in order to authenticate the user (Obviously you wouldn't do this in a real production application but for our demo purposes this is fine). If they match, we then create a set of claims, the claims identity, and the claims principle that represents the authenticated user. Then, we sign in the user (means we issue a cookie to the user which contains the set of claims we've created) and redirect back to the resource that was requested for access.
[HttpPost]
public async Task<IActionResult> Login(string username, string password, string returnUrl)
{
if (username == password)
{
var claims = new List<Claim>
{
new Claim("Read", "true"),
new Claim(ClaimTypes.Name, "ayayalar"),
new Claim(ClaimTypes.Sid, "12345")
};
var claimsIdentity = new ClaimsIdentity(claims, "password");
var claimsPrinciple = new ClaimsPrincipal(claimsIdentity);
await HttpContext.Authentication.SignInAsync("Cookies", claimsPrinciple);
if (Url.IsLocalUrl(returnUrl))
{
return Redirect(returnUrl);
}
return Redirect("~/");
}
return View();
}
9. Add the following action to the HomeController.cs;
[Authorize(Policy = "ReadPolicy")]
public IActionResult Index()
{
return View();
}
Note that we passed the ReadPolicy to the authorization attribute. The user must be authenticated and have a Read claim to have access. Otherwise, they'll be forwarded to the Forbidden page as we specified in the authentication middleware configuration.
The Index.cshtml view for the home page (can be as simple as one line of code) under Views/Home folder;
<h1>Access Granted</h1>
We should be able to test our changes at this point. Once you run the application, you'll be redirected to the login page since you're not authenticated (notice the return url in the query string is set automatically by the framework). Upon successfully submitting your credentials, you will be authorized and redirected to the home page.
For testing purposes, try removing the Read claim we've added in the Loginaction, rebuild your solution and restart the application, even if the user can login successfully, authorization will be denied and the user will be redirected to the Forbidden page.