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 --versionto 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.csusingSystem;usingSystem.Security.Cryptography;usingMicrosoft.AspNetCore.Cryptography.KeyDerivation;namespaceTodoApi.Utils{publicstaticclassHashFunction{publicstatic(string,string)CreateHashAndSalt(stringpassword){byte[]salt=newbyte[128/8];using(varrng=RandomNumberGenerator.Create()){rng.GetBytes(salt);}stringhashed=Convert.ToBase64String(KeyDerivation.Pbkdf2(password:password,salt:salt,prf:KeyDerivationPrf.HMACSHA1,iterationCount:10000,numBytesRequested:256/8));(stringsalt,stringhashed)results=(Convert.ToBase64String(salt),hashed);returnresults;}publicstaticboolCheckPassword(stringpassword,stringsalt,stringhash){stringhashed=Convert.ToBase64String(KeyDerivation.Pbkdf2(password:password,salt:Convert.FromBase64String(salt),prf:KeyDerivationPrf.HMACSHA1,iterationCount:10000,numBytesRequested:256/8));if(hash!=hashed)returnfalse;returntrue;}}}dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer
// JWTAuthentication.csusingSystem;usingSystem.Text;usingSystem.Linq;usingSystem.IdentityModel.Tokens.Jwt;usingMicrosoft.IdentityModel.Tokens;usingSystem.Security.Claims;namespaceTodoApi.Utils{publicstaticclassJWTAuthentication{publicstaticstringGenerateJwtToken(stringuserid){vartokenHandler=newJwtSecurityTokenHandler();// var tokenKey = Encoding.ASCII.GetBytes(key);vartokenDescriptor=newSecurityTokenDescriptor{Subject=newClaimsIdentity(newClaim[]{newClaim(ClaimTypes.Name,userid)}),NotBefore=DateTime.UtcNow,Expires=DateTime.UtcNow.AddHours(3),IssuedAt=DateTime.UtcNow,Issuer="chitsanupong",Audience="public",SigningCredentials=newSigningCredentials(newSymmetricSecurityKey(Encoding.UTF8.GetBytes("1234567812345678")),SecurityAlgorithms.HmacSha256Signature),};vartoken=tokenHandler.CreateToken(tokenDescriptor);returntokenHandler.WriteToken(token);}publicstaticstringValidateJwtToken(stringtoken){vartokenHandler=newJwtSecurityTokenHandler();try{tokenHandler.ValidateToken(token,newTokenValidationParameters{ValidateIssuerSigningKey=true,IssuerSigningKey=newSymmetricSecurityKey(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},outSecurityTokenvalidatedToken);varjwtToken=(JwtSecurityToken)validatedToken;varuserid=jwtToken.Claims.First(x =>x.Type=="unique_name").Value;// return account id from JWT token if validation successfulreturnuserid;}catch{// return null if validation failsreturnnull;}}}}- 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")]publicIActionResultGet([FromHeader]stringAuthorization){// validate tokentry{string[]authorization=Authorization.Split(' ');stringtoken=authorization[1];stringuserid=JWTAuthentication.ValidateJwtToken(token);if(userid==null)returnStatusCode(401,new{message="Invalid Token"});}catch(Exceptione){returnStatusCode(500,new{message=e.ToString()});}vardb=newAMCDbContext();vartodoLists=db.Activities.Select(s =>s);returnOk(todoLists);}// Get todo list depends on id[HttpGet][Route("activities/{id}")]publicIActionResultGet(uintid,[FromHeader]stringAuthorization){// validate tokentry{string[]authorization=Authorization.Split(' ');stringtoken=authorization[1];stringuserid=JWTAuthentication.ValidateJwtToken(token);if(userid==null)returnStatusCode(401,new{message="Invalid Token"});}catch(Exceptione){returnStatusCode(500,new{message=e.ToString()});}vardb=newAMCDbContext();vartodoLists=db.Activities.Where(s =>s.Id==id).Select(s =>s);if(!todoLists.Any())returnNotFound();returnOk(todoLists);}// Create a todo list[HttpPost][Route("activities")]publicIActionResultPost([FromBody]Activitytodo,[FromHeader]stringAuthorization){// validate tokentry{string[]authorization=Authorization.Split(' ');stringtoken=authorization[1];stringuserid=JWTAuthentication.ValidateJwtToken(token);if(userid==null)returnStatusCode(401,new{message="Invalid Token"});}catch(Exceptione){returnStatusCode(500,new{message=e.ToString()});}try{vardb=newAMCDbContext();db.Activities.Add(todo);db.SaveChanges();}catch(Exceptione){returnStatusCode(500,new{message=e.ToString()});}returnStatusCode(201);}// Update a todo list[HttpPut][Route("activities/{id}")]publicIActionResultPut([FromBody]Activitytodo,[FromHeader]stringAuthorization,uintid){// validate tokentry{string[]authorization=Authorization.Split(' ');stringtoken=authorization[1];stringuserid=JWTAuthentication.ValidateJwtToken(token);if(userid==null)returnStatusCode(401,new{message="Invalid Token"});}catch(Exceptione){returnStatusCode(500,new{message=e.ToString()});}try{vardb=newAMCDbContext();vartodoList=db.Activities.Where(s =>s.Id==id).Select(s =>s);if(!todoList.Any())returnNotFound();vartd=todoList.First();td.Id=id;td.Name=todo.Name;td.When=todo.When;db.SaveChanges();}catch(Exceptione){returnStatusCode(500,new{message=e.ToString()});}returnOk();}[HttpDelete][Route("activities/{id}")]publicIActionResultDelete([FromHeader]stringAuthorization,uintid){// validate tokentry{string[]authorization=Authorization.Split(' ');stringtoken=authorization[1];stringuserid=JWTAuthentication.ValidateJwtToken(token);if(userid==null)returnStatusCode(401,new{message="Invalid Token"});}catch(Exceptione){returnStatusCode(500,new{message=e.ToString()});}try{vardb=newAMCDbContext();vartodoList=db.Activities.Find(id);db.Activities.Remove(todoList);db.SaveChanges();}catch(Exceptione){returnStatusCode(500,new{message=e.ToString()});}returnOk();}[HttpPost][Route("tokens")]publicIActionResultLogin([FromBody]Accountaccount){if(account.userid==null||account.password==null)returnBadRequest();try{vardb=newAMCDbContext();varuser=db.Users.Where(s =>s.Id==account.userid).Select(s =>s);if(!user.Any())returnUnauthorized();varu=user.First();// check password with hash functionboolisVerified=HashFunction.CheckPassword(account.password,u.Salt,u.Password);if(!isVerified)returnUnauthorized();// send token if the username and password is truevartoken=JWTAuthentication.GenerateJwtToken(account.userid);returnOk(new{token=token});}catch(Exceptione){returnStatusCode(500,new{message=e.ToString()});}}[HttpPost][Route("signup")]publicIActionResultSignUp([FromBody]Accountaccount){(stringsalt,stringhash)hashedAndSalt=HashFunction.CreateHashAndSalt(account.password);stringsalt=hashedAndSalt.salt;stringhash=hashedAndSalt.hash;try{vardb=newAMCDbContext();db.Users.Add(newUser(){Id=account.userid,Password=hash,Salt=salt,});db.SaveChanges();}catch(Exceptione){returnStatusCode(500,new{message=e.ToString()});}returnStatusCode(201);}dotnet add package Microsoft.AspNet.WebApi.Cors
// add variablereadonlystringMyAllowSpecificOrigins="_myAllowSpecificOrigins";publicvoidConfigureServices(IServiceCollectionservices){services.AddControllers();services.AddSwaggerGen(c =>{c.SwaggerDoc("v1",newOpenApiInfo{Title="TodoApi",Version="v1"});});}publicvoidConfigure(IApplicationBuilderapp,IWebHostEnvironmentenv){if(env.IsDevelopment()){app.UseDeveloperExceptionPage();app.UseSwagger();app.UseSwaggerUI(c =>c.SwaggerEndpoint("/swagger/v1/swagger.json","TodoApi v1"));}app.UseHttpsRedirection();app.UseRouting();// Add app.UseCorsapp.UseCors(options =>options.AllowAnyHeader().AllowAnyMethod().AllowAnyOrigin());app.UseAuthorization();app.UseEndpoints(endpoints =>{endpoints.MapControllers();});}// Startup.cspublicvoidConfigureServices(IServiceCollectionservices){services.AddControllers();// authenticate JWT in every APIservices.AddAuthentication(options =>{options.DefaultAuthenticateScheme=JwtBearerDefaults.AuthenticationScheme;options.DefaultChallengeScheme=JwtBearerDefaults.AuthenticationScheme;options.DefaultScheme=JwtBearerDefaults.AuthenticationScheme;}).AddJwtBearer(options =>{options.SaveToken=true;options.RequireHttpsMetadata=false;options.TokenValidationParameters=newMicrosoft.IdentityModel.Tokens.TokenValidationParameters(){ValidateIssuer=true,ValidateAudience=true,ValidIssuer="chitsanupong",ValidAudience="public",IssuerSigningKey=newSymmetricSecurityKey(Encoding.UTF8.GetBytes(Program.SecurityKey))};});services.AddSwaggerGen(c =>{c.SwaggerDoc("v1",newOpenApiInfo{Title="TodoApi",Version="v1"});});}// TodoApiController[Route("activities")][HttpGet][Authorize(Roles="user")]// add authorizepublicIActionResultGet(){vardb=newAMCDbContext();varactivities=db.Activities.Select(s =>s).OrderBy(a =>a.When);if(!activities.Any())returnNoContent();returnOk(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