Latest commit

History

35 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

Todo List Web API

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.

Technologies

imageimageimageimage

Set up Database

Select MariaDB as a SQL database server. MariaDB is an open-source relational database management system(RDBMS) like MySQL.

  1. Install MariaDB
  2. Install Xampp to run database server on localhost for Web API development
  3. Open Xampp, run Apache and mySQL. If the port error is occurred, you can change the port in Config section.
  4. Create database (todolist) and two tables: user and activity

Set up ASP.NET Web API

  1. Install .NET 5.0 SDK from official website.
  2. run dotnet --version to check whether dotnet is already installed or not.
  3. run these command to create web api project
dotnet new webapi -o myproject
cd myproject
  1. 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
  1. After scaffolding, the models folder is created depends on your database table list.

Auto-increment on primary key (id) in Entity Framework

From: EntityFramework Tutorial Then, go to phpMyAdmin and check the id column to auto-increment.

Hash and Salt

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:

  1. Random salt in Byte[] type
  2. Add password in plain text and random salt to the hash function
  3. The result of hash function is hash in Base64
  4. Store both hash as password and salt in the database instead of plain text.
  5. When you want to verify, get password from user and salt to the hash function.
  6. The result should be matched with the hash that kept in database.
  7. 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;}}}

Install JWT for authentication

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;}}}}

Generate and Validate JWT Reference

GET Request

// 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);}

Post Request

// 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);}

PUT Request

// 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();}

Delete Request

[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();}

Login and Get Token

[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()});}}

Sign up

[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);}

Cors Setting

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();});}

Authorize header in every API

// 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);}

Reference

Problem Solving

  • 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.

About

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.

Topics

Resources

Stars

4 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Latest commit

History

35 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

Todo List Web API

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.

Technologies

imageimageimageimage

Set up Database

Select MariaDB as a SQL database server. MariaDB is an open-source relational database management system(RDBMS) like MySQL.

  1. Install MariaDB
  2. Install Xampp to run database server on localhost for Web API development
  3. Open Xampp, run Apache and mySQL. If the port error is occurred, you can change the port in Config section.
  4. Create database (todolist) and two tables: user and activity

Set up ASP.NET Web API

  1. Install .NET 5.0 SDK from official website.
  2. run dotnet --version to check whether dotnet is already installed or not.
  3. run these command to create web api project
dotnet new webapi -o myproject
cd myproject
  1. 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
  1. After scaffolding, the models folder is created depends on your database table list.

Auto-increment on primary key (id) in Entity Framework

From: EntityFramework Tutorial Then, go to phpMyAdmin and check the id column to auto-increment.

Hash and Salt

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:

  1. Random salt in Byte[] type
  2. Add password in plain text and random salt to the hash function
  3. The result of hash function is hash in Base64
  4. Store both hash as password and salt in the database instead of plain text.
  5. When you want to verify, get password from user and salt to the hash function.
  6. The result should be matched with the hash that kept in database.
  7. 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;}}}

Install JWT for authentication

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;}}}}

Generate and Validate JWT Reference

GET Request

// 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);}

Post Request

// 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);}

PUT Request

// 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();}

Delete Request

[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();}

Login and Get Token

[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()});}}

Sign up

[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);}

Cors Setting

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();});}

Authorize header in every API

// 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);}

Reference

Problem Solving

  • 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.

About

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.

Topics

Resources

Stars

4 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Latest commit

History

35 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

Todo List Web API

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.

Technologies

imageimageimageimage

Set up Database

Select MariaDB as a SQL database server. MariaDB is an open-source relational database management system(RDBMS) like MySQL.

  1. Install MariaDB
  2. Install Xampp to run database server on localhost for Web API development
  3. Open Xampp, run Apache and mySQL. If the port error is occurred, you can change the port in Config section.
  4. Create database (todolist) and two tables: user and activity

Set up ASP.NET Web API

  1. Install .NET 5.0 SDK from official website.
  2. run dotnet --version to check whether dotnet is already installed or not.
  3. run these command to create web api project
dotnet new webapi -o myproject
cd myproject
  1. 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
  1. After scaffolding, the models folder is created depends on your database table list.

Auto-increment on primary key (id) in Entity Framework

From: EntityFramework Tutorial Then, go to phpMyAdmin and check the id column to auto-increment.

Hash and Salt

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:

  1. Random salt in Byte[] type
  2. Add password in plain text and random salt to the hash function
  3. The result of hash function is hash in Base64
  4. Store both hash as password and salt in the database instead of plain text.
  5. When you want to verify, get password from user and salt to the hash function.
  6. The result should be matched with the hash that kept in database.
  7. 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;}}}

Install JWT for authentication

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;}}}}

Generate and Validate JWT Reference

GET Request

// 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);}

Post Request

// 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);}

PUT Request

// 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();}

Delete Request

[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();}

Login and Get Token

[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()});}}

Sign up

[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);}

Cors Setting

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();});}

Authorize header in every API

// 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);}

Reference

Problem Solving

  • 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.

About

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.

Topics

Resources

Stars

4 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Latest commit

History

35 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

Todo List Web API

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.

Technologies

imageimageimageimage

Set up Database

Select MariaDB as a SQL database server. MariaDB is an open-source relational database management system(RDBMS) like MySQL.

  1. Install MariaDB
  2. Install Xampp to run database server on localhost for Web API development
  3. Open Xampp, run Apache and mySQL. If the port error is occurred, you can change the port in Config section.
  4. Create database (todolist) and two tables: user and activity

Set up ASP.NET Web API

  1. Install .NET 5.0 SDK from official website.
  2. run dotnet --version to check whether dotnet is already installed or not.
  3. run these command to create web api project
dotnet new webapi -o myproject
cd myproject
  1. 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
  1. After scaffolding, the models folder is created depends on your database table list.

Auto-increment on primary key (id) in Entity Framework

From: EntityFramework Tutorial Then, go to phpMyAdmin and check the id column to auto-increment.

Hash and Salt

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:

  1. Random salt in Byte[] type
  2. Add password in plain text and random salt to the hash function
  3. The result of hash function is hash in Base64
  4. Store both hash as password and salt in the database instead of plain text.
  5. When you want to verify, get password from user and salt to the hash function.
  6. The result should be matched with the hash that kept in database.
  7. 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;}}}

Install JWT for authentication

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;}}}}

Generate and Validate JWT Reference

GET Request

// 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);}

Post Request

// 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);}

PUT Request

// 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();}

Delete Request

[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();}

Login and Get Token

[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()});}}

Sign up

[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);}

Cors Setting

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();});}

Authorize header in every API

// 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);}

Reference

Problem Solving

  • 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.

About

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.

Topics

Resources

Stars

4 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Latest commit

History

35 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

Todo List Web API

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.

Technologies

imageimageimageimage

Set up Database

Select MariaDB as a SQL database server. MariaDB is an open-source relational database management system(RDBMS) like MySQL.

  1. Install MariaDB
  2. Install Xampp to run database server on localhost for Web API development
  3. Open Xampp, run Apache and mySQL. If the port error is occurred, you can change the port in Config section.
  4. Create database (todolist) and two tables: user and activity

Set up ASP.NET Web API

  1. Install .NET 5.0 SDK from official website.
  2. run dotnet --version to check whether dotnet is already installed or not.
  3. run these command to create web api project
dotnet new webapi -o myproject
cd myproject
  1. 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
  1. After scaffolding, the models folder is created depends on your database table list.

Auto-increment on primary key (id) in Entity Framework

From: EntityFramework Tutorial Then, go to phpMyAdmin and check the id column to auto-increment.

Hash and Salt

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:

  1. Random salt in Byte[] type
  2. Add password in plain text and random salt to the hash function
  3. The result of hash function is hash in Base64
  4. Store both hash as password and salt in the database instead of plain text.
  5. When you want to verify, get password from user and salt to the hash function.
  6. The result should be matched with the hash that kept in database.
  7. 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;}}}

Install JWT for authentication

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;}}}}

Generate and Validate JWT Reference

GET Request

// 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);}

Post Request

// 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);}

PUT Request

// 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();}

Delete Request

[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();}

Login and Get Token

[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()});}}

Sign up

[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);}

Cors Setting

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();});}

Authorize header in every API

// 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);}

Reference

Problem Solving

  • 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.

About

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.

Topics

Resources

Stars

4 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Latest commit

History

35 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

Todo List Web API

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.

Technologies

imageimageimageimage

Set up Database

Select MariaDB as a SQL database server. MariaDB is an open-source relational database management system(RDBMS) like MySQL.

  1. Install MariaDB
  2. Install Xampp to run database server on localhost for Web API development
  3. Open Xampp, run Apache and mySQL. If the port error is occurred, you can change the port in Config section.
  4. Create database (todolist) and two tables: user and activity

Set up ASP.NET Web API

  1. Install .NET 5.0 SDK from official website.
  2. run dotnet --version to check whether dotnet is already installed or not.
  3. run these command to create web api project
dotnet new webapi -o myproject
cd myproject
  1. 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
  1. After scaffolding, the models folder is created depends on your database table list.

Auto-increment on primary key (id) in Entity Framework

From: EntityFramework Tutorial Then, go to phpMyAdmin and check the id column to auto-increment.

Hash and Salt

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:

  1. Random salt in Byte[] type
  2. Add password in plain text and random salt to the hash function
  3. The result of hash function is hash in Base64
  4. Store both hash as password and salt in the database instead of plain text.
  5. When you want to verify, get password from user and salt to the hash function.
  6. The result should be matched with the hash that kept in database.
  7. 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;}}}

Install JWT for authentication

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;}}}}

Generate and Validate JWT Reference

GET Request

// 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);}

Post Request

// 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);}

PUT Request

// 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();}

Delete Request

[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();}

Login and Get Token

[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()});}}

Sign up

[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);}

Cors Setting

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();});}

Authorize header in every API

// 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);}

Reference

Problem Solving

  • 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.

About

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.

Topics

Resources

Stars

4 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Latest commit

History

35 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

Todo List Web API

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.

Technologies

imageimageimageimage

Set up Database

Select MariaDB as a SQL database server. MariaDB is an open-source relational database management system(RDBMS) like MySQL.

  1. Install MariaDB
  2. Install Xampp to run database server on localhost for Web API development
  3. Open Xampp, run Apache and mySQL. If the port error is occurred, you can change the port in Config section.
  4. Create database (todolist) and two tables: user and activity

Set up ASP.NET Web API

  1. Install .NET 5.0 SDK from official website.
  2. run dotnet --version to check whether dotnet is already installed or not.
  3. run these command to create web api project
dotnet new webapi -o myproject
cd myproject
  1. 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
  1. After scaffolding, the models folder is created depends on your database table list.

Auto-increment on primary key (id) in Entity Framework

From: EntityFramework Tutorial Then, go to phpMyAdmin and check the id column to auto-increment.

Hash and Salt

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:

  1. Random salt in Byte[] type
  2. Add password in plain text and random salt to the hash function
  3. The result of hash function is hash in Base64
  4. Store both hash as password and salt in the database instead of plain text.
  5. When you want to verify, get password from user and salt to the hash function.
  6. The result should be matched with the hash that kept in database.
  7. 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;}}}

Install JWT for authentication

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;}}}}

Generate and Validate JWT Reference

GET Request

// 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);}

Post Request

// 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);}

PUT Request

// 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();}

Delete Request

[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();}

Login and Get Token

[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()});}}

Sign up

[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);}

Cors Setting

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();});}

Authorize header in every API

// 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);}

Reference

Problem Solving

  • 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.

About

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.

Topics

Resources

Stars

4 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Latest commit

History

35 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

Todo List Web API

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.

Technologies

imageimageimageimage

Set up Database

Select MariaDB as a SQL database server. MariaDB is an open-source relational database management system(RDBMS) like MySQL.

  1. Install MariaDB
  2. Install Xampp to run database server on localhost for Web API development
  3. Open Xampp, run Apache and mySQL. If the port error is occurred, you can change the port in Config section.
  4. Create database (todolist) and two tables: user and activity

Set up ASP.NET Web API

  1. Install .NET 5.0 SDK from official website.
  2. run dotnet --version to check whether dotnet is already installed or not.
  3. run these command to create web api project
dotnet new webapi -o myproject
cd myproject
  1. 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
  1. After scaffolding, the models folder is created depends on your database table list.

Auto-increment on primary key (id) in Entity Framework

From: EntityFramework Tutorial Then, go to phpMyAdmin and check the id column to auto-increment.

Hash and Salt

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:

  1. Random salt in Byte[] type
  2. Add password in plain text and random salt to the hash function
  3. The result of hash function is hash in Base64
  4. Store both hash as password and salt in the database instead of plain text.
  5. When you want to verify, get password from user and salt to the hash function.
  6. The result should be matched with the hash that kept in database.
  7. 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;}}}

Install JWT for authentication

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;}}}}

Generate and Validate JWT Reference

GET Request

// 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);}

Post Request

// 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);}

PUT Request

// 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();}

Delete Request

[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();}

Login and Get Token

[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()});}}

Sign up

[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);}

Cors Setting

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();});}

Authorize header in every API

// 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);}

Reference

Problem Solving

  • 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.

About

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.

Topics

Resources

Stars

4 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages