ASP.NET 5 and AngularJS Part 6, Security

This is the sixth part in a multiple part blog series on building ASP.NET 5 (ASP.NET vNext) apps with AngularJS. In this series of blog posts, I show how you can create a simple Movie app using ASP.NET 5, MVC 6, and AngularJS.

You can download the code discussed in this blog post from GitHub:

https://github.com/StephenWalther/MovieAngularJSApp

In this blog post, I explain how you can use ASP.NET Identity to provide different permissions to different users. In particular, I demonstrate how to create two users named Stephen and Bob. Stephen will have edit permissions on the Movies database and Bob will not.

Here’s what Stephen’s screen looks like:

Stephen Screen

Notice that Stephen can edit, delete, and add movies.

Now, here is what Bob’s screen looks like:

Bob List

Bob cannot do anything except view the list of movies. He is not allowed to edit or add movies.

Enabling ASP.NET Identity

The first step is to pull in all of the NuGet packages that we need to use ASP.NET Identity. You need to add the following packages to the dependencies section of your project.json file:

  "dependencies": {
	"EntityFramework.SqlServer": "7.0.0-beta2",
	"EntityFramework.Commands": "7.0.0-beta2",
	"Microsoft.AspNet.Mvc": "6.0.0-beta2",
	/* "Microsoft.AspNet.Mvc.WebApiCompatShim": "6.0.0-beta2", */
	"Microsoft.AspNet.Diagnostics": "1.0.0-beta2",
	"Microsoft.AspNet.Diagnostics.Entity": "7.0.0-beta2",
	"Microsoft.AspNet.Identity.EntityFramework": "3.0.0-beta2",
	"Microsoft.AspNet.Security.Cookies": "1.0.0-beta2",
	"Microsoft.AspNet.Server.IIS": "1.0.0-beta2",
	"Microsoft.AspNet.Server.WebListener": "1.0.0-beta2",
	"Microsoft.AspNet.StaticFiles": "1.0.0-beta2",
	"Microsoft.Framework.ConfigurationModel.Json": "1.0.0-beta2",
	"Microsoft.Framework.CodeGenerators.Mvc": "1.0.0-beta2",
	"Microsoft.Framework.Logging": "1.0.0-beta2",
	"Microsoft.Framework.Logging.Console": "1.0.0-beta2",
	"Microsoft.VisualStudio.Web.BrowserLink.Loader": "14.0.0-beta1"
  },

Notice that I am including the Microsoft.AspNet.Identity.EntityFramework and Microsoft.AspNet.Security.Cookies packages. These packages pull in additional dependencies such as Microsoft.AspNet.Identity and Microsoft.AspNet.Security.

Next, you need to update the ConfigureServices() method in your Startup.cs file. The ConfigureServices() method is used to register services with the built-in Dependency Injection framework included with ASP.NET 5:

public void ConfigureServices(IServiceCollection services)
{
	// add Entity Framework
	services.AddEntityFramework(Configuration)
			.AddSqlServer()
			.AddDbContext<MoviesAppContext>(options =>
			{
				options.UseSqlServer(Configuration.Get("Data:DefaultConnection:ConnectionString"));
			});

	// add ASP.NET Identity
	services.AddIdentity<ApplicationUser, IdentityRole>(Configuration)
		.AddEntityFrameworkStores<MoviesAppContext>();

	// add ASP.NET MVC
	services.AddMvc();
}

Notice that I am adding the Identity service. When I register the service, I specify that I will be use the Identity service with the Entity Framework as my data store for storing usernames and passwords. If I was running my app outside of Windows – for example, using OSX – then I could use an alternative data store such as SQLite.

See the following URL for the list of providers that Microsoft plans to support with Entity Framework 7:

https://github.com/aspnet/EntityFramework/wiki/Using-EF7-in-Traditional-.NET-Applications

Finally, we need to update the Configure() method in the Startup.cs file. The Configure() method – unlike the ConfigureServices() method – is used by OWIN to configure the application pipeline. Here’s what our updated Configure() method looks like:

public void Configure(IApplicationBuilder app)
{
	app.UseIdentity();
	app.UseMvc();

	CreateSampleData(app.ApplicationServices).Wait();
}

Notice that I call app.UseIdentity() to add ASP.NET Identity to the application pipeline. I explain the CreateSampleData() method below.

Modifying Your DbContext

In order to take advantage of ASP.NET Identity, we need to modify our DbContext class. Here’s the updated MoviesAppContext:

using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.EntityFramework;
using Microsoft.Data.Entity;
using System;

namespace MovieAngularJSApp.Models
{
    public class ApplicationUser : IdentityUser {}

    public class MoviesAppContext : IdentityDbContext<ApplicationUser>
    {
		public DbSet<Movie> Movies { get; set; }
	}
}

Notice that the modified MoviesAppContext now inherits from the base IdentityDbContext class. Inheriting from this base class adds all of the additional entities required by the Entity Framework. If you open up your database then you can see all of the extra tables that you get after inheriting from IdentityDbContext:

ASP.NET Identity Tables

Notice that I get additional database tables such as the AspNetUsers and AspNetUserClaims tables.
Adding Users at Startup

We want to add the Stephen and Bob users to our database automatically at startup. Therefore, I am going to modify the Startup.cs file so that it creates the database and adds sample data to the database automatically:

public void Configure(IApplicationBuilder app)
{
	app.UseIdentity();
	app.UseMvc();

	CreateSampleData(app.ApplicationServices).Wait();
}


private static async Task CreateSampleData(IServiceProvider applicationServices)
{
	using (var dbContext = applicationServices.GetService<MoviesAppContext>())
	{
		var sqlServerDatabase = dbContext.Database as SqlServerDatabase;
		if (sqlServerDatabase != null)
		{
			// Create database in user root (c:\users\your name)
			if (await sqlServerDatabase.EnsureCreatedAsync()) {
				// add some movies
				var movies = new List<Movie>
				{
					new Movie {Title="Star Wars", Director="Lucas"},
					new Movie {Title="King Kong", Director="Jackson"},
					new Movie {Title="Memento", Director="Nolan"}
				};
				movies.ForEach(m => dbContext.Movies.AddAsync(m));

				// add some users
				var userManager = applicationServices.GetService<UserManager<ApplicationUser>>();

				// add editor user
				var stephen = new ApplicationUser
				{
					UserName = "Stephen"
				};
				var result = await userManager.CreateAsync(stephen, "P@ssw0rd");
				await userManager.AddClaimAsync(stephen, new Claim("CanEdit", "true"));

				// add normal user
				var bob = new ApplicationUser
				{
					UserName = "Bob"
				};
				await userManager.CreateAsync(bob, "P@ssw0rd");
			}

		}
	}
}

Notice that I create an async static method named CreateSampeData(). I need to make the CreateSampleData() method async because the ASP.NET Identity methods are async.

The CreateSampleData() method first ensures that the MoviesDatabase exists. If it does not, the EnsureCreatedAsync() method creates it.

Next, three movies are added to the Movies database table. Each movie is added by calling the dbContext.Movies.AddAsync() method.

Finally, two users are added to the database: Stephen and Bob. The users are added by calling the UserManager.CreateAsync() method.

A claim is added to the Stephen user. Stephen is provided with CanEdit permissions. We’ll use this claim in our Movies app to enable Stephen, but not Bob, to edit movies.

Whenever you want to recreate the sample data, you can just delete the MoviesDatabase database. In Visual Studio, open the SQL Server Object Explorer window, right-click your database, and select the Delete menu option. Make sure that you check Close existing connections or you will be blocked from deleting the database.

Delete Database

Forcing Users to Login

Now that we have the basic setup out of the way, we can force users to login by adding an [Authorize] attribute to our Home controller like this:

using Microsoft.AspNet.Mvc;
using System.Security.Claims;

namespace MovieAngularJSApp.Controllers
{
	[Authorize]
	public class HomeController : Controller
    {
        
        public IActionResult Index()
        {
		var user = (ClaimsIdentity)User.Identity;
		ViewBag.Name = user.Name;
		ViewBag.CanEdit = user.FindFirst("CanEdit") != null ? "true" : "false";
		return View();
        }
    }
}

If an anonymous user attempts to navigate to the root of our app then the anonymous user will be redirected to the /account/login controller action. Here’s what the Login() methods look like in the AccountController:

using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Mvc;
using MovieAngularJSApp.Models;
using System.Threading.Tasks;

namespace MovieAngularJSApp.Controllers
{
    public class AccountController : Controller
    {
		private UserManager<ApplicationUser> _userManager;
		private SignInManager<ApplicationUser> _signInManager;


		public AccountController(UserManager<ApplicationUser> userManager, SignInManager<ApplicationUser> signInManager)
		{
			_userManager = userManager;
			_signInManager = signInManager;
		}


		public IActionResult Login()
        {
            return View();
        }

		[HttpPost]
		public async Task<IActionResult> Login(LoginViewModel login, string returnUrl = null)
		{
			var signInStatus = await _signInManager.PasswordSignInAsync(login.UserName, login.Password, false, false);
			if (signInStatus == SignInStatus.Success)
			{
				return Redirect("/home");
			}
			ModelState.AddModelError("", "Invalid username or password.");
			return View();
		}


		public IActionResult SignOut()
		{
			_signInManager.SignOut();
			return Redirect("/home");
		}

	}
}

The UserManager and SignInManager classes are passed to the AccountController through the built-in ASP.NET 5 dependency injection. The Login() method calls the SignInManager.PasswordSignInAsync() method to validate the username and password combination entered by the user. If the combination validates then the user is redirected back to the Home controller.

Passing Claims Data to AngularJS

Stephen can edit movies but not Bob. To make this work, we need to pass the claims data associated with the Stephen and Bob users from the server (ASP.NET) to the client (AngularJS).

In the Home controller, we add the CanEdit claim to the ViewBag like this:

using Microsoft.AspNet.Mvc;
using System.Security.Claims;

namespace MovieAngularJSApp.Controllers
{
	[Authorize]
	public class HomeController : Controller
    {
        
        public IActionResult Index()
        {
		var user = (ClaimsIdentity)User.Identity;
		ViewBag.Name = user.Name;
		ViewBag.CanEdit = user.FindFirst("CanEdit") != null ? "true" : "false";
		return View();
        }
    }
}

We cast the user identity as a ClaimsIdentity and then we assign the value of the CanEdit claim to the ViewBag.CanEdit property. Because Stephen has the CanEdit claim, the ViewBag.CanEdit property will have the value “true” (we need to use a string here because Boolean.ToString() gets converted to “True” instead of “true” which confuses our JavaScript).

Here’s what our modified Index view looks like:

<!DOCTYPE html>
<html ng-app="moviesApp">
<head>
	<base href="/">
	<meta charset="utf-8" />
	<title>Movies</title>

	<!-- jQuery -->
	<script src="//code.jquery.com/jquery-1.11.2.min.js"></script>

	<!-- AngularJS-->
	<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.3.8/angular.min.js"></script>
	<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.3.8/angular-resource.js"></script>
	<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.3.8/angular-route.js"></script>

	<!-- Bootstrap -->
	<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.1/css/bootstrap.min.css">
	<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.1/css/bootstrap-theme.min.css">
	<script src="https://cdnjs.cloudflare.com/ajax/libs/angular-ui-bootstrap/0.12.0/ui-bootstrap-tpls.min.js"></script>


	<!-- App -->
	<link rel="stylesheet" href="/styles.css" />
	<script src="/app.js"></script>

	<!-- Get Server Data -->
	<script>
		angular.module("moviesApp").value("canEdit", @ViewBag.CanEdit);
	</script>

</head>
<body ng-cloak>

	@ViewBag.Name / <a href="/account/signout" target="_self">sign out</a>

	<div class="container-fluid">
		<ng-view></ng-view>
	</div>
</body>
</html>

Notice the section of the page labeled Get Server Data. The value of the CanEdit claim is assigned to an AngularJS module value named “canEdit”. The canEdit value is used in the AngularJS moviesListController like this:

/* Movies List Controller  */
MoviesListController.$inject = ['$scope', 'Movie', 'canEdit'];

function MoviesListController($scope, Movie, canEdit) {
    $scope.canEdit = canEdit;
    $scope.movies = Movie.query();
}

The canEdit value is injected into the controller and assigned to the controller’s scope. This makes the canEdit value available in the AngularJS list view which is used to display the list of movies:

<div>
    <h1>List Movies</h1>
    <table class="table table-bordered table-striped">
        <thead>
            <tr>
                <th ng-show="canEdit"></th>
                <th>Title</th>
                <th>Director</th>
            </tr>
        </thead>
        <tbody>
            <tr ng-repeat="movie in movies">
                <td ng-show="canEdit">
                    <a href="/movies/edit/{{movie.Id}}" class="btn btn-default btn-xs">edit</a>
                    <a href="/movies/delete/{{movie.Id}}" class="btn btn-danger btn-xs">delete</a>
                </td>
                <td>{{movie.Title}}</td>
                <td>{{movie.Director}}</td>
            </tr>
        </tbody>
    </table>

    <p ng-show="canEdit">
        <a href="/movies/add" class="btn btn-primary">Add New Movie</a>
    </p>
</div>

Notice that the AngularJS ng-show directive is used twice in the list view. The ng-show directive is used to hide or display the first column of the HTML table that lists the movies (this hides or shows the Movie edit and delete buttons). The ng-show directive also is used to hide or show the Add New Movie button at the bottom of the view.

Because Stephen has the CanEdit claim, when Stephen opens the List view, Stephen sees the edit, delete, and add buttons:

Stephen Screen

However, Bob does not have the CanEdit claim. Therefore, Bob does not get these buttons:

Bob List

Securing API Controller Actions

There is one last thing that we need to do to make this all work. Bob cannot see the edit buttons but Bob could, if he is feeling super sneaky, bypass the views and post new movies directly to the Movies API controller.

Here’s how we block this from happening:

using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNet.Mvc;
using MovieAngularJSApp.Models;
using System.Security.Claims;

namespace MovieAngularJSApp.API.Controllers
{
    [Route("api/[controller]")]
    public class MoviesController : Controller
    {
		private readonly MoviesAppContext _dbContext;

		public MoviesController(MoviesAppContext dbContext) 
		{
			_dbContext = dbContext;
		}

        [HttpGet]
        public IEnumerable<Movie> Get()
        {
			return _dbContext.Movies;
        }


        [HttpGet("{id:int}")]
        public IActionResult Get(int id)
        {
			var movie = _dbContext.Movies.FirstOrDefault(m => m.Id == id);
			if (movie == null) {
				return new HttpNotFoundResult();
			} else {
				return new ObjectResult(movie);
            }
		}


        [HttpPost]
		[Authorize("CanEdit", "true")]
        public IActionResult Post([FromBody]Movie movie)
        {
			if (ModelState.IsValid)
			{
				if (movie.Id == 0)
				{
					_dbContext.Movies.Add(movie);
					_dbContext.SaveChanges();
					return new ObjectResult(movie);
				}
				else
				{
					var original = _dbContext.Movies.FirstOrDefault(m => m.Id == movie.Id);
					original.Title = movie.Title;
					original.Director = movie.Director;
					original.TicketPrice = movie.TicketPrice;
					original.ReleaseDate = movie.ReleaseDate;
					_dbContext.SaveChanges();
					return new ObjectResult(original);
				}
			}

			// This will work in later versions of ASP.NET 5
			//return new BadRequestObjectResult(ModelState);
			return null;
		}


		[Authorize("CanEdit", "true")]
		[HttpDelete("{id:int}")]
        public IActionResult Delete(int id)
        {
			var movie = _dbContext.Movies.FirstOrDefault(m => m.Id == id);
			_dbContext.Movies.Remove(movie);
			_dbContext.SaveChanges();
            return new HttpStatusCodeResult(200);
        }


    }
}

Notice that both the Post() and Delete() actions in the MoviesController in the code above are decorated with [Authorize] attributes. The [Authorize] attributes are used to verify that the person invoking the controller action has the CanEdit claim.

If Bob, who does not have the CanEdit claim, attempts to invoke either action then Bob will be blocked. Bob will get a 302 redirect that redirects the Ajax API call back to the /account/login view:

ASP.NET Identity Redirect

Bob’s Ajax request gets redirected because a 401 Status code automatically triggers the ASP.NET framework to redirect the user to the Login page. This is true even when making an Ajax request.

Summary

In this blog post, I described how you can use ASP.NET Identity in an ASP.NET 5 app. I demonstrated how you can create users automatically when an ASP.NET app starts. Next, I showed you how you can pass user claims information from the server (ASP.NET) to the client (AngularJS). We used the CanEdit claim in our AngularJS app to hide or display the movie edit, delete, and add buttons.

In my next blog post, I am going to focus on using ASP.NET 5 outside of Windows. I want to get the Movies app working on my MacBookPro running OSX Yosemite.

Discussion

  1. Keerthi Meda says:

    Without using a Anti-Forgery token the API would be vulnerable to CSRF attacks. Is it possible to modify this example to use Anti-Forger Tokens or Bearer Token Authentication as most people taking a look at this examples would be beginners and would commit the mistake?

  2. Stilgar says:

    Somewhat offtopic but I wonder what people do when every item in the list has a specific permission. For example what if there was a specific claim associated with each director and different people can edit different movies (based on the director). I personally create an object that has the entity (Movie) as property and additional bool properties for each permission like CanEdit, CanDelete… and then return a list of these objects. I wonder if it is the best approach or there is a better practice.