diff --git a/BrightGlimmer.Auth/BrightGlimmer.Auth.csproj b/BrightGlimmer.Auth/BrightGlimmer.Auth.csproj index 64e8e0b..d85fa85 100644 --- a/BrightGlimmer.Auth/BrightGlimmer.Auth.csproj +++ b/BrightGlimmer.Auth/BrightGlimmer.Auth.csproj @@ -18,6 +18,7 @@ + diff --git a/BrightGlimmer.Auth/Controllers/UserController.cs b/BrightGlimmer.Auth/Controllers/UserController.cs index ff4aef9..3ece5cf 100644 --- a/BrightGlimmer.Auth/Controllers/UserController.cs +++ b/BrightGlimmer.Auth/Controllers/UserController.cs @@ -5,35 +5,40 @@ using System.Linq; using System.Security.Claims; using System.Text; using System.Threading.Tasks; +using BrightGlimmer.Auth.Request; +using BrightGlimmer.Domain.Auth; +using BrightGlimmer.Service.Interfaces; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.IdentityModel.Tokens; namespace BrightGlimmer.Auth.Controllers { + [Authorize] [Route("api/[controller]")] [ApiController] public class UserController : ControllerBase { + private readonly IUserService userService; + + public UserController(IUserService userService) + { + this.userService = userService; + } + [AllowAnonymous] [HttpGet] public ActionResult Login(string username, string password) { - /* TODO: Move token creation to service */ - var tokenHandler = new JwtSecurityTokenHandler(); - var key = Encoding.UTF8.GetBytes(Startup.Configuration.GetSection("Keys")["JwtPrivateKey"]); - var tokenDescriptor = new SecurityTokenDescriptor - { - Subject = new ClaimsIdentity(new Claim[] - { - new Claim(ClaimTypes.NameIdentifier, username) - }), - Expires = DateTime.UtcNow.AddDays(3), - SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature) - }; - var tokenSecurity = tokenHandler.CreateToken(tokenDescriptor); - var token = tokenHandler.WriteToken(tokenSecurity); + var token = userService.Login(username, password); + return new JsonResult(token); + } + [AllowAnonymous] + [HttpPost] + public async Task Register(RegisterUserRequest request) + { + var token = await userService.RegisterAsync(request.User, request.Password); return new JsonResult(token); } } diff --git a/BrightGlimmer.Auth/Request/RegisterUserRequest.cs b/BrightGlimmer.Auth/Request/RegisterUserRequest.cs new file mode 100644 index 0000000..5569566 --- /dev/null +++ b/BrightGlimmer.Auth/Request/RegisterUserRequest.cs @@ -0,0 +1,14 @@ +using BrightGlimmer.Domain.Auth; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace BrightGlimmer.Auth.Request +{ + public class RegisterUserRequest + { + public User User { get; set; } + public string Password { get; set; } + } +} diff --git a/BrightGlimmer.Auth/Startup.cs b/BrightGlimmer.Auth/Startup.cs index fea6ab3..f8e7907 100644 --- a/BrightGlimmer.Auth/Startup.cs +++ b/BrightGlimmer.Auth/Startup.cs @@ -4,6 +4,8 @@ using System.Linq; using System.Text; using System.Threading.Tasks; using BrightGlimmer.Data; +using BrightGlimmer.Service.Interfaces; +using BrightGlimmer.Service.Services; using JsonNet.PrivateSettersContractResolvers; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Builder; @@ -26,7 +28,7 @@ namespace BrightGlimmer.Auth Configuration = configuration; } - public static IConfiguration Configuration { get; private set; } + public IConfiguration Configuration { get; private set; } // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) @@ -63,6 +65,9 @@ namespace BrightGlimmer.Auth services.AddDbContext(options => options.UseLazyLoadingProxies() .UseSqlite(Configuration.GetConnectionString("DefaultConnection"))); services.AddTransient(); + + /* Inject Services */ + services.AddTransient(); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. diff --git a/BrightGlimmer.Data/BrightGlimmer.Data.csproj b/BrightGlimmer.Data/BrightGlimmer.Data.csproj index bbf2d69..d3c701c 100644 --- a/BrightGlimmer.Data/BrightGlimmer.Data.csproj +++ b/BrightGlimmer.Data/BrightGlimmer.Data.csproj @@ -13,6 +13,7 @@ + diff --git a/BrightGlimmer.Data/Services/UserService.cs b/BrightGlimmer.Data/Services/UserService.cs deleted file mode 100644 index 7cef23b..0000000 --- a/BrightGlimmer.Data/Services/UserService.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace BrightGlimmer.Data.Services -{ - public class UserService - { - } -} diff --git a/BrightGlimmer.Data/bright_glimmer_auth.db b/BrightGlimmer.Data/bright_glimmer_auth.db index 26f4594..0e700af 100644 Binary files a/BrightGlimmer.Data/bright_glimmer_auth.db and b/BrightGlimmer.Data/bright_glimmer_auth.db differ diff --git a/BrightGlimmer.Domain/Auth/User.cs b/BrightGlimmer.Domain/Auth/User.cs index cd400a6..574ffba 100644 --- a/BrightGlimmer.Domain/Auth/User.cs +++ b/BrightGlimmer.Domain/Auth/User.cs @@ -11,7 +11,10 @@ namespace BrightGlimmer.Domain.Auth public string FirstName { get; private set; } public string MiddleName { get; private set; } public string LastName { get; private set; } - public string PasswordHash { get; private set; } + + /* TODO: Missing DOB */ + + public string PasswordHash { get; set; } public bool EmailConfirmed { get; private set; } public bool AccountLocked { get; private set; } public int RetryAttempts { get; private set; } diff --git a/BrightGlimmer.Service/BrightGlimmer.Service.csproj b/BrightGlimmer.Service/BrightGlimmer.Service.csproj index d66e379..f2dff36 100644 --- a/BrightGlimmer.Service/BrightGlimmer.Service.csproj +++ b/BrightGlimmer.Service/BrightGlimmer.Service.csproj @@ -4,10 +4,6 @@ netstandard2.0 - - - - diff --git a/BrightGlimmer.Service/Interfaces/IUserService.cs b/BrightGlimmer.Service/Interfaces/IUserService.cs new file mode 100644 index 0000000..675f93f --- /dev/null +++ b/BrightGlimmer.Service/Interfaces/IUserService.cs @@ -0,0 +1,14 @@ +using BrightGlimmer.Domain.Auth; +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; + +namespace BrightGlimmer.Service.Interfaces +{ + public interface IUserService + { + string Login(string username, string password); + Task RegisterAsync(User user, string password); + } +} diff --git a/BrightGlimmer.Service/Services/UserService.cs b/BrightGlimmer.Service/Services/UserService.cs new file mode 100644 index 0000000..4aa0db1 --- /dev/null +++ b/BrightGlimmer.Service/Services/UserService.cs @@ -0,0 +1,55 @@ +using BrightGlimmer.Data; +using BrightGlimmer.Domain.Auth; +using BrightGlimmer.Service.Interfaces; +using BrightGlimmer.Tools; +using Microsoft.Extensions.Configuration; +using System.Linq; +using System.Threading.Tasks; + +namespace BrightGlimmer.Service.Services +{ + public class UserService : IUserService + { + private readonly IConfiguration configuration; + private readonly AuthContext context; + + public UserService(IConfiguration configuration, AuthContext context) + { + this.configuration = configuration; + this.context = context; + } + + public string Login(string username, string password) + { + var user = context.Users.SingleOrDefault(x => x.Username == username); + var hasher = new PasswordHasher(); + + if (user == null || hasher.Verify(password, user.PasswordHash)) + { + return CreateAuthToken(username, user.Email); + } + + return null; + } + + public async Task RegisterAsync(User user, string password) + { + /* TODO: Perform validation on user */ + + var hasher = new PasswordHasher(); + var hash = hasher.GetHash(password); + user.PasswordHash = hash; + + context.Users.Add(user); + await context.SaveChangesAsync(); + + return CreateAuthToken(user.Username, user.Email); + } + + private string CreateAuthToken(string username, string email) + { + var tokenCreator = new JwtTokenCreator(configuration.GetSection("Keys")["JwtPrivateKey"]); + return tokenCreator.CreateToken(username, email); + } + } +} diff --git a/BrightGlimmer.Tools/BrightGlimmer.Tools.csproj b/BrightGlimmer.Tools/BrightGlimmer.Tools.csproj new file mode 100644 index 0000000..d6d489e --- /dev/null +++ b/BrightGlimmer.Tools/BrightGlimmer.Tools.csproj @@ -0,0 +1,16 @@ + + + + netstandard2.0 + + + + + ..\..\..\..\..\..\Program Files\dotnet\sdk\NuGetFallbackFolder\microsoft.identitymodel.tokens\5.3.0\lib\netstandard2.0\Microsoft.IdentityModel.Tokens.dll + + + ..\..\..\..\..\..\Program Files\dotnet\sdk\NuGetFallbackFolder\system.identitymodel.tokens.jwt\5.3.0\lib\netstandard2.0\System.IdentityModel.Tokens.Jwt.dll + + + + diff --git a/BrightGlimmer.Tools/JwtTokenCreator.cs b/BrightGlimmer.Tools/JwtTokenCreator.cs new file mode 100644 index 0000000..9e4c848 --- /dev/null +++ b/BrightGlimmer.Tools/JwtTokenCreator.cs @@ -0,0 +1,41 @@ +using Microsoft.IdentityModel.Tokens; +using System; +using System.Collections.Generic; +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Text; + +namespace BrightGlimmer.Tools +{ + /* TODO: WIP */ + public class JwtTokenCreator + { + private readonly string privateKey; + + public JwtTokenCreator(string privateKey) + { + this.privateKey = privateKey; + } + + public string CreateToken(string username, string email) + { + var tokenHandler = new JwtSecurityTokenHandler(); + var key = Encoding.UTF8.GetBytes(privateKey); + + var tokenDescriptor = new SecurityTokenDescriptor + { + Subject = new ClaimsIdentity(new Claim[] + { + new Claim(ClaimTypes.NameIdentifier, username), + new Claim(ClaimTypes.Email, email) + }), + Expires = DateTime.UtcNow.AddDays(1), + SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature) + }; + var tokenSecurity = tokenHandler.CreateToken(tokenDescriptor); + var token = tokenHandler.WriteToken(tokenSecurity); + + return token; + } + } +} diff --git a/BrightGlimmer.Tools/PasswordHasher.cs b/BrightGlimmer.Tools/PasswordHasher.cs new file mode 100644 index 0000000..21d2805 --- /dev/null +++ b/BrightGlimmer.Tools/PasswordHasher.cs @@ -0,0 +1,53 @@ +using System; +using System.Security.Cryptography; + +namespace BrightGlimmer.Tools +{ + public class PasswordHasher + { + private readonly int SALT_LENGTH = 16; + private readonly int PASS_LENGTH = 20; + private readonly int ITERATIONS = 10000; + + public bool Verify(string password, string hash) + { + var storedHash = Convert.FromBase64String(hash); + + var salt = new byte[SALT_LENGTH]; + Array.Copy(storedHash, 0, salt, 0, SALT_LENGTH); + + var givenHash = GetHash(password, salt); + + return hash == givenHash; + } + + public string GetHash(string password, byte[] salt = null) + { + if (salt == null) + { + salt = CreateSalt(); + } + var hash = CreateHash(password, salt); + + var completeHash = new byte[SALT_LENGTH + PASS_LENGTH]; + Array.Copy(salt, 0, completeHash, 0, SALT_LENGTH); + Array.Copy(hash, 0, completeHash, SALT_LENGTH, PASS_LENGTH); + + return Convert.ToBase64String(completeHash); + } + + private byte[] CreateHash(string password, byte[] salt) + { + var pbkdf2 = new Rfc2898DeriveBytes(password, salt, ITERATIONS); + return pbkdf2.GetBytes(PASS_LENGTH); + } + + private byte[] CreateSalt() + { + var salt = new byte[SALT_LENGTH]; + new RNGCryptoServiceProvider().GetBytes(salt); + + return salt; + } + } +} diff --git a/BrightGlimmer.sln b/BrightGlimmer.sln index 3e9e9ef..383bcb7 100644 --- a/BrightGlimmer.sln +++ b/BrightGlimmer.sln @@ -13,7 +13,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BrightGlimmer.Service", "Br EndProject Project("{9092AA53-FB77-4645-B42D-1CCCA6BD08BD}") = "BrightGlimmer.Client", "BrightGlimmer.Client\BrightGlimmer.Client.njsproj", "{6971AB72-FBC2-4F3F-A59C-319E377A9A51}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BrightGlimmer.Auth", "BrightGlimmer.Auth\BrightGlimmer.Auth.csproj", "{E3265892-2D6C-4D51-A7A4-DF845C17CF49}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BrightGlimmer.Auth", "BrightGlimmer.Auth\BrightGlimmer.Auth.csproj", "{E3265892-2D6C-4D51-A7A4-DF845C17CF49}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BrightGlimmer.Tools", "BrightGlimmer.Tools\BrightGlimmer.Tools.csproj", "{FE3B361C-BC0D-4D48-A861-9B10908F5E37}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -45,6 +47,10 @@ Global {E3265892-2D6C-4D51-A7A4-DF845C17CF49}.Debug|Any CPU.Build.0 = Debug|Any CPU {E3265892-2D6C-4D51-A7A4-DF845C17CF49}.Release|Any CPU.ActiveCfg = Release|Any CPU {E3265892-2D6C-4D51-A7A4-DF845C17CF49}.Release|Any CPU.Build.0 = Release|Any CPU + {FE3B361C-BC0D-4D48-A861-9B10908F5E37}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FE3B361C-BC0D-4D48-A861-9B10908F5E37}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FE3B361C-BC0D-4D48-A861-9B10908F5E37}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FE3B361C-BC0D-4D48-A861-9B10908F5E37}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE