Web API with ASP.NET Core created for todo list application. This Web API include GET, POST, PUT, DELETE, connects with MariaDB, store password with hash & salt, and authentication with JWT.
Select MariaDB as a SQL database server. MariaDB is an open-source relational database management system(RDBMS) like MySQL.
- Install MariaDB
- Install Xampp to run database server on localhost for Web API development
- Open Xampp, run Apache and mySQL. If the port error is occurred, you can change the port in Config section.
- Create database (todolist) and two tables: user and activity
- Install .NET 5.0 SDK from official website.
- run
dotnet --version
to check whether dotnet is already installed or not. - run these command to create web api project
dotnet new webapi -o myproject
cd myproject
- Install entity framework to enable ASP.NET Web API project to connect to MariaDB.
dotnet tool update --global dotnet-ef
dotnet add package Microsoft.EntityFrameworkCore.Design
dotnet add package Pomelo.EntityFrameworkCore.MySql
dotnet ef dbcontext scaffold "server=localhost;port=3307;user=root;password=todolist;database=todolist" Pomelo.EntityFrameworkCore.MySql -c AMCDbContext -o Models
- After scaffolding, the models folder is created depends on your database table list.
From: EntityFramework Tutorial Then, go to phpMyAdmin and check the id column to auto-increment.
Hash and Salt is used to protect the password that store in the database. Generally, password is stored in plain text which is not secured at all. Therefore, salt and hash concept are life savior to secure the password with these steps:
- Random salt in Byte[] type
- Add password in plain text and random salt to the hash function
- The result of hash function is hash in Base64
- Store both hash as password and salt in the database instead of plain text.
- When you want to verify, get password from user and salt to the hash function.
- The result should be matched with the hash that kept in database.
- If both hash from database and from hash function are the same, password is true.
// HashFunction.cs
using System;
using System.Security.Cryptography;
using Microsoft.AspNetCore.Cryptography.KeyDerivation;
namespace TodoApi.Utils
{
public static class HashFunction
{
public static (string, string) CreateHashAndSalt(string password)
{
byte[] salt = new byte[128/8];
using (var rng = RandomNumberGenerator.Create())
{
rng.GetBytes(salt);
}
string hashed = Convert.ToBase64String(KeyDerivation.Pbkdf2(
password: password,
salt: salt,
prf: KeyDerivationPrf.HMACSHA1,
iterationCount: 10000,
numBytesRequested: 256/8));
(string salt, string hashed) results = (Convert.ToBase64String(salt), hashed);
return results;
}
public static bool CheckPassword(string password, string salt, string hash)
{
string hashed = Convert.ToBase64String(KeyDerivation.Pbkdf2(
password: password,
salt: Convert.FromBase64String(salt),
prf: KeyDerivationPrf.HMACSHA1,
iterationCount: 10000,
numBytesRequested: 256/8));
if (hash != hashed) return false;
return true;
}
}
}
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer
// JWTAuthentication.cs
using System;
using System.Text;
using System.Linq;
using System.IdentityModel.Tokens.Jwt;
using Microsoft.IdentityModel.Tokens;
using System.Security.Claims;
namespace TodoApi.Utils
{
public static class JWTAuthentication
{
public static string GenerateJwtToken(string userid)
{
var tokenHandler = new JwtSecurityTokenHandler();
// var tokenKey = Encoding.ASCII.GetBytes(key);
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(new Claim[] { new Claim(ClaimTypes.Name, userid)}),
NotBefore = DateTime.UtcNow,
Expires = DateTime.UtcNow.AddHours(3),
IssuedAt = DateTime.UtcNow,
Issuer = "chitsanupong",
Audience = "public",
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(Encoding.UTF8.GetBytes("1234567812345678")), SecurityAlgorithms.HmacSha256Signature),
};
var token = tokenHandler.CreateToken(tokenDescriptor);
return tokenHandler.WriteToken(token);
}
public static string ValidateJwtToken(string token)
{
var tokenHandler = new JwtSecurityTokenHandler();
try
{
tokenHandler.ValidateToken(token, new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes("1234567812345678")),
ValidateIssuer = true,
ValidateAudience = true,
ValidIssuer = "chitsanupong",
ValidAudience = "public",
// set clockskew to zero so tokens expire exactly at token expiration time (instead of 5 minutes later)
ClockSkew = TimeSpan.Zero
}, out SecurityToken validatedToken);
var jwtToken = (JwtSecurityToken)validatedToken;
var userid = jwtToken.Claims.First(x => x.Type == "unique_name").Value;
// return account id from JWT token if validation successful
return userid;
}
catch
{
// return null if validation fails
return null;
}
}
}
}
- ASP.NET Core Authentication with JWT (JSON Web Token)
- ASP.NET Core 3.1 - Create and Validate JWT Tokens + Use Custom JWT Middleware
- Create And Validate JWT Token In .NET 5.0
// Get all todo list
[HttpGet]
[Route("activities")]
public IActionResult Get([FromHeader] string Authorization)
{
// validate token
try {
string[] authorization = Authorization.Split(' ');
string token = authorization[1];
string userid = JWTAuthentication.ValidateJwtToken(token);
if (userid == null) return StatusCode(401, new {message = "Invalid Token"});
} catch (Exception e) {
return StatusCode(500, new {message = e.ToString()});
}
var db = new AMCDbContext();
var todoLists = db.Activities.Select(s => s);
return Ok(todoLists);
}
// Get todo list depends on id
[HttpGet]
[Route("activities/{id}")]
public IActionResult Get(uint id, [FromHeader] string Authorization)
{
// validate token
try {
string[] authorization = Authorization.Split(' ');
string token = authorization[1];
string userid = JWTAuthentication.ValidateJwtToken(token);
if (userid == null) return StatusCode(401, new {message = "Invalid Token"});
} catch (Exception e) {
return StatusCode(500, new {message = e.ToString()});
}
var db = new AMCDbContext();
var todoLists = db.Activities.Where(s => s.Id == id).Select(s => s);
if (!todoLists.Any()) return NotFound();
return Ok(todoLists);
}
// Create a todo list
[HttpPost]
[Route("activities")]
public IActionResult Post([FromBody] Activity todo, [FromHeader] string Authorization)
{
// validate token
try {
string[] authorization = Authorization.Split(' ');
string token = authorization[1];
string userid = JWTAuthentication.ValidateJwtToken(token);
if (userid == null) return StatusCode(401, new {message = "Invalid Token"});
} catch (Exception e) {
return StatusCode(500, new {message = e.ToString()});
}
try {
var db = new AMCDbContext();
db.Activities.Add(todo);
db.SaveChanges();
} catch (Exception e) {
return StatusCode(500, new {message = e.ToString()});
}
return StatusCode(201);
}
// Update a todo list
[HttpPut]
[Route("activities/{id}")]
public IActionResult Put([FromBody] Activity todo, [FromHeader] string Authorization, uint id)
{
// validate token
try {
string[] authorization = Authorization.Split(' ');
string token = authorization[1];
string userid = JWTAuthentication.ValidateJwtToken(token);
if (userid == null) return StatusCode(401, new {message = "Invalid Token"});
} catch (Exception e) {
return StatusCode(500, new {message = e.ToString()});
}
try {
var db = new AMCDbContext();
var todoList = db.Activities.Where(s => s.Id == id).Select(s => s);
if (!todoList.Any()) return NotFound();
var td = todoList.First();
td.Id = id;
td.Name = todo.Name;
td.When = todo.When;
db.SaveChanges();
} catch (Exception e) {
return StatusCode(500, new {message = e.ToString()});
}
return Ok();
}
[HttpDelete]
[Route("activities/{id}")]
public IActionResult Delete([FromHeader] string Authorization, uint id)
{
// validate token
try {
string[] authorization = Authorization.Split(' ');
string token = authorization[1];
string userid = JWTAuthentication.ValidateJwtToken(token);
if (userid == null) return StatusCode(401, new {message = "Invalid Token"});
} catch (Exception e) {
return StatusCode(500, new {message = e.ToString()});
}
try {
var db = new AMCDbContext();
var todoList = db.Activities.Find(id);
db.Activities.Remove(todoList);
db.SaveChanges();
} catch (Exception e) {
return StatusCode(500, new {message = e.ToString()});
}
return Ok();
}
[HttpPost]
[Route("tokens")]
public IActionResult Login([FromBody] Account account)
{
if (account.userid == null || account.password == null) return BadRequest();
try {
var db = new AMCDbContext();
var user = db.Users.Where(s => s.Id == account.userid).Select(s => s);
if (!user.Any()) return Unauthorized();
var u = user.First();
// check password with hash function
bool isVerified = HashFunction.CheckPassword(account.password, u.Salt, u.Password);
if (!isVerified) return Unauthorized();
// send token if the username and password is true
var token = JWTAuthentication.GenerateJwtToken(account.userid);
return Ok(new { token = token });
} catch (Exception e) {
return StatusCode(500, new {message = e.ToString()});
}
}
[HttpPost]
[Route("signup")]
public IActionResult SignUp([FromBody] Account account)
{
(string salt, string hash) hashedAndSalt = HashFunction.CreateHashAndSalt(account.password);
string salt = hashedAndSalt.salt;
string hash = hashedAndSalt.hash;
try {
var db = new AMCDbContext();
db.Users.Add(new User(){
Id = account.userid,
Password = hash,
Salt = salt,
});
db.SaveChanges();
} catch (Exception e) {
return StatusCode(500, new {message = e.ToString()});
}
return StatusCode(201);
}
dotnet add package Microsoft.AspNet.WebApi.Cors
// add variable
readonly string MyAllowSpecificOrigins = "_myAllowSpecificOrigins";
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "TodoApi", Version = "v1" });
});
}
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.UseHttpsRedirection();
app.UseRouting();
// Add app.UseCors
app.UseCors(options => options.AllowAnyHeader().AllowAnyMethod().AllowAnyOrigin());
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
// Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
// authenticate JWT in every API
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 Microsoft.IdentityModel.Tokens.TokenValidationParameters() {
ValidateIssuer = true,
ValidateAudience = true,
ValidIssuer = "chitsanupong",
ValidAudience = "public",
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Program.SecurityKey))
};
});
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "TodoApi", Version = "v1" });
});
}
// TodoApiController
[Route("activities")]
[HttpGet]
[Authorize(Roles = "user")] // add authorize
public IActionResult Get()
{
var db = new AMCDbContext();
var activities = db.Activities.Select(s => s).OrderBy(a => a.When);
if (!activities.Any()) return NoContent();
return Ok(activities);
}
- Message: "System.InvalidOperationException: Unable to track an instance of type 'Activity' because it does not have a primary key...".
- Solve: In AMCDbContext.cs file, you need to remove
entity.HasNoKey();
because it causes data from Web API does not have a primary key.
- Solve: In AMCDbContext.cs file, you need to remove