Securing ASP.NET 5 and AngularJS

Hesam Seyed Mousavi, December 25, 2016

securing-asp-net-5-and-angularjs

Source: ItpTechMag

blog.mousavi.fr

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.

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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
"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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public void ConfigureServices(IServiceCollection services)
{
    // add Entity Framework
    services.AddEntityFramework(Configuration)
            .AddSqlServer()
            .AddDbContext(options =>
            {
                options.UseSqlServer(Configuration.Get("Data:DefaultConnection:ConnectionString"));
            });
    // add ASP.NET Identity
    services.AddIdentity<ApplicationUser, IdentityRole>(Configuration)
        .AddEntityFrameworkStores();
    // 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:

1
2
3
4
5
6
7
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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
    {
        public DbSet 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:

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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
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())
    {
        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
                {
                    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>();
                // 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.

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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Mvc;
using MovieAngularJSApp.Models;
using System.Threading.Tasks;
namespace MovieAngularJSApp.Controllers
{
    public class AccountController : Controller
    {
        private UserManager _userManager;
        private SignInManager _signInManager;
        public AccountController(UserManager userManager, SignInManager signInManager)
        {
            _userManager = userManager;
            _signInManager = signInManager;
        }
        public IActionResult Login()
        {
            return View();
        }
        [HttpPost]
        public async Task 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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<html ng-app="moviesApp">
<head>
    <base href="/">
    <meta charset="utf-8" />
    <title>Moviestitle>
    
    <script src="//code.jquery.com/jquery-1.11.2.min.js">script>
    
    <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>
    
    <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>
    
    <link rel="stylesheet" href="/styles.css" />
    <script src="/app.js">script>
    
    <script>
        angular.module("moviesApp").value("canEdit", @ViewBag.CanEdit);
    script>
head>
<body ng-cloak>
    @ViewBag.Name / <a href="/account/signout" target="_self">sign outa>
    <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:

1
2
3
4
5
6
7
/* 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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<div>
    <h1>List Moviesh1>
    <table class="table table-bordered table-striped">
        <thead>
            <tr>
                <th ng-show="canEdit">th>
                <th>Titleth>
                <th>Directorth>
            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">edita>
                    <a href="/movies/delete/{{movie.Id}}" class="btn btn-danger btn-xs">deletea>
                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 Moviea>
    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.

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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
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 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.

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.

Source: ItpTechMag

blog.mousavi.fr

Advertisements