ASP.NET Core 3.1 - Simple API for Authentication, Registration and User Management
Tutorial built with ASP.NET Core 3.1
Other versions available:
- .NET: .NET 6.0, 5.0, ASP.NET Core 2.2
- Node: Node + MSSQL, Node + MySQL, Node + MongoDB
In this tutorial we'll go through an example boilerplate ASP.NET Core 3.1 API that supports user registration, login with JWT authentication and user management. For an extended example that includes email verification, role based authorization and forgot password functionality see ASP.NET Core 3.1 - Boilerplate API with Email Sign Up, Verification, Authentication & Forgot Password.
The API is configured to use a local SQLite database in development and a SQL Server database in production. It uses EF Core Migrations to automatically generate the database on startup.
The tutorial project code is available on GitHub at https://github.com/cornflourblue/aspnet-core-3-registration-login-api.
Update History:
- 04 Jun 2020 - Updated EF Core Migration instructions for SQL Server.
- 02 Jan 2020 - Added EF Core Migrations for multiple database providers, SQLite in development and SQL Server in production.
- 23 Dec 2019 - Updated from EF Core InMemory Database to SQLite Database
- 13 Dec 2019 - Updated to ASP.NET Core 3.1 (Git commit showing the changes available here)
- 14 Oct 2019 - Built with ASP.NET Core 3.0
ASP.NET Core Tutorial Contents
- Tools required to develop ASP.NET Core 3.1 applications
- Running the example API locally
- Testing the API with Postman
- Running with an Angular app
- Running with a React app
- Running a Vue.js app
- Deploying to Azure
- API Project Structure
Tools required to run the ASP.NET Core 3.1 Tutorial Example Locally
To develop and run ASP.NET Core applications locally, download and install the following:
- .NET Core SDK - includes the .NET Core runtime and command line tools
- Visual Studio Code - code editor that runs on Windows, Mac and Linux
- C# extension for Visual Studio Code - adds support to VS Code for developing .NET Core applications
For detailed instructions see ASP.NET Core - Setup Development Environment.
Running the ASP.NET Core Authentication API Locally
- Download or clone the tutorial project code from https://github.com/cornflourblue/aspnet-core-3-registration-login-api
- Start the api by running
dotnet run
from the command line in the project root folder (where the WebApi.csproj file is located), you should see the messageNow listening on: http://localhost:4000
. Follow the instructions below to test with Postman or hook up with one of the example single page applications available (Angular, React or Vue).
NOTE: You can also start the application in debug mode in VS Code by opening the project root folder in VS Code and pressing F5 or by selecting Debug -> Start Debugging from the top menu. Running in debug mode allows you to attach breakpoints to pause execution and step through the application code.
Testing the ASP.NET Core Auth API with Postman
Postman is a great tool for testing APIs, you can download it at https://www.getpostman.com/.
Below are instructions on how to use Postman to register a new user with the api, authenticate a user to get a JWT token, and then make an authenticated request with the JWT token to retrieve a list of users from the api.
How to register a new user with Postman
To register a new user with the api follow these steps:
- Open a new request tab by clicking the plus (+) button at the end of the tabs.
- Change the http request method to "POST" with the dropdown selector on the left of the URL input field.
- In the URL field enter the address to the register route of your local API -
http://localhost:4000/users/register
. - Select the "Body" tab below the URL field, change the body type radio button to "raw", and change the format dropdown selector to "JSON (application/json)".
- Enter a JSON object containing the required user properties in the "Body" textarea, e.g:
{ "firstName": "Jason", "lastName": "Watmore", "username": "jason", "password": "my-super-secret-password" }
- Click the "Send" button, you should receive a "200 OK" response with an empty JSON object in the response body.
Here's a screenshot of Postman after the request is sent and the new user has been registered:
How to authenticate a user with Postman
To authenticate a user with the api and get a JWT token follow these steps:
- Open a new request tab by clicking the plus (+) button at the end of the tabs.
- Change the http request method to "POST" with the dropdown selector on the left of the URL input field.
- In the URL field enter the address to the authenticate route of your local API -
http://localhost:4000/users/authenticate
. - Select the "Body" tab below the URL field, change the body type radio button to "raw", and change the format dropdown selector to "JSON (application/json)".
- Enter a JSON object containing the username and password in the "Body" textarea:
{ "username": "jason", "password": "my-super-secret-password" }
- Click the "Send" button, you should receive a "200 OK" response with the user details including a JWT token in the response body, make a copy of the token value because we'll be using it in the next step to make an authenticated request.
Here's a screenshot of Postman after the request is sent and the user has been authenticated:
How to make an authenticated request to retrieve all users
To make an authenticated request using the JWT token from the previous step, follow these steps:
- Open a new request tab by clicking the plus (+) button at the end of the tabs.
- Change the http request method to "GET" with the dropdown selector on the left of the URL input field.
- In the URL field enter the address to the users route of your local API -
http://localhost:4000/users
. - Select the "Authorization" tab below the URL field, change the type to "Bearer Token" in the type dropdown selector, and paste the JWT token from the previous authenticate step into the "Token" field.
- Click the "Send" button, you should receive a "200 OK" response containing a JSON array with all the user records in the system.
Here's a screenshot of Postman after making an authenticated request to get all users:
How to update a user with Postman
To update a user with the api follow these steps:
- Open a new request tab by clicking the plus (+) button at the end of the tabs.
- Change the http request method to "PUT" with the dropdown selector on the left of the URL input field.
- In the URL field enter the address to the
/users/{id}
route with the id of the user you registered above, e.g -http://localhost:4000/users/1
. - Select the "Authorization" tab below the URL field, change the type to "Bearer Token" in the type dropdown selector, and paste the JWT token from the previous authenticate step into the "Token" field.
- Select the "Body" tab below the URL field, change the body type radio button to "raw", and change the format dropdown selector to "JSON (application/json)".
- Enter a JSON object in the "Body" textarea containing the properties you want to update, for example to update the first and last names:
{ "firstName": "Foo", "lastName": "Bar" }
- Click the "Send" button, you should receive a "200 OK" response with an empty JSON object in the response body.
Here's a screenshot of Postman after the request is sent and the user has been updated:
Running an Angular 9 client application with the ASP.NET Core API
For full details about the example Angular 9 application see the post Angular 9 - User Registration and Login Example & Tutorial. But to get up and running quickly just follow the below steps.
- Download or clone the Angular 9 tutorial code from https://github.com/cornflourblue/angular-9-registration-login-example
- Install all required npm packages by running
npm install
from the command line in the project root folder (where the package.json is located). - Remove or comment out the line below the comment
// provider used to create fake backend
located in the/src/app/app.module.ts
file. - Start the application by running
npm start
from the command line in the project root folder, this will launch a browser displaying the application and it should be hooked up with the ASP.NET Core API that you already have running.
Running a React client application with the ASP.NET Core API
For full details about the example React application see the post React + Redux - User Registration and Login Tutorial & Example. But to get up and running quickly just follow the below steps.
- Download or clone the React tutorial code from https://github.com/cornflourblue/react-redux-registration-login-example
- Install all required npm packages by running
npm install
from the command line in the project root folder (where the package.json is located). - Remove or comment out the 2 lines below the comment
// setup fake backend
located in the/src/index.jsx
file. - Start the application by running
npm start
from the command line in the project root folder, this will launch a browser displaying the application and it should be hooked up with the ASP.NET Core API that you already have running.
Running a Vue client application with the ASP.NET Core API
For full details about the example Vue application see the post Vue + Vuex - User Registration and Login Tutorial & Example. But to get up and running quickly just follow the below steps.
- Download or clone the Vue tutorial code from https://github.com/cornflourblue/vue-vuex-registration-login-example
- Install all required npm packages by running
npm install
from the command line in the project root folder (where the package.json is located). - Remove or comment out the 2 lines below the comment
// setup fake backend
located in the/src/index.js
file. - Start the application by running
npm start
from the command line in the project root folder, this will launch a browser displaying the application and it should be hooked up with the ASP.NET Core API that you already have running.
Deploying the ASP.NET Core API with a SQL Server Database to Azure
For instructions on how to deploy the ASP.NET Core API to Microsoft Azure with an front-end application built in Angular, React or Vue.js and configure it to connect to an Azure SQL Server Database, refer to the following tutorials:
- Angular + ASP.NET Core + SQL on Azure - How to Deploy a Full Stack App to Microsoft Azure
- React + ASP.NET Core + SQL on Azure - How to Deploy a Full Stack App to Microsoft Azure
- Vue.js + ASP.NET Core + SQL on Azure - How to Deploy a Full Stack App to Microsoft Azure
ASP.NET Core API Tutorial Project Structure
The tutorial project is organised into the following folders:
Controllers
Define the end points / routes for the web api, controllers are the entry point into the web api from client applications via http requests.
Migrations
Database migration files based on the Entities
classes that are used to automatically create the SQL database for the api and automatically update it with changes when the entities are changed. Migrations are generated with the Entity Framework Core CLI, the migrations in this example were generated with the following commands for the different database providers.
- SQLite EF Core Migrations:
dotnet ef migrations add InitialCreate --context SqliteDataContext --output-dir Migrations/SqliteMigrations
- SQL Server EF Core Migrations (MacOS):
ASPNETCORE_ENVIRONMENT=Production dotnet ef migrations add InitialCreate --context DataContext --output-dir Migrations/SqlServerMigrations
- SQL Server EF Core Migrations (Windows):
set ASPNETCORE_ENVIRONMENT=Production
dotnet ef migrations add InitialCreate --context DataContext --output-dir Migrations/SqlServerMigrations
For more info see ASP.NET Core - EF Core Migrations for Multiple Databases.
Models
Represent request and response models for controller methods, request models define the parameters for incoming requests, and response models define what data is returned.
Services
Contain business logic, validation and database access code.
Entities
Represent the application data that is stored in the database.
Helpers
Anything that doesn't fit into the above folders.
Click any of the below links to jump down to a description of each file along with its code:
- Controllers
- Entities
- Helpers
- Migrations
- SqliteMigrations
- 20200102102942_InitialCreate.cs
- 20200102102942_InitialCreate.Designer.cs
- DataContextModelSnapshot.cs
- SqlServerMigrations
- 20200102103423_InitialCreate.cs
- 20200102103423_InitialCreate.Designer.cs
- DataContextModelSnapshot.cs
- SqliteMigrations
- Models
- Services
- appsettings.Development.json
- appsettings.json
- Program.cs
- Startup.cs
- WebApi.csproj
ASP.NET Core Users Controller
The ASP.NET Core users controller defines and handles all routes / endpoints for the api that relate to users, this includes authentication, registration and standard CRUD operations. Within each route the controller calls the user service to perform the action required, this enables the controller to stay 'lean' and completely separated from the database / persistence code.
The controller actions are secured with JWT using the [Authorize]
attribute, with the exception of the Authenticate
and Register
methods which allow public access by overriding the [Authorize]
attribute on the controller with [AllowAnonymous]
attributes on each action method. I chose this approach so any new action methods added to the controller will be secure by default unless explicitly made public.
On successful authentication the Authenticate
method generates a JWT (JSON Web Token) using the JwtSecurityTokenHandler
class that generates a token that is digitally signed using a secret key stored in appsettings.json. The JWT token is returned to the client which then includes it in the HTTP Authorization header of subsequent web api requests for authentication.
using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc;
using AutoMapper;
using System.IdentityModel.Tokens.Jwt;
using WebApi.Helpers;
using Microsoft.Extensions.Options;
using System.Text;
using Microsoft.IdentityModel.Tokens;
using System.Security.Claims;
using Microsoft.AspNetCore.Authorization;
using WebApi.Services;
using WebApi.Entities;
using WebApi.Models.Users;
namespace WebApi.Controllers
{
[Authorize]
[ApiController]
[Route("[controller]")]
public class UsersController : ControllerBase
{
private IUserService _userService;
private IMapper _mapper;
private readonly AppSettings _appSettings;
public UsersController(
IUserService userService,
IMapper mapper,
IOptions<AppSettings> appSettings)
{
_userService = userService;
_mapper = mapper;
_appSettings = appSettings.Value;
}
[AllowAnonymous]
[HttpPost("authenticate")]
public IActionResult Authenticate([FromBody]AuthenticateModel model)
{
var user = _userService.Authenticate(model.Username, model.Password);
if (user == null)
return BadRequest(new { message = "Username or password is incorrect" });
var tokenHandler = new JwtSecurityTokenHandler();
var key = Encoding.ASCII.GetBytes(_appSettings.Secret);
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(new Claim[]
{
new Claim(ClaimTypes.Name, user.Id.ToString())
}),
Expires = DateTime.UtcNow.AddDays(7),
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)
};
var token = tokenHandler.CreateToken(tokenDescriptor);
var tokenString = tokenHandler.WriteToken(token);
// return basic user info and authentication token
return Ok(new
{
Id = user.Id,
Username = user.Username,
FirstName = user.FirstName,
LastName = user.LastName,
Token = tokenString
});
}
[AllowAnonymous]
[HttpPost("register")]
public IActionResult Register([FromBody]RegisterModel model)
{
// map model to entity
var user = _mapper.Map<User>(model);
try
{
// create user
_userService.Create(user, model.Password);
return Ok();
}
catch (AppException ex)
{
// return error message if there was an exception
return BadRequest(new { message = ex.Message });
}
}
[HttpGet]
public IActionResult GetAll()
{
var users = _userService.GetAll();
var model = _mapper.Map<IList<UserModel>>(users);
return Ok(model);
}
[HttpGet("{id}")]
public IActionResult GetById(int id)
{
var user = _userService.GetById(id);
var model = _mapper.Map<UserModel>(user);
return Ok(model);
}
[HttpPut("{id}")]
public IActionResult Update(int id, [FromBody]UpdateModel model)
{
// map model to entity and set id
var user = _mapper.Map<User>(model);
user.Id = id;
try
{
// update user
_userService.Update(user, model.Password);
return Ok();
}
catch (AppException ex)
{
// return error message if there was an exception
return BadRequest(new { message = ex.Message });
}
}
[HttpDelete("{id}")]
public IActionResult Delete(int id)
{
_userService.Delete(id);
return Ok();
}
}
}
ASP.NET Core User Entity
The user entity class represents the data stored in the database for users. It's used by Entity Framework Core to map relational data from the database into .NET objects for data management and CRUD operations.
namespace WebApi.Entities
{
public class User
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string Username { get; set; }
public byte[] PasswordHash { get; set; }
public byte[] PasswordSalt { get; set; }
}
}
ASP.NET Core App Exception
The app exception is a custom exceptions class used to differentiate between handled and unhandled exceptions. Handled exceptions are ones generated by the application and used to display friendly error messages to the client, for example business logic or validation exceptions caused by incorrect input from the user. Unhandled exceptions are generated by the .NET framework and caused by bugs in the application code.
using System;
using System.Globalization;
namespace WebApi.Helpers
{
// Custom exception class for throwing application specific exceptions (e.g. for validation)
// that can be caught and handled within the application
public class AppException : Exception
{
public AppException() : base() {}
public AppException(string message) : base(message) { }
public AppException(string message, params object[] args)
: base(String.Format(CultureInfo.CurrentCulture, message, args))
{
}
}
}
ASP.NET Core App Settings
The app settings class contains properties defined in the appsettings.json file and is used for accessing application settings via objects that are injected into classes using the ASP.NET Core built in dependency injection (DI) system. For example the Users Controller accesses app settings via an IOptions<AppSettings> appSettings
object that is injected into the constructor.
Mapping of configuration sections to classes is done in the ConfigureServices
method of the Startup.cs file.
namespace WebApi.Helpers
{
public class AppSettings
{
public string Secret { get; set; }
}
}
ASP.NET Core AutoMapper Profile
The automapper profile contains the mapping configuration used by the application, AutoMapper is a package available on Nuget that enables automatic mapping of one type of classes to another. In this example we're using it to map between User
entities and a few different model types - UserModel
, RegisterModel
and UpdateModel
.
using AutoMapper;
using WebApi.Entities;
using WebApi.Models.Users;
namespace WebApi.Helpers
{
public class AutoMapperProfile : Profile
{
public AutoMapperProfile()
{
CreateMap<User, UserModel>();
CreateMap<RegisterModel, User>();
CreateMap<UpdateModel, User>();
}
}
}
ASP.NET Core Data Context
The data context class is used for accessing application data through Entity Framework Core and is configured to connect to a SQL Server database. It derives from the EF Core DbContext class and has a public Users
property for accessing and managing user data. The data context is used by services for handling all low level data operations.
In development environments the api is configured to use the SqliteDataContext
which inherits from the DataContext
and overrides the db provider to connect to a local SQLite database instead of SQL Server.
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using WebApi.Entities;
namespace WebApi.Helpers
{
public class DataContext : DbContext
{
protected readonly IConfiguration Configuration;
public DataContext(IConfiguration configuration)
{
Configuration = configuration;
}
protected override void OnConfiguring(DbContextOptionsBuilder options)
{
// connect to sql server database
options.UseSqlServer(Configuration.GetConnectionString("WebApiDatabase"));
}
public DbSet<User> Users { get; set; }
}
}
ASP.NET Core SQLite Data Context
The SQLite data context class is used by the api in development environments, it inherits from the main data context and overrides the provider to use SQLite instead of SQL Server.
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
namespace WebApi.Helpers
{
public class SqliteDataContext : DataContext
{
public SqliteDataContext(IConfiguration configuration) : base(configuration) { }
protected override void OnConfiguring(DbContextOptionsBuilder options)
{
// connect to sqlite database
options.UseSqlite(Configuration.GetConnectionString("WebApiDatabase"));
}
}
}
EF Core SQLite Migrations
This folder contains EF Core migrations for the SQLite database provider used in development environments.
The migrations in this example were generated with the command dotnet ef migrations add InitialCreate --context SqliteDataContext --output-dir Migrations/SqliteMigrations
.
The migrations are generated for SQLite because the SqliteDataContext
class is configured to connect to a SQLite database. The SqliteDataContext
inherits from the main DataContext
class and overrides the provider to use SQLite instead of SQL Server. This enables the project to support multiple different database providers for different environments.
EF Core SQL Server Migrations
This folder contains EF Core migrations for the SQL Server database provider used in production environments.
The migrations in this example were generated with the below command.
Windows
set ASPNETCORE_ENVIRONMENT=Production
dotnet ef migrations add InitialCreate --context DataContext --output-dir Migrations/SqlServerMigrations
MacOS
ASPNETCORE_ENVIRONMENT=Production dotnet ef migrations add InitialCreate --context DataContext --output-dir Migrations/SqlServerMigrations
The environment variable ASPNETCORE_ENVIRONMENT
needs to be set to Production
so the SQL Server DataContext
class is configured with the .NET Core dependency injection system, see the ConfigureServices()
method of the Startup class.
If the environment variable is not set the following error is returned because the production DataContext
class is not configured with the dependency injection system: Unable to create an object of type 'DataContext'. For the different patterns supported at design time, see https://go.microsoft.com/fwlink/?linkid=851728
.
ASP.NET Core Authenticate Model
The authenticate model defines the parameters for incoming POST
requests to the /users/authenticate
route of the api, it is set as the parameter to the Authenticate
method of the UsersController. When an HTTP POST request is received to the route, the data from the body is bound to an instance of the AuthenticateModel
, validated and passed to the method.
ASP.NET Core Data Annotations are used to automatically handle model validation, the [Required]
attribute sets both the username and password as required fields so if either are missing a validation error message is returned from the api.
using System.ComponentModel.DataAnnotations;
namespace WebApi.Models.Users
{
public class AuthenticateModel
{
[Required]
public string Username { get; set; }
[Required]
public string Password { get; set; }
}
}
ASP.NET Core Register Model
The register model defines the parameters for incoming POST
requests to the /users/register
route of the api, it is set as the parameter to the Register
method of the UsersController. When an HTTP POST request is received to the route, the data from the body is bound to an instance of the RegisterModel
, validated and passed to the method.
ASP.NET Core Data Annotations are used to automatically handle model validation, the [Required]
attribute is used to mark all fields (first name, last name, username & password) as required so if any are missing a validation error message is returned from the api.
using System.ComponentModel.DataAnnotations;
namespace WebApi.Models.Users
{
public class RegisterModel
{
[Required]
public string FirstName { get; set; }
[Required]
public string LastName { get; set; }
[Required]
public string Username { get; set; }
[Required]
public string Password { get; set; }
}
}
ASP.NET Core Update Model
The update model defines the parameters for incoming PUT
requests to the /users/{id}
route of the api, it is set as the parameter to the Update
method of the UsersController. When an HTTP PUT request is received to the route, the data from the body is bound to an instance of the UpdateModel
, validated and passed to the method.
There are no validation attributes used in the update model so all fields are optional and only fields that contain a value are updated. The logic for updating users is located in the Update
method of the UserService.
namespace WebApi.Models.Users
{
public class UpdateModel
{
public string FirstName { get; set; }
public string LastName { get; set; }
public string Username { get; set; }
public string Password { get; set; }
}
}
ASP.NET Core User Model
The user model is a response model that defines the data returned for GET
requests to the /users
(get all users) and /users/{id}
(get user by id) routes of the api. The GetAll
and GetById
methods of the UsersController convert user entity data into user model data before returning it in the response in order to prevent some properties from being returned (e.g. password hashes & salts).
namespace WebApi.Models.Users
{
public class UserModel
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string Username { get; set; }
}
}
ASP.NET Core User Service
The ASP.NET Core user service is responsible for all database interaction and core business logic related to user authentication, registration and management.
The top of the file contains an interface that defines the user service, below that is the concrete user service class that implements the interface. The bottom of the class contains a couple of private methods used for creating and verifying hashed passwords that are stored in the database.
using System;
using System.Collections.Generic;
using System.Linq;
using WebApi.Entities;
using WebApi.Helpers;
namespace WebApi.Services
{
public interface IUserService
{
User Authenticate(string username, string password);
IEnumerable<User> GetAll();
User GetById(int id);
User Create(User user, string password);
void Update(User user, string password = null);
void Delete(int id);
}
public class UserService : IUserService
{
private DataContext _context;
public UserService(DataContext context)
{
_context = context;
}
public User Authenticate(string username, string password)
{
if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(password))
return null;
var user = _context.Users.SingleOrDefault(x => x.Username == username);
// check if username exists
if (user == null)
return null;
// check if password is correct
if (!VerifyPasswordHash(password, user.PasswordHash, user.PasswordSalt))
return null;
// authentication successful
return user;
}
public IEnumerable<User> GetAll()
{
return _context.Users;
}
public User GetById(int id)
{
return _context.Users.Find(id);
}
public User Create(User user, string password)
{
// validation
if (string.IsNullOrWhiteSpace(password))
throw new AppException("Password is required");
if (_context.Users.Any(x => x.Username == user.Username))
throw new AppException("Username \"" + user.Username + "\" is already taken");
byte[] passwordHash, passwordSalt;
CreatePasswordHash(password, out passwordHash, out passwordSalt);
user.PasswordHash = passwordHash;
user.PasswordSalt = passwordSalt;
_context.Users.Add(user);
_context.SaveChanges();
return user;
}
public void Update(User userParam, string password = null)
{
var user = _context.Users.Find(userParam.Id);
if (user == null)
throw new AppException("User not found");
// update username if it has changed
if (!string.IsNullOrWhiteSpace(userParam.Username) && userParam.Username != user.Username)
{
// throw error if the new username is already taken
if (_context.Users.Any(x => x.Username == userParam.Username))
throw new AppException("Username " + userParam.Username + " is already taken");
user.Username = userParam.Username;
}
// update user properties if provided
if (!string.IsNullOrWhiteSpace(userParam.FirstName))
user.FirstName = userParam.FirstName;
if (!string.IsNullOrWhiteSpace(userParam.LastName))
user.LastName = userParam.LastName;
// update password if provided
if (!string.IsNullOrWhiteSpace(password))
{
byte[] passwordHash, passwordSalt;
CreatePasswordHash(password, out passwordHash, out passwordSalt);
user.PasswordHash = passwordHash;
user.PasswordSalt = passwordSalt;
}
_context.Users.Update(user);
_context.SaveChanges();
}
public void Delete(int id)
{
var user = _context.Users.Find(id);
if (user != null)
{
_context.Users.Remove(user);
_context.SaveChanges();
}
}
// private helper methods
private static void CreatePasswordHash(string password, out byte[] passwordHash, out byte[] passwordSalt)
{
if (password == null) throw new ArgumentNullException("password");
if (string.IsNullOrWhiteSpace(password)) throw new ArgumentException("Value cannot be empty or whitespace only string.", "password");
using (var hmac = new System.Security.Cryptography.HMACSHA512())
{
passwordSalt = hmac.Key;
passwordHash = hmac.ComputeHash(System.Text.Encoding.UTF8.GetBytes(password));
}
}
private static bool VerifyPasswordHash(string password, byte[] storedHash, byte[] storedSalt)
{
if (password == null) throw new ArgumentNullException("password");
if (string.IsNullOrWhiteSpace(password)) throw new ArgumentException("Value cannot be empty or whitespace only string.", "password");
if (storedHash.Length != 64) throw new ArgumentException("Invalid length of password hash (64 bytes expected).", "passwordHash");
if (storedSalt.Length != 128) throw new ArgumentException("Invalid length of password salt (128 bytes expected).", "passwordHash");
using (var hmac = new System.Security.Cryptography.HMACSHA512(storedSalt))
{
var computedHash = hmac.ComputeHash(System.Text.Encoding.UTF8.GetBytes(password));
for (int i = 0; i < computedHash.Length; i++)
{
if (computedHash[i] != storedHash[i]) return false;
}
}
return true;
}
}
}
ASP.NET Core App Settings (Development)
Configuration file with application settings that are specific to the development environment, including the connection string for the local SQLite development database.
{
"ConnectionStrings": {
"WebApiDatabase": "Data Source=LocalDatabase.db"
},
"Logging": {
"LogLevel": {
"Default": "Debug",
"System": "Information",
"Microsoft": "Information"
}
}
}
ASP.NET Core App Settings
Root configuration file containing default application settings for all environments (unless overridden in environment config). Includes placeholder for your production SQL Server connection string.
IMPORTANT: The "Secret"
property is used by the api to sign and verify JWT tokens for authentication, update it with your own random string to ensure nobody else can generate a JWT to gain unauthorised access to your application.
{
"AppSettings": {
"Secret": "THIS IS USED TO SIGN AND VERIFY JWT TOKENS, REPLACE IT WITH YOUR OWN SECRET, IT CAN BE ANY STRING"
},
"ConnectionStrings": {
"WebApiDatabase": "ENTER PRODUCTION SQL SERVER CONNECTION STRING HERE"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*"
}
ASP.NET Core Program
The program class is a console app that is the main entry point to start the application, it configures and launches the web api host and web server using an instance of IHostBuilder
. ASP.NET Core applications require a host in which to execute.
Kestrel is the web server used in the example, it's a new cross-platform web server for ASP.NET Core that's included in new project templates by default. Kestrel is fine to use on it's own for internal applications and development, but for public facing websites and applications it should sit behind a more mature reverse proxy server (IIS, Apache, Nginx etc) that will receive HTTP requests from the internet and forward them to Kestrel after initial handling and security checks.
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;
namespace WebApi
{
public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>()
.UseUrls("http://localhost:4000");
});
}
}
ASP.NET Core Startup
The startup class configures the request pipeline of the application and how all requests are handled.
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Hosting;
using WebApi.Helpers;
using WebApi.Services;
using AutoMapper;
using Microsoft.IdentityModel.Tokens;
using System.Text;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using System;
namespace WebApi
{
public class Startup
{
private readonly IWebHostEnvironment _env;
private readonly IConfiguration _configuration;
public Startup(IWebHostEnvironment env, IConfiguration configuration)
{
_env = env;
_configuration = configuration;
}
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
// use sql server db in production and sqlite db in development
if (_env.IsProduction())
services.AddDbContext<DataContext>();
else
services.AddDbContext<DataContext, SqliteDataContext>();
services.AddCors();
services.AddControllers();
services.AddAutoMapper(AppDomain.CurrentDomain.GetAssemblies());
// configure strongly typed settings objects
var appSettingsSection = _configuration.GetSection("AppSettings");
services.Configure<AppSettings>(appSettingsSection);
// configure jwt authentication
var appSettings = appSettingsSection.Get<AppSettings>();
var key = Encoding.ASCII.GetBytes(appSettings.Secret);
services.AddAuthentication(x =>
{
x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(x =>
{
x.Events = new JwtBearerEvents
{
OnTokenValidated = context =>
{
var userService = context.HttpContext.RequestServices.GetRequiredService<IUserService>();
var userId = int.Parse(context.Principal.Identity.Name);
var user = userService.GetById(userId);
if (user == null)
{
// return unauthorized if user no longer exists
context.Fail("Unauthorized");
}
return Task.CompletedTask;
}
};
x.RequireHttpsMetadata = false;
x.SaveToken = true;
x.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(key),
ValidateIssuer = false,
ValidateAudience = false
};
});
// configure DI for application services
services.AddScoped<IUserService, UserService>();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, DataContext dataContext)
{
// migrate any database changes on startup (includes initial db creation)
dataContext.Database.Migrate();
app.UseRouting();
// global cors policy
app.UseCors(x => x
.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader());
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints => endpoints.MapControllers());
}
}
}
ASP.NET Core Web Api csproj
The csproj (C# project) is an MSBuild based file that contains target framework and NuGet package dependency information for the application.
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AutoMapper" Version="9.0.0" />
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="7.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="3.1.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="3.1.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="3.1.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="3.1.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="3.1.0" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="5.6.0" />
</ItemGroup>
</Project>
Need Some ASP.NET Core Help?
Search fiverr for freelance ASP.NET Core developers.
Follow me for updates
When I'm not coding...
Me and Tina are on a motorcycle adventure around Australia.
Come along for the ride!