Security in ASP.NET Core MVC 5 and Entity Framework Core

Create a database named SecurityASPNETCoreMVC5. This database have 3 tables: Role, Account and Account_Role as below:

USE SecurityASPNETCoreMVC5

GO
CREATE TABLE Account (
    Id int IDENTITY(1,1) NOT NULL PRIMARY KEY,
	Username varchar(250) NULL,
	Password varchar(250) NULL,
	FullName varchar(250) NULL,
	Enable bit NULL,
)

GO
CREATE TABLE Role(
	Id int IDENTITY(1,1) NOT NULL PRIMARY KEY,
	Name varchar(250) NULL
)

GO
CREATE TABLE Account_Role(
	AccountId int NOT NULL,
	RoleId int NOT NULL,
	Enable bit NULL,
    PRIMARY KEY(AccountId, RoleId),
    FOREIGN KEY(AccountId) REFERENCES Account(Id),
    FOREIGN KEY(RoleId) REFERENCES Role(Id)
)

GO
INSERT INTO Account(Id, Username, Password, FullName, Enable) VALUES (1, 'acc1', '$2a$04$G9AnawJboy8aYDBFeU6.6.2CuwAXN7OOQU2YYA29Xk/TgHI034UVW', 'Account 1', 1)
INSERT INTO Account(Id, Username, Password, FullName, Enable) VALUES (2, 'acc2', '$2a$04$G9AnawJboy8aYDBFeU6.6.2CuwAXN7OOQU2YYA29Xk/TgHI034UVW', 'Account 2', 1)
INSERT INTO Account(Id, Username, Password, FullName, Enable) VALUES (3, 'acc3', '$2a$04$G9AnawJboy8aYDBFeU6.6.2CuwAXN7OOQU2YYA29Xk/TgHI034UVW', 'Account 3', 1)

GO
INSERT INTO Role(Id, Name) VALUES (1, 'SuperAdmin')
INSERT INTO Role(Id, Name) VALUES (2, 'Admin')
INSERT INTO Role(Id, Name) VALUES (3, 'Employee')

GO
INSERT INTO Account_Role(AccountId, RoleId, Enable) VALUES (1, 1, 1)
INSERT INTO Account_Role(AccountId, RoleId, Enable) VALUES (1, 2, 1)
INSERT INTO Account_Role(AccountId, RoleId, Enable) VALUES (2, 2, 1)
INSERT INTO Account_Role(AccountId, RoleId, Enable) VALUES (1, 3, 1)
INSERT INTO Account_Role(AccountId, RoleId, Enable) VALUES (2, 3, 1)
INSERT INTO Account_Role(AccountId, RoleId, Enable) VALUES (3, 3, 1)







On the Visual Studio, select Create a new project from Get Started

Select ASP.NET Core Web Application




Input Project Name and select Location for new project

Select ASP.NET Core 5.0 Version and select ASP.NET Core Empty Template. Click Create button to finish




Use NuGet add Libraries need for Entity Framework Core as below:

  • Microsoft.EntityFrameworkCore
  • Microsoft.EntityFrameworkCore.Tools
  • Microsoft.EntityFrameworkCore.SqlServer
  • Microsoft.EntityFrameworkCore.SqlServer.Design
  • Microsoft.EntityFrameworkCore.Proxies
  • Microsoft.Extensions.Configuration.JSON

Open Startup.cs file and add new configurations as below:

using LearnASPNETCoreMVC5.Models;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

namespace LearnASPNETCoreMVC5
{
    public class Startup
    {
        public IConfiguration configuration;

        public Startup(IConfiguration _configuration)
        {
            configuration = _configuration;
        }

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
                    .AddCookie(options =>
                    {
                        options.LoginPath = "/Account/Index";
                        options.LogoutPath = "/Account/SignOut";
                        options.AccessDeniedPath = "/Account/AccessDenied";
                    });

            services.AddControllersWithViews();

            var connectionString = configuration.GetConnectionString("DefaultConnection");
            services.AddDbContext<DataContext>(options => options.UseLazyLoadingProxies().UseSqlServer(connectionString));
        }

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseRouting();

            app.UseAuthentication();
			
            app.UseAuthorization();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllerRoute(
                    name: "default",
                    pattern: "{controller=Account}/{action=Index}/{id?}");
            });
        }
    }
}




Select Project and right click to select Add\New Item Menu

Select Web\ASP.NET in left side. Select App Settings File item and click Add button to Finish

In appsettings.json file and new configurations as below:

{
  "ConnectionStrings": {
    "DefaultConnection": "Server=.;Database=SecurityASPNETCoreMVC5;user id=sa;password=123456"
  }
}

Create new folder named Models. In this folder, create new class as below:

Create new class named Account.cs as below:

using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;

namespace LearnASPNETCoreMVC5.Models
{
    [Table("Account")]
    public partial class Account
    {
        public Account()
        {
            AccountRoles = new HashSet<AccountRole>();
        }

        public int Id { get; set; }
        public string Username { get; set; }
        public string Password { get; set; }
        public string FullName { get; set; }
        public bool? Enable { get; set; }

        public virtual ICollection<AccountRole> AccountRoles { get; set; }
    }
}

Create new class named Role.cs as below:

using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;

namespace LearnASPNETCoreMVC5.Models
{
    [Table("Role")]
    public partial class Role
    {
        public Role()
        {
            AccountRoles = new HashSet<AccountRole>();
        }

        public int Id { get; set; }
        public string Name { get; set; }

        public virtual ICollection<AccountRole> AccountRoles { get; set; }
    }
}




Create new class named AccountRole.cs as below:

using System.ComponentModel.DataAnnotations.Schema;

namespace LearnASPNETCoreMVC5.Models
{
    [Table("Account_Role")]
    public partial class AccountRole
    {
        public int AccountId { get; set; }
        public int RoleId { get; set; }
        public bool? Enable { get; set; }

        public virtual Account Account { get; set; }
        public virtual Role Role { get; set; }
    }
}

In Models folder, create new class named DatabaseContext.cs as below:

using Microsoft.EntityFrameworkCore;

namespace LearnASPNETCoreMVC5.Models
{
    public partial class DatabaseContext : DbContext
    {
        public DatabaseContext()
        {
        }

        public DatabaseContext(DbContextOptions<DatabaseContext> options)
            : base(options)
        {
        }

        public virtual DbSet<Account> Accounts { get; set; }
        public virtual DbSet<AccountRole> AccountRoles { get; set; }
        public virtual DbSet<Role> Roles { get; set; }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<Account>(entity =>
            {
                entity.Property(e => e.FullName)
                    .HasMaxLength(250)
                    .IsUnicode(false);

                entity.Property(e => e.Password)
                    .HasMaxLength(250)
                    .IsUnicode(false);

                entity.Property(e => e.Username)
                    .HasMaxLength(250)
                    .IsUnicode(false);
            });

            modelBuilder.Entity<AccountRole>(entity =>
            {
                entity.HasKey(e => new { e.RoleId, e.AccountId });

                entity.ToTable("Account_Role");

                entity.HasOne(d => d.Account)
                    .WithMany(p => p.AccountRoles)
                    .HasForeignKey(d => d.AccountId)
                    .OnDelete(DeleteBehavior.ClientSetNull)
                    .HasConstraintName("FK_Account_Role_Account");

                entity.HasOne(d => d.Role)
                    .WithMany(p => p.AccountRoles)
                    .HasForeignKey(d => d.RoleId)
                    .OnDelete(DeleteBehavior.ClientSetNull)
                    .HasConstraintName("FK_Account_Role_Role");
            });

            modelBuilder.Entity<Role>(entity =>
            {
                entity.Property(e => e.Name)
                    .HasMaxLength(250)
                    .IsUnicode(false);
            });
        }
    }
}




Create Security folder in project. This folder contains class need for security in ASP.NET Core MVC as below:

using LearnASPNETCoreMVC5.Models;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Http;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;

namespace LearnASPNETCoreMVC5.Security
{
    public class SecurityManager
    {
        public async void SignIn(HttpContext httpContext, Account account)
        {
            ClaimsIdentity claimsIdentity = new ClaimsIdentity(getUserClaims(account), CookieAuthenticationDefaults.AuthenticationScheme);
            ClaimsPrincipal claimsPrincipal = new ClaimsPrincipal(claimsIdentity);
            await httpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, claimsPrincipal);
        }

        public async void SignOut(HttpContext httpContext)
        {
            await httpContext.SignOutAsync();
        }

        private IEnumerable<Claim> getUserClaims(Account account)
        {
            List<Claim> claims = new List<Claim>();
            claims.Add(new Claim(ClaimTypes.Name, account.Username));
            account.AccountRoles.ToList().ForEach(ac =>
            {
                claims.Add(new Claim(ClaimTypes.Role, ac.Role.Name));
            });
            return claims;
        }
    }
}

Create new folder named Controllers. In this folder, create new controllers as below:

Create new controller named DemoController.cs as below:

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using System.Diagnostics;

namespace LearnASPNETCoreMVC5.Controllers
{
    public class DemoController : Controller
    {
        [AllowAnonymous]
        public IActionResult Index()
        {
            return View();
        }

        [Authorize(Roles = "SuperAdmin")]
        public IActionResult Work1()
        {
            return View("Work1");
        }

        [Authorize(Roles = "SuperAdmin,Admin")]
        public IActionResult Work2()
        {
            var user = User.FindFirst(ClaimTypes.Name);
            Debug.WriteLine(user.Value);
            return View("Work2");
        }

        [Authorize(Roles = "SuperAdmin,Admin,Employee")]
        public IActionResult Work3()
        {
            return View("Work3");
        }
    }
}




Create new controller named AccountController.cs as below:

using Microsoft.AspNetCore.Mvc;
using LearnASPNETCoreMVC5.Models;
using LearnASPNETCoreMVC5.Security;
using System.Linq;

namespace LearnASPNETCoreMVC5.Controllers
{
    public class AccountController : Controller
    {
        private DatabaseContext db = new DatabaseContext();

        private SecurityManager securityManager = new SecurityManager();

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

        [HttpPost]
        public IActionResult Login(string username, string password)
        {
            var account = processLogin(username, password);
            if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(password) || account == null)
            {
                ViewBag.error = "Invalid";
                return View("Index");
            }
            else
            {
                securityManager.SignIn(this.HttpContext, account);
                return RedirectToAction("welcome");
            }
        }

        private Account processLogin(string username, string password)
        {
            var account = db.Accounts.SingleOrDefault(a => a.Username.Equals(username) && a.Enable == true);
            if (account != null)
            {
                if (BCrypt.Net.BCrypt.Verify(password, account.Password))
                {
                    return account;
                }
            }
            return null;
        }

        public IActionResult Welcome()
        {
            return View("Welcome");
        }

        public IActionResult AccessDenied()
        {
            return View("AccessDenied");
        }

        public IActionResult SignOut()
        {
            securityManager.SignOut(this.HttpContext);
            return RedirectToAction("Index");
        }
    }
}

Create new folder named Views. In this folder, create new views as below:

Create new folder named Account folder. In this folder, create new views as below:

Create new view named Index.cshtml as below:

<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Login</title>
</head>
<body>

    <h3>Login Page</h3>
    @ViewBag.error
    <form method="post" asp-controller="account" asp-action="login">
        <table>
            <tr>
                <td>Username</td>
                <td>
                    <input type="text" name="username" />
                </td>
            </tr>
            <tr>
                <td>Password</td>
                <td>
                    <input type="password" name="password" />
                </td>
            </tr>
            <tr>
                <td>&nbsp;</td>
                <td>
                    <input type="submit" value="Login" />
                </td>
            </tr>
        </table>
    </form>

</body>
</html>




Create new view named Welcome.cshtml as below:

@using System.Security.Claims;
@{
    var userId = User.FindFirst(ClaimTypes.Name);
}

<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Welcome</title>
</head>
<body>

    <h3>Welcome Page</h3>
    Welcome @userId.Value
    <br />
    <a asp-controller="account" asp-action="SignOut">Logout</a>

</body>
</html>

Create new view named AccessDenied.cshtml as below:

<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Access Denied</title>
</head>
<body>

    <h3>Access Denied Page</h3>

</body>
</html>

Createw new folder named Demo. In this folder, create new views as below:

Create new view named Index.cshtml as below:

<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Index</title>
</head>
<body>

    <h3>Index Page</h3>

</body>
</html>

Create new view named Work1.cshtml as below:

<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Work 1</title>
</head>
<body>

    <h3>Work 1 Page</h3>

</body>
</html>




Create new view named Work2.cshtml as below:

<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Work 2</title>
</head>
<body>

    <h3>Work 2 Page</h3>

</body>
</html>

Create new view named Work3.cshtml as below:

<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Work 3</title>
</head>
<body>

    <h3>Work 3 Page</h3>

</body>
</html>




Select Views folder and right click to select Add\New Item Menu

Select Web\ASP.NET in left side. Select Razor View Imports item and click Add button to Finish

In _ViewImports.cshtml file and TagHelpers library as below:

@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers




  • Access Index action in Account controller with following url: http://localhost:49328/Account/Index

    Output

  • Test access Index action in Demo controller without account with url: http://localhost:49328/Demo/Index

    Output

  • Test access Work1 action in Demo controller without account with url: http://localhost:49328/Demo/Work1

    Output

  • Test access Work2 action in Demo controller without account with url: http://localhost:49328/Demo/Work2

    Output

  • Test access Work3 action in Demo controller without account with url: http://localhost:49328/Demo/Work3

    Output

  • Test login with invalid account: username is abc and password is 456

    Output

  • Test login with valid account: username is acc2 and password is 123. This account have roles: admin and employee

    Output

  • Use acc2 has logged access Work1 action in Demo controller with url: http://localhost:49328/Demo/Work1

    Output

  • Use acc2 has logged access Work2 action in Demo controller with url: http://localhost:49328/Demo/Work2

    Output

  • Use acc2 has logged access Work3 action in Demo controller with url: http://localhost:49328/Demo/Work3

    Output