From a161d2277a4f9c0d892dabd58a3d3f6f8d52ec6b Mon Sep 17 00:00:00 2001 From: amazing-username Date: Sun, 21 Apr 2019 21:07:13 -0400 Subject: [PATCH] Implemented Auth0 functionality to handler authentication. Not complete, will add to it some more. #15 --- Authorization/Handlers/HasScopeHandler.cs | 32 ++++++ Authorization/HasScopeRequirement.cs | 19 ++++ Controllers/LoginController.cs | 0 Controllers/LogoutController.cs | 0 Controllers/RegisterController.cs | 54 ++++++++++ Controllers/SongDataController.cs | 2 + Controllers/Utilities/PasswordEncryption.cs | 73 ++++++++++++++ Models/Context/BaseStoreContext.cs | 21 ++++ Models/Context/MusicStoreContext.cs | 4 - Models/Context/UserStoreContext.cs | 104 ++++++++++++++++++++ Models/User.cs | 4 + Startup.cs | 35 ++++++- appsettings.Development.json | 4 + appsettings.json | 4 + 14 files changed, 351 insertions(+), 5 deletions(-) create mode 100644 Authorization/Handlers/HasScopeHandler.cs create mode 100644 Authorization/HasScopeRequirement.cs create mode 100644 Controllers/LoginController.cs create mode 100644 Controllers/LogoutController.cs create mode 100644 Controllers/RegisterController.cs create mode 100644 Controllers/Utilities/PasswordEncryption.cs create mode 100644 Models/Context/BaseStoreContext.cs create mode 100644 Models/Context/UserStoreContext.cs diff --git a/Authorization/Handlers/HasScopeHandler.cs b/Authorization/Handlers/HasScopeHandler.cs new file mode 100644 index 0000000..ca3be48 --- /dev/null +++ b/Authorization/Handlers/HasScopeHandler.cs @@ -0,0 +1,32 @@ +using System; +using System.Linq; +using System.Threading.Tasks; + +using Microsoft.AspNetCore.Authorization; + +using Icarus.Authorization; + +namespace Icarus.Authorization.Handlers +{ + public class HasScopeHandler : AuthorizationHandler + { + protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, + HasScopeRequirement requirement) + { + if (!context.User.HasClaim(c => c.Type == "scope" && c.Issuer == requirement.Issuer)) + { + return Task.CompletedTask; + } + + var scopes = context.User.FindFirst(c => c.Type == "scope" && + c.Issuer == requirement.Issuer).Value.Split(' '); + + if (scopes.Any(s => s == requirement.Scope)) + { + context.Succeed(requirement); + } + + return Task.CompletedTask; + } + } +} diff --git a/Authorization/HasScopeRequirement.cs b/Authorization/HasScopeRequirement.cs new file mode 100644 index 0000000..819b7f7 --- /dev/null +++ b/Authorization/HasScopeRequirement.cs @@ -0,0 +1,19 @@ +using System; + +using Microsoft.AspNetCore.Authorization; + +namespace Icarus.Authorization +{ + public class HasScopeRequirement : IAuthorizationRequirement + { + public string Issuer { get; } + public string Scope { get; } + + + public HasScopeRequirement(string scope, string issuer) + { + Scope = scope ?? throw new ArgumentNullException(nameof(scope)); + Issuer = issuer ?? throw new ArgumentNullException(nameof(issuer)); + } + } +} diff --git a/Controllers/LoginController.cs b/Controllers/LoginController.cs new file mode 100644 index 0000000..e69de29 diff --git a/Controllers/LogoutController.cs b/Controllers/LogoutController.cs new file mode 100644 index 0000000..e69de29 diff --git a/Controllers/RegisterController.cs b/Controllers/RegisterController.cs new file mode 100644 index 0000000..617e5b4 --- /dev/null +++ b/Controllers/RegisterController.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using System.Configuration; +using System.Linq; +using System.Threading.Tasks; + +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Configuration; + +using Icarus.Controllers.Managers; +using Icarus.Controllers.Utilities; +using Icarus.Models; +using Icarus.Models.Context; + +namespace Icarus.Controllers +{ + [Route("api/register")] + [ApiController] + public class RegisterController : ControllerBase + { + #region Fields + private IConfiguration _config; + #endregion + + + #region Properties + #endregion + + + #region Constructor + public RegisterController(IConfiguration config) + { + _config = config; + } + #endregion + + + [HttpPost] + public void Post([FromBody] User user) + { + Console.WriteLine($"Username: {user.Username}"); + Console.WriteLine($"Password: {user.Password}"); + + PasswordEncryption pe = new PasswordEncryption(); + user = pe.HashPassword(user); + Console.WriteLine($"Hashed Password: {user.Password}"); + + UserStoreContext context = HttpContext.RequestServices + .GetService(typeof(UserStoreContext)) + as UserStoreContext; + context.SaveUser(user); + } + } +} diff --git a/Controllers/SongDataController.cs b/Controllers/SongDataController.cs index 86f5455..2184a96 100644 --- a/Controllers/SongDataController.cs +++ b/Controllers/SongDataController.cs @@ -41,6 +41,8 @@ namespace Icarus.Controllers [HttpGet("{id}")] + //[Route("private-scoped")] + //[Authorize("download:songs")] public async Task Get(int id) { MusicStoreContext context = HttpContext.RequestServices diff --git a/Controllers/Utilities/PasswordEncryption.cs b/Controllers/Utilities/PasswordEncryption.cs new file mode 100644 index 0000000..2c36190 --- /dev/null +++ b/Controllers/Utilities/PasswordEncryption.cs @@ -0,0 +1,73 @@ +using System; +using System.Security.Cryptography; + +using Microsoft.AspNetCore.Cryptography.KeyDerivation; + +using Icarus.Models; + +namespace Icarus.Controllers.Utilities +{ + public class PasswordEncryption + { + #region Fields + #endregion + + + #region Properties + #endregion + + + #region Constructor + #endregion + + + #region Methods + public User HashPassword(User user) + { + try + { + var userSalt = GenerateSalt(); + var userHash = GenerateHash(user.Password, userSalt); + + user.Password = userHash; + user.Salt = userSalt; + + return user; + } + catch (Exception ex) + { + var exMsg = ex.Message; + Console.WriteLine($"An error occurred {exMsg}"); + } + + return null; + } + + string GenerateHash(string password, byte[] salt) + { + + string hashed = Convert.ToBase64String(KeyDerivation.Pbkdf2( + password: password, + salt: salt, + prf: KeyDerivationPrf.HMACSHA1, + iterationCount: 10000, + numBytesRequested: 256/8)); + + return hashed; + } + + byte[] GenerateSalt() + { + byte[] salt = new byte[128/8]; + + using (var rng = RandomNumberGenerator.Create()) + { + rng.GetBytes(salt); + } + + + return salt; + } + #endregion + } +} diff --git a/Models/Context/BaseStoreContext.cs b/Models/Context/BaseStoreContext.cs new file mode 100644 index 0000000..7e19957 --- /dev/null +++ b/Models/Context/BaseStoreContext.cs @@ -0,0 +1,21 @@ +using System; + +using MySql.Data.MySqlClient; + +namespace Icarus.Models.Context +{ + public class BaseStoreContext + { + #region Fields + protected string _connectionString; + #endregion + + + #region Methods + protected MySqlConnection GetConnection() + { + return new MySqlConnection(_connectionString); + } + #endregion + } +} diff --git a/Models/Context/MusicStoreContext.cs b/Models/Context/MusicStoreContext.cs index 706995e..06a174d 100644 --- a/Models/Context/MusicStoreContext.cs +++ b/Models/Context/MusicStoreContext.cs @@ -14,10 +14,6 @@ namespace Icarus.Models.Context this.ConnectionString = connectionString; } - public void InsertSongDetails() - { - } - public void SaveSong(Song song) { diff --git a/Models/Context/UserStoreContext.cs b/Models/Context/UserStoreContext.cs new file mode 100644 index 0000000..88ea29d --- /dev/null +++ b/Models/Context/UserStoreContext.cs @@ -0,0 +1,104 @@ +using System; +using System.Collections.Generic; + +using MySql.Data.MySqlClient; + +using Icarus.Models; + +namespace Icarus.Models.Context +{ + public class UserStoreContext: BaseStoreContext + { + #region Fields + #endregion + + + #region Properties + #endregion + + + #region Constructor + public UserStoreContext(string connectionString) + { + _connectionString = connectionString; + } + #endregion + + + #region Methods + public void SaveUser(User user) + { + try + { + using (MySqlConnection conn = GetConnection()) + { + conn.Open(); + string query = "INSERT INTO Users(Username, Password, Salt, " + + "Email, PhoneNumber, Firstname, Lastname, " + + "DateCreated, LastLogin) VALUES(@Username, " + + "@Password, @Salt, @Email, @PhoneNumber, " + + "@Firstname, @Lastname, @DateCreated, @LastLogin)"; + using (MySqlCommand cmd = new MySqlCommand(query, conn)) + { + cmd.Parameters.AddWithValue("@Username", user.Username); + cmd.Parameters.AddWithValue("@Password", user.Password); + cmd.Parameters.AddWithValue("@Salt", user.Salt); + cmd.Parameters.AddWithValue("@Email", user.Email); + cmd.Parameters.AddWithValue("@PhoneNumber", user.PhoneNumber); + cmd.Parameters.AddWithValue("@Firstname", user.Firstname); + cmd.Parameters.AddWithValue("@Lastname", user.Lastname); + cmd.Parameters.AddWithValue("@DateCreated", DateTime.Now); + cmd.Parameters.AddWithValue("@LastLogin", DateTime.Now); + + + cmd.ExecuteNonQuery(); + } + } + } + catch (Exception ex) + { + var exMsg = ex.Message; + Console.WriteLine($"An error occurred:\n{exMsg}"); + } + } + + public User RetrieveUser(User user) + { + try + { + using (MySqlConnection conn = GetConnection()) + { + conn.Open(); + var query = "SELECT * FROM Users WHERE Username=@Username"; + + using (MySqlCommand cmd = new MySqlCommand(query, conn)) + { + cmd.Parameters.AddWithValue("@Username", user.Username); + + using (var reader = cmd.ExecuteReader()) + { + while (reader.Read()) + { + user.Id = Convert.ToInt32(reader["Id"]); + user.Email = reader["Email"].ToString(); + user.PhoneNumber = reader["PhoneNumber"].ToString(); + user.Firstname = reader["Firstname"].ToString(); + user.Lastname = reader["Lastname"].ToString(); + user.DateCreated = DateTime.ParseExact(reader["DateCreated"].ToString(), "yyyy-MM-dd HH:mm:ss", System.Globalization.CultureInfo.InvariantCulture); + user.LastLogin = DateTime.ParseExact(reader["LastLogin"].ToString(), "yyyy-MM-dd HH:mm:ss", System.Globalization.CultureInfo.InvariantCulture); + } + } + } + } + } + catch (Exception ex) + { + var exMsg = ex.Message; + Console.WriteLine($"An error occurred:\n{exMsg}"); + } + + return null; + } + #endregion + } +} diff --git a/Models/User.cs b/Models/User.cs index 3710b9f..df3e81d 100644 --- a/Models/User.cs +++ b/Models/User.cs @@ -10,6 +10,10 @@ namespace Icarus.Models public int Id { get; set; } [JsonProperty("username")] public string Username { get; set; } + [JsonProperty("password")] + public string Password { get; set; } + [JsonIgnore] + public byte[] Salt { get; set; } [JsonProperty("email")] public string Email { get; set; } [JsonProperty("phone_number")] diff --git a/Startup.cs b/Startup.cs index 66c92a2..a9ace4c 100644 --- a/Startup.cs +++ b/Startup.cs @@ -2,7 +2,10 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using System.IdentityModel.Tokens.Jwt; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.HttpsPolicy; @@ -16,7 +19,8 @@ using MySql.Data; using MySql.Data.EntityFrameworkCore.Extensions; using MySql.Data.MySqlClient; -using Icarus.Models; +using Icarus.Authorization; +using Icarus.Authorization.Handlers; using Icarus.Models.Context; namespace Icarus @@ -35,13 +39,40 @@ namespace Icarus { services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2); services.AddSingleton(Configuration); + + string domain = $"https://{Configuration["Auth0:Domain"]}/"; + + services.AddAuthentication(options => + { + options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; + options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; + }).AddJwtBearer(options => + { + options.Authority = domain; + options.Audience = Configuration["Auth0:ApiIdentifier"]; + }); + + services.AddAuthorization(options => + { + options.AddPolicy("download:songs", policy => + policy.Requirements.Add(new HasScopeRequirement + ("download:songs", domain))); + }); + + + services.AddSingleton(); + var connString = Configuration.GetConnectionString("DefaultConnection"); services.Add(new ServiceDescriptor(typeof(MusicStoreContext), new MusicStoreContext(Configuration .GetConnectionString("DefaultConnection")))); + services.Add(new ServiceDescriptor(typeof(UserStoreContext), + new UserStoreContext(Configuration + .GetConnectionString("DefaultConnection")))); services.AddDbContext(options => options.UseMySQL(connString)); + services.AddDbContext(options => options.UseMySQL(connString)); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. @@ -57,6 +88,8 @@ namespace Icarus app.UseHsts(); } + app.UseAuthentication(); + app.UseHttpsRedirection(); app.UseMvc(); } diff --git a/appsettings.Development.json b/appsettings.Development.json index f5baa39..52ad37f 100644 --- a/appsettings.Development.json +++ b/appsettings.Development.json @@ -6,6 +6,10 @@ "Microsoft": "Information" } }, + "Auth0": { + "Domain": "[domain].auth0.com", + "ApiIdentifier": "https://[identifier]/api" + }, "ConnectionStrings": { "DefaultConnection":"Server=;Database=;Uid=;Pwd=;" }, diff --git a/appsettings.json b/appsettings.json index 63e4404..e195c62 100644 --- a/appsettings.json +++ b/appsettings.json @@ -5,6 +5,10 @@ } }, "AllowedHosts": "*", + "Auth0": { + "Domain": "[domain].auth0.com", + "ApiIdentifier": "https://[identifier]/api" + }, "ConnectionStrings": { "DefaultConnection":"Server=;Database=;Uid=;Pwd=;" },