diff --git a/LaDOSE.Src/LaDOSE.Api/Context/LaDOSEDbContext.cs b/LaDOSE.Src/LaDOSE.Api/Context/LaDOSEDbContext.cs index 2540afe..2d534c3 100644 --- a/LaDOSE.Src/LaDOSE.Api/Context/LaDOSEDbContext.cs +++ b/LaDOSE.Src/LaDOSE.Api/Context/LaDOSEDbContext.cs @@ -7,6 +7,7 @@ namespace LaDOSE.Api.Context public class LaDOSEDbContext : DbContext { public DbSet Game { get; set; } + public DbSet ApplicationUser { get; set; } public LaDOSEDbContext(DbContextOptions options) : base(options) { diff --git a/LaDOSE.Src/LaDOSE.Api/Controllers/ValuesController.cs b/LaDOSE.Src/LaDOSE.Api/Controllers/GameController.cs similarity index 70% rename from LaDOSE.Src/LaDOSE.Api/Controllers/ValuesController.cs rename to LaDOSE.Src/LaDOSE.Api/Controllers/GameController.cs index 667bd81..2fcc707 100644 --- a/LaDOSE.Src/LaDOSE.Api/Controllers/ValuesController.cs +++ b/LaDOSE.Src/LaDOSE.Api/Controllers/GameController.cs @@ -4,18 +4,20 @@ using System.Linq; using System.Threading.Tasks; using LaDOSE.Api.Context; using LaDOSE.Entity; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; namespace LaDOSE.Api.Controllers { + [Authorize] [Route("api/[controller]")] [ApiController] - public class ConfigController : ControllerBase + public class GameController : ControllerBase { private readonly LaDOSEDbContext _db; - public ConfigController(LaDOSEDbContext db) + public GameController(LaDOSEDbContext db) { _db = db; } @@ -27,12 +29,14 @@ namespace LaDOSE.Api.Controllers return _db.Game.ToList(); } - + // GET api/Config/5 [HttpGet("{id}")] - public ActionResult Get(int id) + public Game Get(int id) { - return "value"; + return _db.Game.FirstOrDefault(e=>e.Id==id); } + + } } diff --git a/LaDOSE.Src/LaDOSE.Api/Controllers/UsersController.cs b/LaDOSE.Src/LaDOSE.Api/Controllers/UsersController.cs new file mode 100644 index 0000000..865c573 --- /dev/null +++ b/LaDOSE.Src/LaDOSE.Api/Controllers/UsersController.cs @@ -0,0 +1,106 @@ +using System; +using System.Collections.Generic; +using System.IdentityModel.Tokens.Jwt; +using System.Linq; +using System.Security.Claims; +using System.Text; +using System.Threading.Tasks; +using LaDOSE.Api.Services; +using LaDOSE.Entity; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Tokens; + +namespace LaDOSE.Api.Controllers +{ + [Authorize] + [ApiController] + [Route("[controller]")] + public class UsersController : ControllerBase + { + private IUserService _userService; + + + public UsersController( + IUserService userService + ) + { + _userService = userService; + + } + + [AllowAnonymous] + [HttpGet("test")] + public String Test() + { + return "DEAD"; + } + + + + [HttpGet("test2")] + public String Test2() + { + return "DEAD"; + } + + + [AllowAnonymous] + [HttpPost("authenticate")] + public IActionResult Authenticate([FromBody]ApplicationUser userDto) + { + var user = _userService.Authenticate(userDto.Username, userDto.Password); + + if (user == null) + return BadRequest(new { message = "Username or password is incorrect" }); + + var tokenHandler = new JwtSecurityTokenHandler(); + var key = Encoding.ASCII.GetBytes("this is my custom Secret key for authnetication"); + var tokenDescriptor = new SecurityTokenDescriptor + { + Subject = new ClaimsIdentity(new Claim[] + { + new Claim(ClaimTypes.Name, user.Id.ToString()) + }), + Expires = DateTime.UtcNow.AddDays(7), + SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature) + }; + var token = tokenHandler.CreateToken(tokenDescriptor); + var tokenString = tokenHandler.WriteToken(token); + + // return basic user info (without password) and token to store client side + return Ok(new + { + Id = user.Id, + Username = user.Username, + FirstName = user.FirstName, + LastName = user.LastName, + Token = tokenString + }); + } + + [AllowAnonymous] + [HttpPost("register")] + public IActionResult Register([FromBody]ApplicationUser userDto) + { + // map dto to entity + + + try + { + // save + _userService.Create(userDto, userDto.Password); + return Ok(); + } + catch (Exception ex) + { + // return error message if there was an exception + return BadRequest(new { message = ex.Message }); + } + } + + + } + +} diff --git a/LaDOSE.Src/LaDOSE.Api/Services/UserService.cs b/LaDOSE.Src/LaDOSE.Api/Services/UserService.cs new file mode 100644 index 0000000..14b4e03 --- /dev/null +++ b/LaDOSE.Src/LaDOSE.Api/Services/UserService.cs @@ -0,0 +1,154 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using LaDOSE.Api.Context; +using LaDOSE.Entity; + +namespace LaDOSE.Api.Services +{ + public interface IUserService + { + ApplicationUser Authenticate(string username, string password); + IEnumerable GetAll(); + ApplicationUser GetById(int id); + ApplicationUser Create(ApplicationUser user, string password); + void Update(ApplicationUser user, string password = null); + void Delete(int id); + } + + public class UserService : IUserService + { + private LaDOSEDbContext _context; + + public UserService(LaDOSEDbContext context) + { + _context = context; + } + + public ApplicationUser Authenticate(string username, string password) + { + if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(password)) + return null; + + var user = _context.ApplicationUser.SingleOrDefault(x => x.Username == username); + + // check if username exists + if (user == null) + return null; + + // check if password is correct + if (!VerifyPasswordHash(password, user.PasswordHash, user.PasswordSalt)) + return null; + + // authentication successful + return user; + } + + public IEnumerable GetAll() + { + return _context.ApplicationUser; + } + + public ApplicationUser GetById(int id) + { + return _context.ApplicationUser.Find(id); + } + + public ApplicationUser Create(ApplicationUser user, string password) + { + // validation + if (string.IsNullOrWhiteSpace(password)) + throw new Exception("Password is required"); + + if (_context.ApplicationUser.Any(x => x.Username == user.Username)) + throw new Exception("Username \"" + user.Username + "\" is already taken"); + + byte[] passwordHash, passwordSalt; + CreatePasswordHash(password, out passwordHash, out passwordSalt); + + user.PasswordHash = passwordHash; + user.PasswordSalt = passwordSalt; + + _context.ApplicationUser.Add(user); + _context.SaveChanges(); + + return user; + } + + public void Update(ApplicationUser userParam, string password = null) + { + var user = _context.ApplicationUser.Find(userParam.Id); + + if (user == null) + throw new Exception("User not found"); + + if (userParam.Username != user.Username) + { + // username has changed so check if the new username is already taken + if (_context.ApplicationUser.Any(x => x.Username == userParam.Username)) + throw new Exception("Username " + userParam.Username + " is already taken"); + } + + // update user properties + user.FirstName = userParam.FirstName; + user.LastName = userParam.LastName; + user.Username = userParam.Username; + + // update password if it was entered + if (!string.IsNullOrWhiteSpace(password)) + { + byte[] passwordHash, passwordSalt; + CreatePasswordHash(password, out passwordHash, out passwordSalt); + + user.PasswordHash = passwordHash; + user.PasswordSalt = passwordSalt; + } + + _context.ApplicationUser.Update(user); + _context.SaveChanges(); + } + + public void Delete(int id) + { + var user = _context.ApplicationUser.Find(id); + if (user != null) + { + _context.ApplicationUser.Remove(user); + _context.SaveChanges(); + } + } + + // private helper methods + + private static void CreatePasswordHash(string password, out byte[] passwordHash, out byte[] passwordSalt) + { + if (password == null) throw new ArgumentNullException("password"); + if (string.IsNullOrWhiteSpace(password)) throw new ArgumentException("Value cannot be empty or whitespace only string.", "password"); + + using (var hmac = new System.Security.Cryptography.HMACSHA512()) + { + passwordSalt = hmac.Key; + passwordHash = hmac.ComputeHash(System.Text.Encoding.UTF8.GetBytes(password)); + } + } + + private static bool VerifyPasswordHash(string password, byte[] storedHash, byte[] storedSalt) + { + if (password == null) throw new ArgumentNullException("password"); + if (string.IsNullOrWhiteSpace(password)) throw new ArgumentException("Value cannot be empty or whitespace only string.", "password"); + if (storedHash.Length != 64) throw new ArgumentException("Invalid length of password hash (64 bytes expected).", "passwordHash"); + if (storedSalt.Length != 128) throw new ArgumentException("Invalid length of password salt (128 bytes expected).", "passwordHash"); + + using (var hmac = new System.Security.Cryptography.HMACSHA512(storedSalt)) + { + var computedHash = hmac.ComputeHash(System.Text.Encoding.UTF8.GetBytes(password)); + for (int i = 0; i < computedHash.Length; i++) + { + if (computedHash[i] != storedHash[i]) return false; + } + } + + return true; + } + } +} \ No newline at end of file diff --git a/LaDOSE.Src/LaDOSE.Api/Startup.cs b/LaDOSE.Src/LaDOSE.Api/Startup.cs index 6c289a5..f169ca2 100644 --- a/LaDOSE.Src/LaDOSE.Api/Startup.cs +++ b/LaDOSE.Src/LaDOSE.Api/Startup.cs @@ -1,17 +1,23 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Text; using System.Threading.Tasks; using LaDOSE.Api.Context; +using LaDOSE.Api.Services; +using LaDOSE.Entity; +using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.HttpsPolicy; +using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Tokens; using Pomelo.EntityFrameworkCore.MySql; using Pomelo.EntityFrameworkCore.MySql.Infrastructure; @@ -29,6 +35,7 @@ namespace LaDOSE.Api // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { + services.AddCors(); services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1); services.AddDbContextPool( // replace "YourDbContext" with the class name of your DbContext options => options.UseMySql("Server=localhost;Database=ladose;User=root;Password=;", // replace with your Connection String @@ -37,13 +44,53 @@ namespace LaDOSE.Api mysqlOptions.ServerVersion(new Version(10, 1, 16), ServerType.MariaDb); // replace with your Server Version and Type } )); + + var key = Encoding.ASCII.GetBytes("this is my custom Secret key for authnetication"); + services.AddAuthentication(x => + { + x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; + x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; + }) + .AddJwtBearer(x => + { + x.Events = new JwtBearerEvents + { + OnTokenValidated = context => + { + var userService = context.HttpContext.RequestServices.GetRequiredService(); + var userId = int.Parse(context.Principal.Identity.Name); + var user = userService.GetById(userId); + if (user == null) + { + // return unauthorized if user no longer exists + context.Fail("Unauthorized"); + } + return Task.CompletedTask; + } + }; + x.RequireHttpsMetadata = false; + x.SaveToken = true; + x.TokenValidationParameters = new TokenValidationParameters + { + ValidateIssuerSigningKey = true, + IssuerSigningKey = new SymmetricSecurityKey(key), + ValidateIssuer = false, + ValidateAudience = false + }; + }); + + // configure DI for application services + services.AddScoped(); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - public void Configure(IApplicationBuilder app, IHostingEnvironment env) + public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) { + loggerFactory.AddConsole(Configuration.GetSection("Logging")); + loggerFactory.AddDebug(); + if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); @@ -52,8 +99,14 @@ namespace LaDOSE.Api { app.UseHsts(); } + app.UseCors(x => x + .AllowAnyOrigin() + .AllowAnyMethod() + .AllowAnyHeader() + .AllowCredentials()); app.UseHttpsRedirection(); + app.UseAuthentication(); app.UseMvc(); } } diff --git a/LaDOSE.Src/LaDOSE.Entity/ApplicationUser.cs b/LaDOSE.Src/LaDOSE.Entity/ApplicationUser.cs new file mode 100644 index 0000000..2091b39 --- /dev/null +++ b/LaDOSE.Src/LaDOSE.Entity/ApplicationUser.cs @@ -0,0 +1,19 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + + +namespace LaDOSE.Entity +{ + public class ApplicationUser + { + public int Id { get; set; } + public string FirstName { get; set; } + public string LastName { get; set; } + public string Username { get; set; } + [NotMapped] + public string Password { get; set; } + public byte[] PasswordHash { get; set; } + public byte[] PasswordSalt { get; set; } + } + +} \ No newline at end of file