From 467a9d7e0f20275ab40695319606e0b57e885378 Mon Sep 17 00:00:00 2001 From: Kun Deng Date: Sat, 13 Aug 2022 18:23:03 -0400 Subject: [PATCH] Saving changes Using asymmetric validation --- .gitignore | 3 + Controllers/Managers/TokenManager.cs | 180 ++++++++++++++++++++++ Controllers/v1/LoginController.cs | 2 +- Database/Contexts/UserContext.cs | 5 + Exceptions/InvalidCredentialsException.cs | 20 +++ Models/Shared/User.cs | 21 +++ Models/User.cs | 18 +++ Repositories/UserRepository.cs | 37 +++++ Services/AuthenticationService.cs | 31 ++++ Services/TokenService.cs | 63 ++++++++ Services/UserService.sc.cs | 38 +++++ Startup.cs | 111 +++++++++++++ 12 files changed, 528 insertions(+), 1 deletion(-) create mode 100644 Exceptions/InvalidCredentialsException.cs create mode 100644 Models/Shared/User.cs create mode 100644 Repositories/UserRepository.cs create mode 100644 Services/AuthenticationService.cs create mode 100644 Services/TokenService.cs create mode 100644 Services/UserService.sc.cs diff --git a/.gitignore b/.gitignore index 3b08958..e035641 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,9 @@ ################################################################################ /.vs/Icarus +/bin/* /bin/Debug/netcoreapp2.2 /Migrations /obj +/obj/* +/Icarus.txt diff --git a/Controllers/Managers/TokenManager.cs b/Controllers/Managers/TokenManager.cs index 4dbd878..8944d24 100644 --- a/Controllers/Managers/TokenManager.cs +++ b/Controllers/Managers/TokenManager.cs @@ -1,9 +1,19 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Security.Claims; +using System.Security.Cryptography; +using System.Threading.Tasks; +using JWT; +using JWT.Serializers; using Microsoft.Extensions.Configuration; using Newtonsoft.Json; +using Org.BouncyCastle.Crypto; +using Org.BouncyCastle.Crypto.Parameters; +using Org.BouncyCastle.OpenSsl; +using Org.BouncyCastle.Security; + using RestSharp; using Icarus.Models; @@ -15,6 +25,8 @@ namespace Icarus.Controllers.Managers #region Fields private string _clientId; private string _clientSecret; + private string _privateKeyPath; + private string _publicKeyPath; private string _audience; private string _grantType; private string _url; @@ -68,6 +80,172 @@ namespace Icarus.Controllers.Managers Message = "Successfully retrieved token" }; } + + + public LoginResult LogIn(User user) + { + var tokenResult = new Token(); + tokenResult.TokenType = "Jwt"; + + var privateKey = ReadKeyContent(_privateKeyPath).Result; + var publicKey = ReadKeyContent(_publicKeyPath).Result; + + var payload = Payload(); + + var token = CreateToken(payload, privateKey); + tokenResult.AccessToken = token; + + var expClaim = payload.FirstOrDefault(cl => + { + return cl.Type.Equals("exp"); + }); + + tokenResult.Expiration = System.Convert.ToInt32(expClaim.Value); + + return new LoginResult + { + UserID = user.UserID, Username = user.Username, Token = tokenResult.AccessToken, + TokenType = tokenResult.TokenType, Expiration = tokenResult.Expiration, + Message = "Successfully retrieved token" + }; + } + + + private string AllScopes() + { + var allScopes = new List() + { + "download:songs", + "read:song_details", + "upload:songs", + "delete:songs", + "read:albums", + "read:artists", + "update:songs", + "stream:songs", + "read:genre", + "read:year", + "download:cover_art" + }; + + var scopes = string.Empty; + + for (var i = 0; i < allScopes.Count; i++) + { + if (i == allScopes.Count - 1) + { + scopes += allScopes[i]; + } + else + { + scopes += allScopes[i] + " "; + } + } + + return scopes; + } + + private List Payload() + { + const int expLimit = 24; + var currentDate = DateTime.Now; + var expiredDate = currentDate.AddHours(expLimit); + var issued = Math.Floor((currentDate - DateTime.UnixEpoch).TotalSeconds); + var expires = Math.Floor((expiredDate - DateTime.UnixEpoch).TotalSeconds); + var issuer = "https://soaricarus.auth0.com"; + issuer = "http://localhost:5002"; + var audience = "https://icarus/api"; + audience = "http://localhost:5002"; + + var claim = new List() + { + new System.Security.Claims.Claim("scope", AllScopes(), "string"), + new System.Security.Claims.Claim("exp", $"{expires}", "integer"), + new System.Security.Claims.Claim("aud", $"{audience}", "string"), + new System.Security.Claims.Claim("iss", $"{issuer}", "string"), + new System.Security.Claims.Claim("iat", $"{issued}", "integer") + }; + + return claim; + } + + private string CreateToken(List claims, string privateKey) + { + var token = string.Empty; + + if (string.IsNullOrEmpty(privateKey)) + { + privateKey = ReadKeyContent(_privateKeyPath).Result; + } + + RSAParameters rsaParams; + using (var tr = new System.IO.StringReader(privateKey)) + { + var pemReader = new PemReader(tr); + var keyPair = pemReader.ReadObject() as AsymmetricCipherKeyPair; + if (keyPair == null) + { + throw new Exception("Could not read RSA private key"); + } + var privateRsaParams = keyPair.Private as RsaPrivateCrtKeyParameters; + rsaParams = DotNetUtilities.ToRSAParameters(privateRsaParams); + } + + using (RSACryptoServiceProvider rsa = new RSACryptoServiceProvider()) + { + var rsaParamsPublic = GetRSAPublic(ReadKeyContent(_publicKeyPath).Result); + var rsaPublic = new RSACryptoServiceProvider(); + + rsa.ImportParameters(rsaParams); + rsaPublic.ImportParameters(rsaParamsPublic); + + Dictionary payload = new Dictionary(); + + foreach (var claim in claims) + { + var type = claim.Type; + var val = Int32.TryParse(claim.Value, out _); + + if (val) + { + payload.Add(type, Convert.ToInt32(claim.Value)); + } + else + { + payload.Add(type, claim.Value); + } + + } + + var algorithm = new JWT.Algorithms.RS256Algorithm(rsaPublic, rsa); + IJsonSerializer serializer = new JsonNetSerializer(); + IBase64UrlEncoder urlEncoder = new JwtBase64UrlEncoder(); + IJwtEncoder encoder = new JwtEncoder(algorithm, serializer, urlEncoder); + + token = encoder.Encode(payload, privateKey); + } + + return token; + } + + private RSAParameters GetRSAPublic(string publicKey) + { + using (var tr = new System.IO.StringReader(publicKey)) + { + var pemReader = new PemReader(tr); + var publicKeyParams = pemReader.ReadObject() as RsaKeyParameters; + if (publicKeyParams == null) + { + throw new Exception("Could not read RSA public key"); + } + return DotNetUtilities.ToRSAParameters(publicKeyParams); + } + } + + private async Task ReadKeyContent(string filepath) + { + return await System.IO.File.ReadAllTextAsync(filepath); + } private TokenRequest RetrieveTokenRequest() { @@ -89,6 +267,8 @@ namespace Icarus.Controllers.Managers _audience = _config["Auth0:ApiIdentifier"]; _grantType = "client_credentials"; _url = $"https://{_config["Auth0:Domain"]}"; + _privateKeyPath = _config["RSAKeys:PrivateKeyPath"]; + _publicKeyPath = _config["RSAKeys:PublicKeyPath"]; PrintCredentials(); } diff --git a/Controllers/v1/LoginController.cs b/Controllers/v1/LoginController.cs index 9510a98..78ee567 100644 --- a/Controllers/v1/LoginController.cs +++ b/Controllers/v1/LoginController.cs @@ -73,7 +73,7 @@ namespace Icarus.Controllers.V1 TokenManager tk = new TokenManager(_config); - loginRes = tk.RetrieveLoginResult(user); + loginRes = tk.LogIn(user); return Ok(loginRes); } diff --git a/Database/Contexts/UserContext.cs b/Database/Contexts/UserContext.cs index 030b538..2083f3c 100644 --- a/Database/Contexts/UserContext.cs +++ b/Database/Contexts/UserContext.cs @@ -8,6 +8,7 @@ using System.Linq; // using MySql.Data.Entity; // using MySql.Data.MySqlClient; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; using Icarus.Models; @@ -18,11 +19,15 @@ namespace Icarus.Database.Contexts public DbSet Users { get; set; } + #region Constructors public UserContext(DbContextOptions options) : base(options) { } + [ActivatorUtilitiesConstructor] public UserContext(string connString) : base(new DbContextOptionsBuilder() .UseMySQL(connString).Options) { } + #endregion + protected override void OnModelCreating(ModelBuilder modelBuilder) { diff --git a/Exceptions/InvalidCredentialsException.cs b/Exceptions/InvalidCredentialsException.cs new file mode 100644 index 0000000..2145caa --- /dev/null +++ b/Exceptions/InvalidCredentialsException.cs @@ -0,0 +1,20 @@ +using System; + +namespace Icarus.Exceptions +{ + public class InvalidCredentialsException : Exception + { + public InvalidCredentialsException() + { + } + + public InvalidCredentialsException(string message) : base(message) + { + } + + public InvalidCredentialsException(string message, + Exception exception) : base(message, exception) + { + } + } +} \ No newline at end of file diff --git a/Models/Shared/User.cs b/Models/Shared/User.cs new file mode 100644 index 0000000..5a467d1 --- /dev/null +++ b/Models/Shared/User.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; + +namespace Icarus.Models.Shared +{ + public class User + { + public string Username { get; set; } + public string Password { get; set; } + public IEnumerable Roles { get; set; } + + public IEnumerable Claims() + { + var claims = new List { new Claim(ClaimTypes.Name, Username) }; + claims.AddRange(Roles.Select(role => new Claim(ClaimTypes.Role, role))); + + return claims; + } + } +} \ No newline at end of file diff --git a/Models/User.cs b/Models/User.cs index 6fd74a9..4dece75 100644 --- a/Models/User.cs +++ b/Models/User.cs @@ -1,6 +1,7 @@ using System; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; +using System.Linq; using Newtonsoft.Json; @@ -9,6 +10,7 @@ namespace Icarus.Models [Table("User")] public class User { + #region Properties [JsonProperty("user_id")] [Column("UserID")] [Key] @@ -35,5 +37,21 @@ namespace Icarus.Models public string Status { get; set; } [JsonProperty("last_login")] public DateTime? LastLogin { get; set; } + + [JsonIgnore] + [NotMapped] + public System.Collections.Generic.IEnumerable Roles { get; set; } + #endregion + + + #region Methods + public System.Collections.Generic.IEnumerable Claims() + { + var claims = new System.Collections.Generic.List { new System.Security.Claims.Claim(System.Security.Claims.ClaimTypes.Name, Username) }; + claims.AddRange(Roles.Select(role => new System.Security.Claims.Claim(System.Security.Claims.ClaimTypes.Role, role))); + + return claims; + } + #endregion } } diff --git a/Repositories/UserRepository.cs b/Repositories/UserRepository.cs new file mode 100644 index 0000000..073fbb2 --- /dev/null +++ b/Repositories/UserRepository.cs @@ -0,0 +1,37 @@ +// using JwtAuthentication.Shared.Models; +// using Icarus.Models.Shared; +using System.Collections.Generic; +using System.Linq; + +namespace Icarus.Repositories +{ + // TODO: Replace this with the UserContext. + public class UserRepository + { + private readonly IEnumerable users; + + public UserRepository() + { + users = new List + { + new Icarus.Models.User + { + Username = "john.doe", + Password = "john.password", + Roles = new []{"User"} + }, + new Icarus.Models.User + { + Username = "jane.doe", + Password = "jane.password", + Roles = new []{"User", "Admin"} + } + }; + } + + public Icarus.Models.User GetUser(string username) + { + return users.SingleOrDefault(u => u.Username.Equals(username)); + } + } +} \ No newline at end of file diff --git a/Services/AuthenticationService.cs b/Services/AuthenticationService.cs new file mode 100644 index 0000000..61c1354 --- /dev/null +++ b/Services/AuthenticationService.cs @@ -0,0 +1,31 @@ +// using JwtAuthentication.Shared.Models; +// using JwtAuthentication.Shared.Services; +using Icarus; +using Icarus.Repositories; +using Icarus.Models.Shared; +using Icarus.Services; + +namespace Icarus.Services +{ + public class AuthenticationService + { + private Microsoft.Extensions.Configuration.IConfiguration _configuration; + private readonly UserService userService; + private readonly TokenService tokenService; + + public AuthenticationService(UserService userService, TokenService tokenService, Microsoft.Extensions.Configuration.IConfiguration configuration) + { + this.userService = userService; + this.tokenService = tokenService; + this._configuration = configuration; + } + + public string Authenticate(UserCredentials userCredentials) + { + userService.ValidateCredentials(userCredentials); + string securityToken = tokenService.GetToken(userCredentials.Username); + + return securityToken; + } + } +} \ No newline at end of file diff --git a/Services/TokenService.cs b/Services/TokenService.cs new file mode 100644 index 0000000..9bd7267 --- /dev/null +++ b/Services/TokenService.cs @@ -0,0 +1,63 @@ +// using JwtAuthentication.AsymmetricEncryption.Certificates; +using Icarus; +using Icarus.Repositories; +using Icarus.Models.Shared; +// using JwtAuthentication.Shared; +// using JwtAuthentication.Shared.Models; +using Microsoft.IdentityModel.Tokens; +using System; +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; + +namespace Icarus.Services +{ + public class TokenService + { + private Microsoft.Extensions.Configuration.IConfiguration _configuration; + private readonly UserRepository userRepository; + private readonly SigningAudienceCertificate signingAudienceCertificate; + private string _publickKeyPath = string.Empty; + + /** + public TokenService(UserRepository userRepository) + { + this.userRepository = userRepository; + signingAudienceCertificate = new SigningAudienceCertificate(); + } + */ + + public TokenService(UserRepository userRepository, Microsoft.Extensions.Configuration.IConfiguration configuration) + { + this._configuration = configuration; + this.userRepository = userRepository; + signingAudienceCertificate = new SigningAudienceCertificate(); + _publickKeyPath = configuration["RSAKeys:PublicKeyPath"]; + } + + public string GetToken(string username) + { + var user = userRepository.GetUser(username); + SecurityTokenDescriptor tokenDescriptor = GetTokenDescriptor(user); + + var tokenHandler = new JwtSecurityTokenHandler(); + SecurityToken securityToken = tokenHandler.CreateToken(tokenDescriptor); + string token = tokenHandler.WriteToken(securityToken); + + return token; + } + + private SecurityTokenDescriptor GetTokenDescriptor(Icarus.Models.User user) + { + const int expiringDays = 7; + + var tokenDescriptor = new SecurityTokenDescriptor + { + Subject = new ClaimsIdentity(user.Claims()), + Expires = DateTime.UtcNow.AddDays(expiringDays), + SigningCredentials = signingAudienceCertificate.GetAudienceSigningKey(_publickKeyPath) + }; + + return tokenDescriptor; + } + } +} \ No newline at end of file diff --git a/Services/UserService.sc.cs b/Services/UserService.sc.cs new file mode 100644 index 0000000..a5e9689 --- /dev/null +++ b/Services/UserService.sc.cs @@ -0,0 +1,38 @@ +// using JwtAuthentication.Shared.Exceptions; +// using JwtAuthentication.Shared.Models; +using Icarus.Exceptions; +using Icarus.Models.Shared; +using Icarus.Services; +using Icarus.Repositories; + +namespace Icarus.Services +{ + public class UserService + { + private Microsoft.Extensions.Configuration.IConfiguration _configuration; + private readonly UserRepository userRepository; + + public UserService(UserRepository userRepository, Microsoft.Extensions.Configuration.IConfiguration configuration) + { + this._configuration = configuration; + this.userRepository = userRepository; + } + + public void ValidateCredentials(UserCredentials userCredentials) + { + Icarus.Models.User user = userRepository.GetUser(userCredentials.Username); + bool isValid = user != null && AreValidCredentials(userCredentials, user); + + if (!isValid) + { + throw new InvalidCredentialsException(); + } + } + + private static bool AreValidCredentials(UserCredentials userCredentials, Icarus.Models.User user) + { + return user.Username == userCredentials.Username && + user.Password == userCredentials.Password; + } + } +} \ No newline at end of file diff --git a/Startup.cs b/Startup.cs index 44e5481..9bc5aa6 100644 --- a/Startup.cs +++ b/Startup.cs @@ -4,6 +4,8 @@ using System.Linq; using System.Threading.Tasks; using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Authorization; @@ -21,22 +23,113 @@ using Newtonsoft.Json; using NLog; using NLog.Web; using NLog.Web.AspNetCore; +using Org.BouncyCastle.Crypto; +using Org.BouncyCastle.Crypto.Parameters; +using Org.BouncyCastle.OpenSsl; +using Org.BouncyCastle.Security; +using Icarus.Services; +using Icarus.Repositories; using Icarus.Authorization; using Icarus.Authorization.Handlers; using Icarus.Database.Contexts; namespace Icarus { + #region Classes + public class SigningIssuerCertificate + { + public SigningIssuerCertificate() + { + } + + // public or private key? + public RsaSecurityKey GetIssuerSigningKey(string publicKeyPath) + { + var file = publicKeyPath; + var publicKey = System.IO.File.ReadAllText(file); + var rsa = new RSACryptoServiceProvider(); + + using (var reader = System.IO.File.OpenText(file)) + { + var pem = new PemReader(reader); + var o = (RsaKeyParameters)pem.ReadObject(); + var parameters = new RSAParameters(); + parameters.Modulus = o.Modulus.ToByteArray(); + parameters.Exponent = o.Exponent.ToByteArray(); + rsa.ImportParameters(parameters); + } + + return new RsaSecurityKey(rsa); + } + } + + public class SigningAudienceCertificate + { + // public or private key? + public SigningCredentials GetAudienceSigningKey(string keyPath) + { + // string privateXmlKey = System.IO.File.ReadAllText("./private_key.xml"); + // rsa.FromXmlString(privateXmlKey); + + var file = keyPath; + var publicKey = System.IO.File.ReadAllText(file); + var rsa = new RSACryptoServiceProvider(); + + using (var reader = System.IO.File.OpenText(file)) + { + var pem = new PemReader(reader); + var o = (RsaKeyParameters)pem.ReadObject(); + var parameters = new RSAParameters(); + parameters.Modulus = o.Modulus.ToByteArray(); + parameters.Exponent = o.Exponent.ToByteArray(); + rsa.ImportParameters(parameters); + } + + return new SigningCredentials( + key: new RsaSecurityKey(rsa), + algorithm: SecurityAlgorithms.RsaSha256); + } + } + #endregion + + public static class MyClass + { + public static IServiceCollection AddAsymmetricAuthentication(this IServiceCollection services, IConfiguration configuration) + // public static IServiceCollection AddAsymmetricAuthentication(this IServiceCollection services, IConfiguration configuration) + { + var issuerSigningCertificate = new SigningIssuerCertificate(); + RsaSecurityKey issuerSigningKey = issuerSigningCertificate.GetIssuerSigningKey(configuration["RSAKeys:PublicKeyPath"]); + + services.AddAuthentication(authOptions => + { + }) + .AddJwtBearer(options => + { + options.TokenValidationParameters = new TokenValidationParameters + { + IssuerSigningKey = issuerSigningKey, + }; + }); + + return services; + } + } public class Startup { + #region Constructors public Startup(IConfiguration configuration) { Configuration = configuration; } + #endregion + #region Properties public IConfiguration Configuration { get; } + #endregion + + #region Methods // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { @@ -45,6 +138,7 @@ namespace Icarus var auth_id = Configuration["Auth0:Domain"]; var domain = $"https://{auth_id}/"; var audience = Configuration["Auth0:ApiIdentifier"]; + /** services .AddAuthentication(JwtBearerDefaults.AuthenticationScheme) @@ -103,6 +197,7 @@ namespace Icarus services.AddSingleton(); + */ var connString = Configuration.GetConnectionString("DefaultConnection"); @@ -114,6 +209,21 @@ namespace Icarus services.AddDbContext(options => options.UseMySQL(connString)); services.AddDbContext(options => options.UseMySQL(connString)); + services.AddAsymmetricAuthentication(Configuration); + + services.AddTransient(au => new AuthenticationService(new UserService(new UserRepository(), Configuration), new TokenService(new UserRepository(), Configuration), Configuration)); + services.AddTransient(us => new UserService(new UserRepository(), Configuration)); + services.AddTransient(tk => new TokenService(new UserRepository(), Configuration)); + services.AddTransient(); + services.AddTransient(uc => new UserContext(connString)); + /** + services.AddTransient((providers) => + { + return new Func((numParams) => + new UserContext(providers.GetRequiredService, numParams)); + }); + */ + services.AddControllers() .AddNewtonsoftJson(); } @@ -131,5 +241,6 @@ namespace Icarus endpoints.MapControllers(); }); } + #endregion } }