PROWARE technologies
PROWARE technologies

Setup/Add Local Authentication/User Accounts to ASP.NET Core

It is very easy to add local authentication to ASP.NET Core Web Applications. It is easier to configure a REST API to authorize requests with JSON Web Tokens; see this article if using JSON web tokens.

To avoid using a database in this example, the filesystem is used to store user email and password. It is very simple. Probably, users will be stored in a database in production.

NOTE: this example uses ASP.NET Core 3.1 but should be backwards compatible and hopefully forwards compatible, too.

Create a new ASP.NET Core Web Application Project named "AuthWebSite" or open an existing one.

Create a new C# class file and save it to the project's root folder as User.cs.

// User.cs
namespace AuthWebSite
{
	public class User
	{
		public string Email { get; }
		public User(string email)
		{
			Email = email;
		}
	}
}

Create a C# class file and save it to the project's root folder as IUserDatabase.cs.

// IUserDatabase.cs
using System.Threading.Tasks;

namespace AuthWebSite
{
	public interface IUserDatabase
	{
		Task<User> AuthenticateUser(string email, string password);
		Task<User> AddUser(string email, string password);
	}
}

Now, create a folder named "Users" in the project's folder and then create another new C# class file as UserDatabase.cs and save it to the same project root folder. This is the user database implementation.

// UserDatabase.cs
using Microsoft.AspNetCore.Hosting;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;

namespace AuthWebSite
{
	public class UserDatabase : IUserDatabase
	{
		private readonly IWebHostEnvironment env;
		public UserDatabase(IWebHostEnvironment env)
		{
			this.env = env;
		}
		private static string CreateHash(string str)
		{
			var md5 = new HMACMD5(Encoding.UTF8.GetBytes("997eff51db1544c7a3c2ddeb2053f051"));
			byte[] data = md5.ComputeHash(Encoding.UTF8.GetBytes(str));
			str = "";
			foreach(var x in data)
				str += x.ToString("X2");
			return str;
		}
		public async Task<User> AuthenticateUser(string email, string password)
		{
			return await Task.Run(() =>
			{
				if (string.IsNullOrEmpty(email) || string.IsNullOrEmpty(password))
					return null;
				var path = env.ContentRootPath + "\\Users"; // CREATE THE "USERS" FOLDER IN THE PROJECT'S FOLDER!!!
				if (!System.IO.Directory.Exists(path))
					return null;
				path += '\\' + email;
				if (!System.IO.File.Exists(path))
					return null;
				if (System.IO.File.ReadAllText(path) != CreateHash(password))
					return null;
				return new User(email);
			});
		}
		public async Task<User> AddUser(string email, string password)
		{
			return await Task.Run(() =>
			{
				try
				{
					if (string.IsNullOrEmpty(email) || string.IsNullOrEmpty(password))
						return null;
					var path = env.ContentRootPath + "\\Users"; // CREATE THE "USERS" FOLDER IN THE PROJECT'S FOLDER!!!
					if (!System.IO.Directory.Exists(path))
						System.IO.Directory.CreateDirectory(path);
					path += '\\' + email;
					if (System.IO.File.Exists(path))
						return null;
					System.IO.File.WriteAllText(path, CreateHash(password));
					return new User(email);
				}
				catch
				{
					return null;
				}
			});
		}
	}
}

Modify the Startup.cs file as follows (this is an ASP.NET Core 3.0 file). The comments note where code has been added.

// Startup.cs
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

namespace AuthWebSite
{
	public class Startup
	{
		public Startup(IConfiguration configuration)
		{
			Configuration = configuration;
		}

		public IConfiguration Configuration { get; }

		// This method gets called by the runtime. Use this method to add services to the container.
		public void ConfigureServices(IServiceCollection services)
		{
			services.AddControllersWithViews();

			// NOTE: LOCAL AUTHENTICATION CODE IS BELOW THIS LINE BUT INSIDE THIS BLOCK
			services.AddTransient<IUserDatabase, UserDatabase>();  // NOTE: LOCAL AUTHENTICATION ADDED HERE; AddTransient() IS OK TO USE BECAUSE STATE IS SAVED TO THE DRIVE
			// NOTE: LOCAL AUTHENTICATION ADDED HERE
			services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie(options =>
			{
				options.LoginPath = "/Home/Login"; // THIS IS THE PAGE THAT UNAUTHORIZED REQUESTS WILL BE REDIRECTED TO
				options.AccessDeniedPath = "/Home/AccessDenied";
			});

		}

		// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
		public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
		{
			if (env.IsDevelopment())
			{
				app.UseDeveloperExceptionPage();
			}
			else
			{
				app.UseExceptionHandler("/Home/Error");
			}
			app.UseStaticFiles();

			app.UseAuthentication(); // <-- NOTE: LOCAL AUTHENTICATION ADDED HERE

			app.UseRouting();

			app.UseAuthorization();

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

This is just the Login & Register Model. Create a new C# class file in the models folder named LoginModel.cs and add this code:

// LoginModel.cs
using System.ComponentModel.DataAnnotations;

namespace AuthWebSite.Models
{
	public class LoginModel
	{
		[Required]
		public string Email { get; set; }
		[Required]
		public string Password { get; set; }
	}
}

In this example, the home controller will be the one that handles authentication. Modify it to look like this:

// HomeController.cs
using System.Diagnostics;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using System.Threading.Tasks;
using System.Security.Claims;
using System.Collections.Generic;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication;
using AuthWebSite.Models;

namespace AuthWebSite.Controllers
{
	public class HomeController : Controller
	{
		private readonly IUserDatabase userdb;
		public HomeController(IUserDatabase userdb)
		{
			this.userdb = userdb;
		}
		private async Task<IActionResult> SignIn(User user, string ReturnUrl) // this is used by Register and Login methods
		{
			var claims = new List<Claim>
			{
				new Claim(ClaimTypes.NameIdentifier, user.Email),
				new Claim(ClaimTypes.Name, user.Email),
				new Claim(ClaimTypes.Email, user.Email)
			};
			var id = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
			var principal = new ClaimsPrincipal(id);
			await HttpContext.SignInAsync(principal);
			if(string.IsNullOrEmpty(ReturnUrl))
				return RedirectToAction("Index", "Home");
			return LocalRedirect(ReturnUrl);
		}
		public IActionResult Login()
		{
			return View();
		}
		[ValidateAntiForgeryToken]
		[HttpPost]
		[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] // NOTE: don't forget this
		public async Task<IActionResult> Login(LoginModel model, string ReturnUrl)
		{
			if (!ModelState.IsValid)
				return View(model);
			var user = await userdb.AuthenticateUser(model.Email, model.Password);
			if (user == null)
				return View(model);
			return await SignIn(user, ReturnUrl);
		}
		public IActionResult Register()
		{
			return View();
		}
		[ValidateAntiForgeryToken]
		[HttpPost]
		[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] // NOTE: don't forget this
		public async Task<IActionResult> Register(LoginModel model)
		{
			if (!ModelState.IsValid)
				return View(model);
			var user = await userdb.AddUser(model.Email, model.Password);
			if (user == null)
				return View(model);
			return await SignIn(user, null);
		}
		[ValidateAntiForgeryToken]
		[HttpPost]
		[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] // NOTE: don't forget this
		public async Task<IActionResult> Logout()
		{
			await HttpContext.SignOutAsync();
			return RedirectToAction("Index", "Home");
		}
		[Authorize] // NOTE: don't forget this; it makes this endpoint accessible only by logged in users
		public IActionResult Privacy()
		{
			return View();
		}



		// NOTE: the following code is unmodified
		public IActionResult Index()
		{
			return View();
		}

		[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
		public IActionResult Error()
		{
			return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
		}
	}
}

Modify Privacy.cshtml:

@{
	ViewData["Title"] = "Privacy Policy";
}
<h1>@ViewData["Title"]</h1>

<p>Use this page to detail your site's privacy policy.</p>
<p>NOTE: this page can only be viewed by registered users.</p>

Create Register.cshtml, modify it as follows and save it in the Views/Home folder.

@{
	ViewData["Title"] = "Register";
}
<h1>@ViewData["Title"]</h1>
<form action="/Home/Register" method="post">
	@Html.AntiForgeryToken()
	<input type="email" placeholder="EMAIL" name="Email" value="@Model?.Email" required />
	<input type="password" placeholder="PASSWORD" name="Password" required />
	<button type="submit">REGISTER</button>
</form>

Create Login.cshtml, modify it as follows and save it in the Views/Home folder.

@{
	ViewData["Title"] = "Login";
}
<h1>@ViewData["Title"]</h1>
@if(!string.IsNullOrEmpty(Context.Request.Query["ReturnUrl"]))
{
	<p>You must login to access <b>@Context.Request.Query["ReturnUrl"]</b></p>
}
<form action="/Home/Login?ReturnUrl=@Html.Raw(Uri.EscapeDataString("" + Context.Request.Query["ReturnUrl"]))" method="post">
	@Html.AntiForgeryToken()
	<input type="email" placeholder="EMAIL" name="Email" value="@Model?.Email" required />
	<input type="password" placeholder="PASSWORD" name="Password" required />
	<button type="submit">LOGIN</button>
</form>

Modify _Layout.cshtml to have these links in the navbar:

<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="utf-8" />
	<meta name="viewport" content="width=device-width, initial-scale=1.0" />
	<title>@ViewData["Title"] - AuthWebSite</title>
	<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" />
	<link rel="stylesheet" href="~/css/site.css" />
</head>
<body>
	<header>
		<nav class="navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom box-shadow mb-3">
			<div class="container">
				<a class="navbar-brand" asp-area="" asp-controller="Home" asp-action="Index">AuthWebSite</a>
				<button class="navbar-toggler" type="button" data-toggle="collapse" data-target=".navbar-collapse" aria-controls="navbarSupportedContent"
						aria-expanded="false" aria-label="Toggle navigation">
					<span class="navbar-toggler-icon"></span>
				</button>
				<div class="navbar-collapse collapse d-sm-inline-flex flex-sm-row-reverse">
					<ul class="navbar-nav flex-grow-1">
						<li class="nav-item">
							<a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Index">Home</a>
						</li>
						<li class="nav-item">
							<a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
						</li>
						<!-- NOTE: IF ELSE STATEMENT ADDED -->
						@if (User.Identity.IsAuthenticated)
						{
						<li class="nav-item">
							<!-- NOTE: NEW ITEM ADDED -->
							<form action="/Home/Logout" method="post" id="logout-form" style="display:none;">@Html.AntiForgeryToken()</form>
							<a class="nav-link text-dark" href="javascript:document.getElementById('logout-form').submit();">Logout</a>
						</li>
						<li class="nav-item">
							<!-- NOTE: NEW ITEM ADDED -->
							<span class="nav-link text-dark"><small>Hello, @User.Identity.Name</small></span>
						</li>
						}
						else
						{
						<li class="nav-item">
							<!-- NOTE: NEW ITEM ADDED -->
							<a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Register">Register</a>
						</li>
						<li class="nav-item">
							<!-- NOTE: NEW ITEM ADDED -->
							<a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Login">Login</a>
						</li>
						}
					</ul>
				</div>
			</div>
		</nav>
	</header>
	<div class="container">
		<main role="main" class="pb-3">
			@RenderBody()
		</main>
	</div>

	<footer class="border-top footer text-muted">
		<div class="container">
			&copy; 2019 - AuthWebSite - <a asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
		</div>
	</footer>
	<script src="~/lib/jquery/dist/jquery.min.js"></script>
	<script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
	<script src="~/js/site.js" asp-append-version="true"></script>
	@RenderSection("Scripts", required: false)
</body>
</html>

Now, run the project and try to access the /Home/Privacy path. To access this URL, the user must be registered and logged in. To secure a REST API controller, simply adding the [Authorize] attribute to each endpoint is one solution, but a more secure one would use JSON Web Tokens (JWT), or both.

Coding Video

https://youtu.be/uoF1H1gHQwU