This article shows how to build a ToDo list application using .Net 5 Web API, JWT authentication and AspNetCore Identity. Microsoft SQL Server is used to view the database and tables.
The article will show all the necessary steps that are required to build a complete ToDo application backend. In the end the article demonstrate that only logged in users are able to access the ToDo list end points.
Tools
Visual Studio 2019
Microsoft SQL Server
User Stories
Create a new project using Visual Studio 2019 community edition.
Choose the ASP.NET Core Web API template. The next step would be an option to give a project name and choose a location for the project.
Choose .NET 5.0 as the target framework, nose for the authentication type field and unselect the configure for HTTPS. Then click create to create the project.
The project template will have this structure.
Install the latest version of the below packages using NuGet package manager. Th NuGet package manager can be found by right clicking the project, in this example, the ToDoAPI, and select manage NuGet packages.
Microsoft.EntityFrameworkCore.SqlServer
Microsoft.EntityFrameworkCore.Tools
Microsoft.AspNetCore.Identity.EntityFrameworkCore
Microsoft.AspNetCore.Identity
Microsoft.AspNetCore.Authentication.JwtBearer
Microsoft.VisualStudio.Web.CodeGeneration.Design
Create an Authentication folder that will contain a ApplicationUser.cs class that will inherit the IdentityUser class and Response.cs class that will return a message and a status code when a user register or login to the application. The IdentityUser class is a part of AspNetCore Identity.
ApplicationUser.cs
using Microsoft.AspNetCore.Identity;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace ToDoAPI.Authentication
{
public class ApplicationUser: IdentityUser
{ }
}
Response.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace ToDoAPI.Authentication
{
public class Response
{
public string Status { get; set; }
public string Message { get; set; }
}
}
Create a models folder Models that will contain a RegisterModel.cs class for user registration, a LoginModel.cs class for user login, UserRoles.cs for user roles and ToDoItemModel.cs for to do items. It will also contain the ApplicationDbContext.cs file that maps the models to the tables that will be created through migration.
The RegisterModel.cs, LoginModel.cs and UserRoles.cs will be bounded to the identity tables. This means that only the fields that are described in the models will be required when a user register and login to the application. The roles will show the roles that a user can have, e.g., “admin”.
RegisterModel.cs
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
namespace ToDoAPI.Models
{
public class RegisterModel
{
[Required(ErrorMessage="Username is required")]
public string Username { get; set; }
[RegularExpression(@"^[\w!#$%&'*+\-/=?\^_`{|}~]+(\.
[\w!#$%&'*+\-/=?\^_`{|}~]+)*"+"@"+@"((([\-\w]+\.)+[a-zA-
Z]{2,4})|(([0-9]{1,3}\.){3}[0-9]{1,3}))$",
ErrorMessage="You have entered an invalid email address")]
[Required(ErrorMessage="Email is required")]
public string Email { get; set; }
[RegularExpression(@"^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?
&])[A-Za-z\d@$!%*?&]{8,}$",
ErrorMessage="Minimum eight characters, at least one uppercase
letter, one lowercase letter, one number and one special
character")]
[Required(ErrorMessage="Password is required")]
public string Password { get; set; }
}
}
LoginModel.cs
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
namespace ToDoAPI.Models
{
public class LoginModel
{
[Required(ErrorMessage = "Username is required")]
public string Username { get; set; }
[RegularExpression(@"^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?
&])[A-Za-z\d@$!%*?&]{8,}$",
ErrorMessage="Minimum eight characters, at least one uppercase
letter,
"+"one lowercase letter, one number and one special
character")]
[Required(ErrorMessage="Password is required")]
public string Password { get; set; }
}
}
UserRoles.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace ToDoAPI.Models
{
public class UserRoles
{
public const string Admin = "Admin";
public const string User = "User";
}
}
ToDoItemModel.cs
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using System.Threading.Tasks;
namespace ToDoAPI.Models
{
public class ToDoItemModel
{
[Key]
public int ItemId { get; set; }
[Required(ErrorMessage = "ItemName is required")]
[Column(TypeName = "nvarchar(100)")]
public string ItemName { get; set; }
[Required(ErrorMessage = "ItemDescription is required")]
[Column(TypeName = "nvarchar(100)")]
public string ItemDescription { get; set; }
[Required(ErrorMessage = "ItemStatus is required")]
[Column(TypeName = "bit")]
public bool ItemStatus { get; set; }
}
}
ApplicationDbContext.cs
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using System;usingSystem.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using ToDoAPI.Authentication;
namespace ToDoAPI.Models
{
public class ApplicationDbContext: IdentityDbContext<ApplicationUser>
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext>
options) : base(options)
{
}
public DbSet<ToDoItemModel> ToDoItems { get; set; }
protected override void OnModelCreating(ModelBuilderbuilder)
{
builder.Entity<ToDoItemModel>(entity=>
{
entity.Property(e =>e.ItemName)
.IsRequired()
.HasMaxLength(100);
entity.Property(e => e.ItemDescription)
.IsRequired()
.HasMaxLength(100);
entity.Property(e => e.ItemStatus)
.IsRequired()
.HasMaxLength(1);
});
base.OnModelCreating(builder);
}
}
}
The folder structure of the project now looks as shown below.
Modify the appsettings.json file to add the connection string and the jwt token secrete string, valid issuer, which is the port for the backend server and valid audience which is the port for the frontend server.
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*",
"ConnectionStrings": {
"SQLConnection": "Server=.;
Database=ToDoDB;
Trusted_Connection = True;
Integrated Security = true"
},
"JWT": {
"ValidAudience": "http: //localhost:4200",
"ValidIssuer": "http://localhost:24288",
"Secret": "MySecretStringMuuustBeVeeeeeeeeeeryLooooooooOng"
}
}
In the ConfigureServices method in the Startup.cs file add the DbContext and show that the application uses the SQL Server (it can also use MySQL, Postgres, etc.) and add the connection string that was described in the appsettings.json file, add JwtBearer authentication and add AspNetCore Identity as shown below. In the Configure method in the Startup.cs file add that the application uses authentication.
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.IdentityModel.Tokens;
using Microsoft.OpenApi.Models;
using System;
using System.Collections.Generic;'
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using ToDoAPI.Authentication;
using ToDoAPI.Models;
namespace ToDoAPI
{
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.AddControllers();
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo
{
Title = "ToDoAPI",
Version = "v1"
});
});
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString
("SQLConnection")));
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme =
JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme =
JwtBearerDefaults.AuthenticationScheme;
options.DefaultScheme =
JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.SaveToken = true;
options.RequireHttpsMetadata = false;
options.TokenValidationParameters = new
TokenValidationParameters()
{
ValidateIssuer = true,
ValidateAudience = true,
ValidAudience = Configuration["JWT:ValidAudience"],
ValidIssuer = Configuration["JWT:ValidIssuer"],
IssuerSigningKey = new SymmetricSecurityKey
(Encoding.UTF8.GetBytes
(Configuration["JWT:Secret"]))
};
});
}
// 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();
app.UseSwagger();
app.UseSwaggerUI(c => c.SwaggerEndpoint
("/swagger/v1/swagger.json", "ToDoAPI v1"));
}
app.UseAuthentication();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
}
}
Inside the Controllers folder create a Web API controller Authentication. This is done by right-clicking on the Controllers folder, select add, choose controller from the drop-down and then select API Controller — Empty.
AuthenticationController.cs
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.IdentityModel.Tokens;
using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
using System.Linq;
using System.Security.Claims;
using System.Text;
using System.Threading.Tasks;
using ToDoAPI.Authentication;
using ToDoAPI.Models;
namespace ToDoAPI.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class AuthenticationController : ControllerBase
{
private readonly UserManager<ApplicationUser> userManager;
private readonly RoleManager<IdentityRole> roleManager;
private readonly IConfiguration_configuration;
public AuthenticationController(
UserManager<ApplicationUser> userManager,
RoleManager<IdentityRole> roleManager,
IConfigurationconfiguration)
{
this.userManager = userManager;
this.roleManager = roleManager;
_configuration = configuration;
}
[HttpPost]
[Route("login")]
public async Task<IActionResult> Login([FromBody] LoginModelmodel)
{
var user = await userManager.FindByNameAsync(model.Username);
if (user != null && await userManager.CheckPasswordAsync(user,
model.Password))
{
var userRoles= await userManager.GetRolesAsync(user);
var authClaims = new List<Claim>
{
new Claim("name", user.UserName),
new Claim(JwtRegisteredClaimNames.Jti,
Guid.NewGuid().ToString()),
};
foreach (var userRole in userRoles)
{
authClaims.Add(newClaim("role", userRole));
}
var authSigningKey = new SymmetricSecurityKey
(Encoding.UTF8.GetBytes(_configuration["JWT:Secret"]));
var token = new JwtSecurityToken(
issuer: _configuration["JWT:ValidIssuer"],
audience: _configuration["JWT:ValidAudience"],
expires: DateTime.Now.AddHours(3),
claims: authClaims,
signingCredentials: newSigningCredentials
(authSigningKey, SecurityAlgorithms.HmacSha256)
);
return Ok(new
{
token = new JwtSecurityTokenHandler()
.WriteToken(token),
expiration = token.ValidTo
});
}
return Unauthorized();
}
[HttpPost]
[Route("register")]
public async Task<IActionResult> Register([FromBody]
RegisterModelmodel)
{
var userExists = await userManager.FindByNameAsync
(model.Username);
if (userExists!=null)
{
return StatusCode(
StatusCodes.Status500InternalServerError,
newResponse {
Status="Error",
Message="User already exists!"
});
};
ApplicationUser user = new ApplicationUser()
{
Email=model.Email,
SecurityStamp=Guid.NewGuid().ToString(),
UserName=model.Username
};
var result = await userManager.CreateAsync
(user, model.Password);
if (!result.Succeeded)
{
return StatusCode(
StatusCodes.Status500InternalServerError,
newResponse {
Status="Error",
Message="User creation failed! Please check user
details and try again."
});
}
return Ok(new Response {
Status="Success",
Message="User created successfully" });
}
[HttpPost]
[Route("register-admin")]
public async Task<IActionResult> RegisterAdmin([FromBody]
RegisterModelmodel)
{
var userExists = await userManager.FindByNameAsync
(model.Username);
if (userExists!=null)
{
return StatusCode(
StatusCodes.Status500InternalServerError,
newResponse {
Status="Error",
Message="User already exists!" });
};
ApplicationUser user = new ApplicationUser()
{
Email = model.Email,
SecurityStamp = Guid.NewGuid().ToString(),
UserName = model.Username
};
var result = await userManager.CreateAsync(user,
model.Password);
if (!result.Succeeded)
{
return StatusCode(
StatusCodes.Status500InternalServerError,
new Response {
Status="Error",
Message="User creation failed! Please check user
details and try again."
});
}
if (!awaitroleManager.RoleExistsAsync(UserRoles.Admin))
{
await roleManager.CreateAsync(new IdentityRole
(UserRoles.Admin));
}
if (!awaitroleManager.RoleExistsAsync(UserRoles.User))
{
await roleManager.CreateAsync(new IdentityRole
(UserRoles.User));
}
if (await roleManager.RoleExistsAsync(UserRoles.Admin))
{
await userManager.AddToRoleAsync(user, UserRoles.Admin);
}
return Ok(new Response
{
Status = "Success",
Message = "User created successfully"
});
Create a Web API controller ToDoItem. This is done by right-clicking on the Controllers folder, select add, choose “New Scaffolded Item” from the drop-down and then select API Controller with actions, using Entity Framework.
Choose the ToDoItemModel for the model class, ApplicationDbContext for the data context class and a controller name.
ToDoItemController.cs
Add [Authorize] to allow only user who have logged in and have a valid JWT token to access the ToDoItem APIs.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using ToDoAPI.Models;
namespace ToDoAPI.Controllers
{
[Authorize]
[Route("api/[controller]")]
[ApiController]
public class ToDoItemController : ControllerBase
{
private readonly ApplicationDbContext_context;
public ToDoItemController(ApplicationDbContextcontext)
{
_context = context;
}
// GET: api/ToDoItem
[HttpGet]
public async Task<ActionResult<IEnumerable<ToDoItemModel>>> GetToDoItems()
{
return await _context.ToDoItems.ToListAsync();
}
// GET: api/ToDoItem/5
[HttpGet("{id}")]
public async Task<ActionResult<ToDoItemModel>> GetToDoItemModel(int id)
{
var toDoItemModel = await _context.ToDoItems.FindAsync(id);
if (toDoItemModel == null)
{
return NotFound();
}
return toDoItemModel;
}
// PUT: api/ToDoItem/5
// To protect from overposting attacks, see https://go.microsoft.com/fwlink/?linkid=2123754
[HttpPut("{id}")]
public async Task<IActionResult> PutToDoItemModel
(int id, ToDoItemModel toDoItemModel)
{
if (id != toDoItemModel.ItemId)
{
return BadRequest();
}
_context.Entry(toDoItemModel).State=EntityState.Modified;
try
{
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!ToDoItemModelExists(id))
{
return NotFound();
}
else
{
throw;
}
}
return NoContent();
}
// POST: api/ToDoItem
// To protect from overposting attacks, see https://go.microsoft.com/fwlink/?linkid=2123754
[HttpPost]
public async Task<ActionResult<ToDoItemModel>>
PostToDoItemModel(ToDoItemModel toDoItemModel)
{
_context.ToDoItems.Add(toDoItemModel);
await _context.SaveChangesAsync();
return CreatedAtAction("GetToDoItemModel", new
{
id=toDoItemModel.ItemId
},
toDoItemModel);
}
// DELETE: api/ToDoItem/5
[HttpDelete("{id}")]
public async Task<IActionResult> DeleteToDoItemModel(int id)
{
var toDoItemModel=await _context.ToDoItems.FindAsync(id);
if (toDoItemModel == null)
{
return NotFound();
}
_context.ToDoItems.Remove(toDoItemModel);
await _context.SaveChangesAsync();
return NoContent();
}
private bool ToDoItemModelExists(int id)
{
return _context.ToDoItems.Any(e=>e.ItemId==id);
}
}
}
Create a migration script by using “add-migration” command in the package manger console.
A migrations folder is created in the project. The folder structure of the project now looks as shown below.
Create a database and tables by using “update-database” command in the package manger console. Use the Microsoft SQL Server object explorer to see the created database and tables.
Use postman to test the APIs. If you try to access the ToDoItem before registering to the application, you get a 401 unauthorized status code.
Once a user has registered the user can login and get a valid JWT token.
This can then be used to access the ToDoItem Web APIs.
Source: Medium - Sakhile Msibi
The Tech Platform
Comments