From 9797897b768f11bae8ff410ead7b06346811d7c3 Mon Sep 17 00:00:00 2001 From: Kun Deng Date: Thu, 30 Jun 2022 21:44:28 -0400 Subject: [PATCH 01/21] Added solution file --- Icarus.sln | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 Icarus.sln diff --git a/Icarus.sln b/Icarus.sln new file mode 100644 index 0000000..018545a --- /dev/null +++ b/Icarus.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.2.32616.157 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Icarus", "Icarus.csproj", "{66389DAC-0D3C-4271-BBAE-AF27FCFD28B7}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {66389DAC-0D3C-4271-BBAE-AF27FCFD28B7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {66389DAC-0D3C-4271-BBAE-AF27FCFD28B7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {66389DAC-0D3C-4271-BBAE-AF27FCFD28B7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {66389DAC-0D3C-4271-BBAE-AF27FCFD28B7}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {0BE61157-7A0C-41CF-B097-71178A3F6755} + EndGlobalSection +EndGlobal From 2cd8c83e28698fb010d2edfbac6369f95b95ed26 Mon Sep 17 00:00:00 2001 From: kdeng00 Date: Sun, 7 Aug 2022 17:03:47 -0400 Subject: [PATCH 02/21] Updated config --- appsettings.Development.json | 4 ++++ appsettings.json | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/appsettings.Development.json b/appsettings.Development.json index 827e74a..cd727fb 100644 --- a/appsettings.Development.json +++ b/appsettings.Development.json @@ -12,6 +12,10 @@ "ClientId": "", "ClientSecret": "" }, + "RSAKeys": { + "PrivateKeyPath": "", + "PublicKeyPath": "" + }, "ConnectionStrings": { "DefaultConnection": "Server=;Database=;Uid=;Pwd=;" }, diff --git a/appsettings.json b/appsettings.json index bd1b862..68eb482 100644 --- a/appsettings.json +++ b/appsettings.json @@ -12,6 +12,10 @@ "ClientId": "", "ClientSecret": "" }, + "RSAKeys": { + "PrivateKeyPath": "", + "PublicKeyPath": "" + }, "ConnectionStrings": { "DefaultConnection": "Server=;Database=;Uid=;Pwd=;" }, From f0551a48015b8e59bda10ea63c7e7076b41a894b Mon Sep 17 00:00:00 2001 From: kdeng00 Date: Sun, 7 Aug 2022 17:05:49 -0400 Subject: [PATCH 03/21] Updates Added packages and updated readme --- Icarus.csproj | 2 ++ README.md | 41 ++++++++++++++++++++++++++++++++++++++--- 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/Icarus.csproj b/Icarus.csproj index bfbfd41..7c8b877 100644 --- a/Icarus.csproj +++ b/Icarus.csproj @@ -11,6 +11,7 @@ + @@ -21,6 +22,7 @@ + diff --git a/README.md b/README.md index e205100..a49349e 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,22 @@ One can interface with Icarus the music server either by: * C# [.NET](https://dotnet.microsoft.com/) 6 * [MySql](https://www.nuget.org/packages/MySql.Data/) +* OpenSSL +* BCrypt.Net-Next +* DotNetZip +* ID3 +* JWT +* Microsoft.AspNetCore.Authentication.JwtBearer +* Microsoft.AspNetCore.Mvc.NewtonsoftJson +* Microsoft.EntityFrameworkCore +* Microsoft.EntityFrameworkCore.Tools +* MySql.EntityFrameworkCore * [Newtonsoft.Json](https://www.newtonsoft.com/json) +* NLog.Web.AspNetCpre +* Portable.BouncyCastle +* RestSharp +* SevenZip +* System.IdentityModel.Tokens.Jwt * [TagLib#](https://github.com/mono/taglib-sharp) ![image](https://user-images.githubusercontent.com/14333136/56252069-28532d00-6084-11e9-896d-1a3c378014ef.png) @@ -24,9 +39,10 @@ One can interface with Icarus the music server either by: ## Getting started There are several things that need to be completed to properly setup and secure the API. 1. Auth0 API configuration -2. API filesystem paths -3. Database connection string -4. Migrations +2. Creating RSA keys +3. API filesystem paths +4. Database connection string +5. Migrations ### Auth0 API configuration @@ -80,6 +96,25 @@ Enter the information in the corresponding appsettings json file }, ``` +### Creating RSA keys +1. Create private key +``` +openssl genrsa -out private.pem 2048 +``` +2. Create public key +``` +openssl rsa -in private -pubout -out public.pem +``` + +Configure the key paths in the config files + +```Json +"RSAKeys": { + "PrivateKeyPath": "", + "PublicKeyPath": "" +} +``` + ### API filesystem paths For the purposes of properly uploading, downloading, updating, deleting, and streaming songs the API filesystem paths must be configured. What is meant by this is that the `RootMusicPath` directory where all music will be stored must exist as well as the `ArchivePath` and `TemporaryMusicPath` paths. An example on a Linux system: From 467a9d7e0f20275ab40695319606e0b57e885378 Mon Sep 17 00:00:00 2001 From: Kun Deng Date: Sat, 13 Aug 2022 18:23:03 -0400 Subject: [PATCH 04/21] 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 } } From 2cc13ac9bbdd0582c577ccb851f09bc3b44a8c5c Mon Sep 17 00:00:00 2001 From: Kun Deng Date: Thu, 25 Aug 2022 20:56:12 -0400 Subject: [PATCH 05/21] Code changes Did not run code. Had a spur of the moment to try something out. --- Authorization/Handlers/HasScopeHandler.cs | 13 +++- Certs/Signing.cs | 87 +++++++++++++++++++++++ Models/Shared/UserCredentials.cs | 8 +++ Scripts/Migrations/Linux/AddUpdate.sh | 0 Services/TokenService.cs | 33 +++++++-- Services/UserService.sc.cs | 21 +++++- Startup.cs | 25 +++++-- 7 files changed, 171 insertions(+), 16 deletions(-) create mode 100644 Certs/Signing.cs create mode 100644 Models/Shared/UserCredentials.cs mode change 100755 => 100644 Scripts/Migrations/Linux/AddUpdate.sh diff --git a/Authorization/Handlers/HasScopeHandler.cs b/Authorization/Handlers/HasScopeHandler.cs index 8f51065..82e54e6 100644 --- a/Authorization/Handlers/HasScopeHandler.cs +++ b/Authorization/Handlers/HasScopeHandler.cs @@ -12,7 +12,18 @@ namespace Icarus.Authorization.Handlers { protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, HasScopeRequirement requirement) { - if (!context.User.HasClaim(c => c.Type == "scope" && c.Issuer == requirement.Issuer)) + var issuer = requirement.Issuer; + var scope = requirement.Scope; + var claim = string.Empty; + // if (!context.User.HasClaim(c => c.Type == "scope" && c.Issuer == requirement.Issuer)) + if (!context.User.HasClaim(c => + { + var i = 0; + var b = 99; + claim = c.Type; + return c.Type == "scope"; + }) + ) { return Task.CompletedTask; } diff --git a/Certs/Signing.cs b/Certs/Signing.cs new file mode 100644 index 0000000..4f53a26 --- /dev/null +++ b/Certs/Signing.cs @@ -0,0 +1,87 @@ +using System; +using System.Security.Cryptography; + +using Microsoft.IdentityModel.Tokens; +using Org.BouncyCastle.Crypto.Parameters; +using Org.BouncyCastle.OpenSsl; + + +namespace Icarus.Certs +{ + + public class SigningIssuerCertificate : IDisposable + { + private readonly RSACryptoServiceProvider _rsa; + + public SigningIssuerCertificate() + { + _rsa = new RSACryptoServiceProvider(); + } + + // public or private key? + public RsaSecurityKey GetIssuerSigningKey(string publicKeyPath) + { + var file = publicKeyPath; + var publicKey = System.IO.File.ReadAllText(file); + + 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 void Dispose() + { + _rsa?.Dispose(); + } + } + + + public class SigningAudienceCertificate : IDisposable + { + // private readonly RSA _rsa; + private readonly RSACryptoServiceProvider _rsa; + + public SigningAudienceCertificate() + { + _rsa = new RSACryptoServiceProvider(); + } + // 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); + } + + public void Dispose() + { + _rsa?.Dispose(); + } + } +} \ No newline at end of file diff --git a/Models/Shared/UserCredentials.cs b/Models/Shared/UserCredentials.cs new file mode 100644 index 0000000..1b8bf3d --- /dev/null +++ b/Models/Shared/UserCredentials.cs @@ -0,0 +1,8 @@ +namespace Icarus.Models.Shared +{ + public class UserCredentials + { + public string Username { get; set; } + public string Password { get; set; } + } +} \ No newline at end of file diff --git a/Scripts/Migrations/Linux/AddUpdate.sh b/Scripts/Migrations/Linux/AddUpdate.sh old mode 100755 new mode 100644 diff --git a/Services/TokenService.cs b/Services/TokenService.cs index 9bd7267..3c0e598 100644 --- a/Services/TokenService.cs +++ b/Services/TokenService.cs @@ -1,13 +1,19 @@ // using JwtAuthentication.AsymmetricEncryption.Certificates; +using System; +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Linq; + +using Microsoft.IdentityModel.Tokens; +using Microsoft.Extensions.Configuration; + using Icarus; +using Icarus.Certs; +using Icarus.Database.Contexts; 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 { @@ -15,7 +21,8 @@ namespace Icarus.Services { private Microsoft.Extensions.Configuration.IConfiguration _configuration; private readonly UserRepository userRepository; - private readonly SigningAudienceCertificate signingAudienceCertificate; + private readonly UserContext _userContext; + private readonly Icarus.Certs.SigningAudienceCertificate signingAudienceCertificate; private string _publickKeyPath = string.Empty; /** @@ -26,17 +33,29 @@ namespace Icarus.Services } */ + /** public TokenService(UserRepository userRepository, Microsoft.Extensions.Configuration.IConfiguration configuration) { this._configuration = configuration; this.userRepository = userRepository; - signingAudienceCertificate = new SigningAudienceCertificate(); + signingAudienceCertificate = new Icarus.Certs.SigningAudienceCertificate(); + _publickKeyPath = configuration["RSAKeys:PublicKeyPath"]; + } + */ + + public TokenService(Microsoft.Extensions.Configuration.IConfiguration configuration) + { + this._configuration = configuration; + // this.userRepository = userRepository; + this._userContext = new UserContext(configuration.GetConnectionString("DefaultConnection")); + signingAudienceCertificate = new Icarus.Certs.SigningAudienceCertificate(); _publickKeyPath = configuration["RSAKeys:PublicKeyPath"]; } public string GetToken(string username) { - var user = userRepository.GetUser(username); + // var user = userRepository.GetUser(username); + var user = _userContext.Users.FirstOrDefault(usr => usr.Username.Equals(username)); SecurityTokenDescriptor tokenDescriptor = GetTokenDescriptor(user); var tokenHandler = new JwtSecurityTokenHandler(); diff --git a/Services/UserService.sc.cs b/Services/UserService.sc.cs index a5e9689..19684f4 100644 --- a/Services/UserService.sc.cs +++ b/Services/UserService.sc.cs @@ -1,9 +1,14 @@ // using JwtAuthentication.Shared.Exceptions; // using JwtAuthentication.Shared.Models; +using System.Linq; + +using Microsoft.Extensions.Configuration; + using Icarus.Exceptions; using Icarus.Models.Shared; using Icarus.Services; using Icarus.Repositories; +using Icarus.Database.Contexts; namespace Icarus.Services { @@ -11,6 +16,7 @@ namespace Icarus.Services { private Microsoft.Extensions.Configuration.IConfiguration _configuration; private readonly UserRepository userRepository; + private readonly UserContext _userContext; public UserService(UserRepository userRepository, Microsoft.Extensions.Configuration.IConfiguration configuration) { @@ -18,9 +24,17 @@ namespace Icarus.Services this.userRepository = userRepository; } + // public UserService(UserContext userContext, Microsoft.Extensions.Configuration.IConfiguration configuration) + public UserService(Microsoft.Extensions.Configuration.IConfiguration configuration) + { + this._configuration = configuration; + this._userContext = new UserContext(configuration.GetConnectionString("DefaultConnection")); + } + public void ValidateCredentials(UserCredentials userCredentials) { - Icarus.Models.User user = userRepository.GetUser(userCredentials.Username); + // Icarus.Models.User user = userRepository.GetUser(userCredentials.Username); + var user = _userContext.Users.FirstOrDefault(usr => usr.Username.Equals(userCredentials.Username)); bool isValid = user != null && AreValidCredentials(userCredentials, user); if (!isValid) @@ -31,8 +45,9 @@ namespace Icarus.Services private static bool AreValidCredentials(UserCredentials userCredentials, Icarus.Models.User user) { - return user.Username == userCredentials.Username && - user.Password == userCredentials.Password; + // TODO: Has the user provided password and compair it to the hash retrieved from + // the DB + return true; } } } \ No newline at end of file diff --git a/Startup.cs b/Startup.cs index 9bc5aa6..13323a9 100644 --- a/Startup.cs +++ b/Startup.cs @@ -37,6 +37,7 @@ using Icarus.Database.Contexts; namespace Icarus { #region Classes + /** public class SigningIssuerCertificate { public SigningIssuerCertificate() @@ -63,7 +64,9 @@ namespace Icarus return new RsaSecurityKey(rsa); } } + */ + /** public class SigningAudienceCertificate { // public or private key? @@ -91,6 +94,7 @@ namespace Icarus algorithm: SecurityAlgorithms.RsaSha256); } } + */ #endregion public static class MyClass @@ -98,7 +102,7 @@ namespace Icarus public static IServiceCollection AddAsymmetricAuthentication(this IServiceCollection services, IConfiguration configuration) // public static IServiceCollection AddAsymmetricAuthentication(this IServiceCollection services, IConfiguration configuration) { - var issuerSigningCertificate = new SigningIssuerCertificate(); + var issuerSigningCertificate = new Icarus.Certs.SigningIssuerCertificate(); RsaSecurityKey issuerSigningKey = issuerSigningCertificate.GetIssuerSigningKey(configuration["RSAKeys:PublicKeyPath"]); services.AddAuthentication(authOptions => @@ -114,6 +118,14 @@ namespace Icarus return services; } + + private static bool LifetimeValidator(DateTime? notBefore, + DateTime? expires, + SecurityToken securityToken, + TokenValidationParameters validationParameters) + { + return expires != null && expires > DateTime.UtcNow; + } } public class Startup { @@ -138,7 +150,6 @@ namespace Icarus var auth_id = Configuration["Auth0:Domain"]; var domain = $"https://{auth_id}/"; var audience = Configuration["Auth0:ApiIdentifier"]; - /** services .AddAuthentication(JwtBearerDefaults.AuthenticationScheme) @@ -148,6 +159,7 @@ namespace Icarus options.Audience = audience; }); + /** services.AddAuthorization(options => { options.AddPolicy("download:songs", policy => @@ -211,9 +223,12 @@ namespace Icarus 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(au => new AuthenticationService(new UserService(new UserRepository(), Configuration), new TokenService(new UserRepository(), Configuration), Configuration)); + services.AddTransient(au => new AuthenticationService(new UserService(Configuration), new TokenService(Configuration), Configuration)); + // services.AddTransient(us => new UserService(new UserRepository(), Configuration)); + services.AddTransient(us => new UserService(Configuration)); + // services.AddTransient(tk => new TokenService(new UserRepository(), Configuration)); + services.AddTransient(tk => new TokenService(Configuration)); services.AddTransient(); services.AddTransient(uc => new UserContext(connString)); /** From d48716a54ffd8acc9bdc684c3a545046e9e44f4d Mon Sep 17 00:00:00 2001 From: Kun Deng Date: Fri, 26 Aug 2022 18:01:29 -0400 Subject: [PATCH 06/21] Endpoint authorization Tokens are validated for some of the endpoints. Need to add other measures to rule out bogus tokens. --- Controllers/Managers/SongManager.cs | 22 +++++++-- Controllers/Managers/TokenManager.cs | 66 ++++++++++++++++++++++++-- Controllers/v1/SongController.cs | 34 ++++++++++++- Controllers/v1/SongDataController.cs | 31 +++++++++++- Controllers/v1/SongStreamController.cs | 32 ++++++++++++- Models/Token.cs | 46 ++++++++++++++++++ Startup.cs | 2 +- 7 files changed, 221 insertions(+), 12 deletions(-) create mode 100644 Models/Token.cs diff --git a/Controllers/Managers/SongManager.cs b/Controllers/Managers/SongManager.cs index 96e0c20..be7097a 100644 --- a/Controllers/Managers/SongManager.cs +++ b/Controllers/Managers/SongManager.cs @@ -220,8 +220,9 @@ namespace Icarus.Controllers.Managers song.SongDirectory = _tempDirectoryRoot; song.DateCreated = DateTime.Now; + var tempPath = song.SongPath(); - using (var filestream = new FileStream(song.SongPath(), FileMode.Create)) + using (var filestream = new FileStream(tempPath, FileMode.Create)) { _logger.Info("Saving song to temporary directory"); await songFile.CopyToAsync(filestream); @@ -246,11 +247,22 @@ namespace Icarus.Controllers.Managers using (var fileStream = new FileStream(filePath, FileMode.Create)) { - var songBytes = await System.IO.File.ReadAllBytesAsync(song.SongPath()); + var songBytes = await System.IO.File.ReadAllBytesAsync(tempPath); - await System.IO.File.WriteAllBytesAsync(filePath, songBytes); - System.IO.File.Delete(song.SongPath()); - song.SongDirectory = dirMgr.SongDirectory; + try + { + if (!System.IO.File.Exists(filePath)) + { + await System.IO.File.WriteAllBytesAsync(filePath, songBytes); + System.IO.File.Delete(tempPath); + } + } + catch (Exception ex) + { + var msg = ex.Message; + _logger.Error($"An error occurred: {msg}"); + } + // song.SongDirectory = dirMgr.SongDirectory; _logger.Info("Song successfully saved to filesystem"); } diff --git a/Controllers/Managers/TokenManager.cs b/Controllers/Managers/TokenManager.cs index 8944d24..c2ca547 100644 --- a/Controllers/Managers/TokenManager.cs +++ b/Controllers/Managers/TokenManager.cs @@ -20,13 +20,26 @@ using Icarus.Models; namespace Icarus.Controllers.Managers { + #region Classes + /** + { + "scope":"download:songs read:song_details upload:songs delete:songs read:albums read:artists update:songs stream:songs read:genre read:year download:cover_art", + "exp":1661616071, + "aud":"http://localhost:5002", + "iss":"http://localhost:5002", + "iat":1661529671 +} + */ + public class TokenManager : BaseManager { #region Fields private string _clientId; private string _clientSecret; private string _privateKeyPath; + private string _privateKey; private string _publicKeyPath; + private string _publicKey; private string _audience; private string _grantType; private string _url; @@ -70,7 +83,7 @@ namespace Icarus.Controllers.Managers _logger.Info("Deserializing response"); var tokenResult = JsonConvert - .DeserializeObject(response.Content); + .DeserializeObject(response.Content); _logger.Info("Response deserialized"); return new LoginResult @@ -84,7 +97,7 @@ namespace Icarus.Controllers.Managers public LoginResult LogIn(User user) { - var tokenResult = new Token(); + var tokenResult = new TokenTierOne(); tokenResult.TokenType = "Jwt"; var privateKey = ReadKeyContent(_privateKeyPath).Result; @@ -110,6 +123,50 @@ namespace Icarus.Controllers.Managers }; } + public bool IsTokenValid(string scope, string accessToken) + { + var result = false; + var token = DecodeToken(accessToken); + + if (token == null) + { + return result; + } + + result = (!token.TokenExpired() && token.ContainsScope(scope)) ? true : false; + + // What would make a token valid? + // 1. The expiration date must be before the current date + // 2. The desired scope must be part of the scopes within the access token + // 3. Must be able to be decoded + + return result; + } + + public Token? DecodeToken(string accessToken) + { + var rsaParams = GetRSAPublic(_publicKey); + Token tok = null; + + using (var rsa = new RSACryptoServiceProvider()) + { + rsa.ImportParameters(rsaParams); + + IJsonSerializer serializer = new JsonNetSerializer(); + IDateTimeProvider provider = new UtcDateTimeProvider(); + IJwtValidator validator = new JwtValidator(serializer, provider); + IBase64UrlEncoder urlEncoder = new JwtBase64UrlEncoder(); + var algorithm = new JWT.Algorithms.RS256Algorithm(rsa); + IJwtDecoder decoder = new JwtDecoder(serializer, validator, urlEncoder, algorithm); + + var json = decoder.Decode(accessToken); + tok = JsonConvert.DeserializeObject(json); + } + + + return tok; + } + private string AllScopes() { @@ -269,6 +326,8 @@ namespace Icarus.Controllers.Managers _url = $"https://{_config["Auth0:Domain"]}"; _privateKeyPath = _config["RSAKeys:PrivateKeyPath"]; _publicKeyPath = _config["RSAKeys:PublicKeyPath"]; + _privateKey = System.IO.File.ReadAllText(_privateKeyPath); + _publicKey = System.IO.File.ReadAllText(_publicKeyPath); PrintCredentials(); } @@ -299,7 +358,7 @@ namespace Icarus.Controllers.Managers [JsonProperty("grant_type")] public string GrantType { get; set; } } - private class Token + private class TokenTierOne { [JsonProperty("access_token")] public string AccessToken { get; set; } @@ -310,4 +369,5 @@ namespace Icarus.Controllers.Managers } #endregion } + #endregion } diff --git a/Controllers/v1/SongController.cs b/Controllers/v1/SongController.cs index 82a72d6..77d755b 100644 --- a/Controllers/v1/SongController.cs +++ b/Controllers/v1/SongController.cs @@ -43,10 +43,40 @@ namespace Icarus.Controllers.V1 #endregion + #region Methods + private string ParseBearerTokenFromHeader() + { + var token = string.Empty; + const string tokenType = "Bearer"; + + var req = Request; + var auth = req.Headers.Authorization; + var val = auth.ToString(); + + if (val.Contains(tokenType) && val.Split(" ").Count() > 1) + { + var split = val.Split(" "); + token = split[1]; + } + + + return token; + } + #region HTTP Endpoints + + [HttpGet] - [Authorize("read:song_details")] + // [Authorize("read:song_details")] public IActionResult Get() { + var token = ParseBearerTokenFromHeader(); + var tokMgr = new TokenManager(_config); + + if (!tokMgr.IsTokenValid("read:song_details", token)) + { + return StatusCode(401, "Not allowed"); + } + List songs = new List(); Console.WriteLine("Attemtping to retrieve songs"); _logger.LogInformation("Attempting to retrieve songs"); @@ -100,5 +130,7 @@ namespace Icarus.Controllers.V1 return Ok(songRes); } + #endregion + #endregion } } diff --git a/Controllers/v1/SongDataController.cs b/Controllers/v1/SongDataController.cs index 8b66acc..deb37d9 100644 --- a/Controllers/v1/SongDataController.cs +++ b/Controllers/v1/SongDataController.cs @@ -47,6 +47,27 @@ namespace Icarus.Controllers.V1 #endregion + private string ParseBearerTokenFromHeader() + { + var token = string.Empty; + const string tokenType = "Bearer"; + const string otherTokenType = "Jwt"; + + var req = Request; + var auth = req.Headers.Authorization; + var val = auth.ToString(); + + if (val.Contains(tokenType) && val.Split(" ").Count() > 1 || + val.Contains(otherTokenType) && val.Split(" ").Count() > 1) + { + var split = val.Split(" "); + token = split[1]; + } + + + return token; + } + [HttpGet("download/{id}")] [Route("private-scoped")] @@ -112,9 +133,17 @@ namespace Icarus.Controllers.V1 // [HttpPost("upload/with/data")] [Route("private-scoped")] - [Authorize("upload:songs")] + // [Authorize("upload:songs")] public async Task Post ([FromForm] UploadSongWithDataForm up) { + var token = ParseBearerTokenFromHeader(); + var tokMgr = new TokenManager(_config); + + if (!tokMgr.IsTokenValid("upload:songs", token)) + { + return StatusCode(401, "Not allowed"); + } + try { if (up.SongData.Length > 0 && up.CoverArtData.Length > 0 && !string.IsNullOrEmpty(up.SongFile)) diff --git a/Controllers/v1/SongStreamController.cs b/Controllers/v1/SongStreamController.cs index a15607d..01a8c44 100644 --- a/Controllers/v1/SongStreamController.cs +++ b/Controllers/v1/SongStreamController.cs @@ -13,6 +13,7 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using Icarus.Models; +using Icarus.Controllers.Managers; using Icarus.Database.Contexts; namespace Icarus.Controllers.V1 @@ -42,11 +43,40 @@ namespace Icarus.Controllers.V1 #endregion + private string ParseBearerTokenFromHeader() + { + var token = string.Empty; + const string tokenType = "Bearer"; + + var req = Request; + var auth = req.Headers.Authorization; + var val = auth.ToString(); + + if (val.Contains(tokenType) && val.Split(" ").Count() > 1) + { + var split = val.Split(" "); + token = split[1]; + } + + + return token; + } + #region HTTP endpoints [HttpGet("{id}")] - [Authorize("stream:songs")] + // [Authorize("stream:songs")] public async Task Get(int id) { + var token = ParseBearerTokenFromHeader(); + var tokMgr = new TokenManager(_config); + + /** + if (!tokMgr.IsTokenValid("stream:songs", token)) + { + return StatusCode(401, "Not allowed"); + } + */ + var context = new SongContext(_config.GetConnectionString("DefaultConnection")); var song = context.Songs.FirstOrDefault(sng => sng.SongID == id); diff --git a/Models/Token.cs b/Models/Token.cs new file mode 100644 index 0000000..b1d4214 --- /dev/null +++ b/Models/Token.cs @@ -0,0 +1,46 @@ +using System; +using System.Linq; + +using Newtonsoft.Json; + + +namespace Icarus.Models +{ + public class Token + { + #region Properties + [JsonProperty("scope")] + public string Scope { get; set; } + [JsonProperty("exp")] + public int Expiration { get; set; } + [JsonProperty("aud")] + public string Audience { get; set; } + [JsonProperty("iss")] + public string Issuer { get; set; } + [JsonProperty("iat")] + public int Issued { get; set; } + #endregion + + #region Methods + public bool TokenExpired() + { + var result = false; + var currentDate = DateTime.Now; + + var currentDateInSeconds = Math.Floor((currentDate - DateTime.UnixEpoch).TotalSeconds); + + result = (currentDateInSeconds >= Expiration) ? true : false; + + return result; + } + + public bool ContainsScope(string desiredScope) + { + var result = false; + result = Scope.Contains(desiredScope); + + return result; + } + #endregion + } +} \ No newline at end of file diff --git a/Startup.cs b/Startup.cs index 13323a9..05fb68d 100644 --- a/Startup.cs +++ b/Startup.cs @@ -151,6 +151,7 @@ namespace Icarus var domain = $"https://{auth_id}/"; var audience = Configuration["Auth0:ApiIdentifier"]; + /** services .AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => @@ -159,7 +160,6 @@ namespace Icarus options.Audience = audience; }); - /** services.AddAuthorization(options => { options.AddPolicy("download:songs", policy => From fd0f48761553f0e7429210064b2359bb749f88f7 Mon Sep 17 00:00:00 2001 From: Kun Deng Date: Fri, 26 Aug 2022 23:09:48 -0400 Subject: [PATCH 07/21] Runtime fixes Fixed issues preventing endpoints from successfully completing --- Controllers/Managers/SongManager.cs | 107 ++++++++++++++++++++------- Controllers/v1/SongDataController.cs | 27 ++++--- Models/Song.cs | 16 ++++ 3 files changed, 113 insertions(+), 37 deletions(-) diff --git a/Controllers/Managers/SongManager.cs b/Controllers/Managers/SongManager.cs index be7097a..d0a39de 100644 --- a/Controllers/Managers/SongManager.cs +++ b/Controllers/Managers/SongManager.cs @@ -180,23 +180,40 @@ namespace Icarus.Controllers.Managers DirectoryManager dirMgr = new DirectoryManager(_config, song); dirMgr.CreateDirectory(); - var filePath = dirMgr.SongDirectory; - filePath += $"{song.Filename}"; + /** + await Task.Run(() => + { + System.IO.File.Delete(song.SongPath()); + }); + */ + + var tempPath = song.SongPath(); + song.Filename = song.GenerateFilename(1); + var filePath = $"{dirMgr.SongDirectory}{song.Filename}"; + // $"{dirMgr.SongDirectory}"; + // filePath += $"{song.Filename}"; _logger.Info($"Absolute song path: {filePath}"); - using (var fileStream = new FileStream(filePath, FileMode.Create)) + await Task.Run(() => { - var songBytes = await System.IO.File.ReadAllBytesAsync(song.SongPath()); + using (var fileStream = new FileStream(filePath, FileMode.Create)) + { + var songBytes = System.IO.File.ReadAllBytes(tempPath); - System.IO.File.WriteAllBytesAsync(filePath, songBytes); - System.IO.File.Delete(song.SongPath()); + // System.IO.File.WriteAllBytes(filePath, songBytes); + _logger.Info("Saving song to the filesystem"); + fileStream.Write(songBytes, 0, songBytes.Count()); - song.SongDirectory = dirMgr.SongDirectory; + System.IO.File.Delete(tempPath); + _logger.Info("Deleting temp file"); - _logger.Info("Song successfully saved to filesystem"); - } + _logger.Info("Song successfully saved to filesystem"); + } + }); + + song.SongDirectory = dirMgr.SongDirectory; var coverMgr = new CoverArtManager(_config); var coverArt = coverMgr.SaveCoverArt(song); @@ -211,50 +228,83 @@ namespace Icarus.Controllers.Managers } } - public async Task SaveSongToFileSystem(IFormFile songFile, IFormFile coverArtData, Song song) + public void SaveSongToFileSystem(IFormFile songFile, IFormFile coverArtData, Song song) { + song.SongDirectory = _tempDirectoryRoot; + song.DateCreated = DateTime.Now; + if (string.IsNullOrEmpty(song.Filename)) { song.Filename = song.GenerateFilename(1); } - song.SongDirectory = _tempDirectoryRoot; - song.DateCreated = DateTime.Now; + _logger.Info($"Temporary directory: {_tempDirectoryRoot}"); + var tempPath = song.SongPath(); + _logger.Info("Temporary song path: {0}", tempPath); + using (var filestream = new FileStream(tempPath, FileMode.Create)) { _logger.Info("Saving song to temporary directory"); - await songFile.CopyToAsync(filestream); + songFile.CopyTo(filestream); } var coverMgr = new CoverArtManager(_config); - var coverArt = coverMgr.SaveCoverArt(coverArtData, song); - var meta = new MetadataRetriever(); + var coverArt = coverMgr.SaveCoverArt(coverArtData, song); + meta.UpdateCoverArt(song, coverArt); song.Duration = meta.RetrieveSongDuration(song.SongPath()); meta.UpdateMetadata(song, song); - meta.UpdateCoverArt(song, coverArt); + DirectoryManager dirMgr = new DirectoryManager(_config, song); dirMgr.CreateDirectory(); - var filePath = dirMgr.SongDirectory + song.Filename; + song.SongDirectory = dirMgr.SongDirectory; + var filePath = song.SongPath(); _logger.Info($"Absolute song path: {filePath}"); + if (System.IO.File.Exists(filePath)) + { + _logger.Info("Why does the file exist?"); + } + + if (System.IO.File.Exists(tempPath)) + { + // System.IO.File.Move(tempPath, filePath); + // _logger.Info("Moved song from temporary location to final location"); + } using (var fileStream = new FileStream(filePath, FileMode.Create)) { - var songBytes = await System.IO.File.ReadAllBytesAsync(tempPath); + var songBytes = System.IO.File.ReadAllBytes(tempPath); + // fileStream.ReadAsync(songBytes) + + if (System.IO.File.Exists(filePath)) + { + _logger.Info("Why does the file exist?"); + } try { - if (!System.IO.File.Exists(filePath)) + if (System.IO.File.Exists(filePath) && System.IO.File.Exists(tempPath) && fileStream.Length > 0) { - await System.IO.File.WriteAllBytesAsync(filePath, songBytes); System.IO.File.Delete(tempPath); + _logger.Info("Deleted temp song from filesystem: {0}", tempPath); + } + else + { + // System.IO.File.WriteAllBytes(filePath, songBytes); + // + + fileStream.Write(songBytes, 0, songBytes.Count()); + _logger.Info("Saved song to filesystem: {0}", filePath); + + System.IO.File.Delete(tempPath); + _logger.Info("Deleted temp song from filesystem: {0}", tempPath); } } catch (Exception ex) @@ -262,12 +312,10 @@ namespace Icarus.Controllers.Managers var msg = ex.Message; _logger.Error($"An error occurred: {msg}"); } - // song.SongDirectory = dirMgr.SongDirectory; _logger.Info("Song successfully saved to filesystem"); } - coverMgr.SaveCoverArtToDatabase(ref song, ref coverArt); SaveSongToDatabase(song); } @@ -308,20 +356,23 @@ namespace Icarus.Controllers.Managers var song = new Song(); _logger.Info("Assigning song filename"); song.SongDirectory = _tempDirectoryRoot; - song.Filename = song.GenerateFilename(1); + var filename = song.GenerateFilename(1); + song.Filename = filename; using (var filestream = new FileStream(song.SongPath(), FileMode.Create)) { - _logger.Info("Saving song to temporary directory"); + _logger.Info("Saving temp song: {0}", song.SongPath()); await songFile.CopyToAsync(filestream); } - - MetadataRetriever meta = new MetadataRetriever(); - song = meta.RetrieveMetaData(song.SongPath()); + await Task.Run(() => + { + MetadataRetriever meta = new MetadataRetriever(); + song = meta.RetrieveMetaData(song.SongPath()); + }); song.SongDirectory = _tempDirectoryRoot; - song.Filename = song.GenerateFilename(1); song.DateCreated = DateTime.Now; + song.Filename = filename; return song; } diff --git a/Controllers/v1/SongDataController.cs b/Controllers/v1/SongDataController.cs index deb37d9..e44bebe 100644 --- a/Controllers/v1/SongDataController.cs +++ b/Controllers/v1/SongDataController.cs @@ -96,24 +96,33 @@ namespace Icarus.Controllers.V1 // [HttpPost("upload"), DisableRequestSizeLimit] [Route("private-scoped")] - [Authorize("upload:songs")] - public async Task Post([FromForm(Name = "file")] List songData) + // [Authorize("upload:songs")] + public IActionResult Post([FromForm(Name = "file")] List songData) { + var token = ParseBearerTokenFromHeader(); + var tokMgr = new TokenManager(_config); + + if (!tokMgr.IsTokenValid("upload:songs", token)) + { + return StatusCode(401, "Not allowed"); + } + try { - Console.WriteLine("Uploading song..."); + // Console.WriteLine("Uploading song..."); _logger.LogInformation("Uploading song..."); var uploads = _songTempDir; - Console.WriteLine($"Song Root Path {uploads}"); + // Console.WriteLine($"Song Root Path {uploads}"); _logger.LogInformation($"Song root path {uploads}"); foreach (var sng in songData) - if (sng.Length > 0) { - Console.WriteLine($"Song filename {sng.FileName}"); + if (sng.Length > 0) + { + // Console.WriteLine($"Song filename {sng.FileName}"); _logger.LogInformation($"Song filename {sng.FileName}"); - await _songMgr.SaveSongToFileSystem(sng); + _songMgr.SaveSongToFileSystem(sng).Wait(); } return Ok(); @@ -134,7 +143,7 @@ namespace Icarus.Controllers.V1 [HttpPost("upload/with/data")] [Route("private-scoped")] // [Authorize("upload:songs")] - public async Task Post ([FromForm] UploadSongWithDataForm up) + public IActionResult Post ([FromForm] UploadSongWithDataForm up) { var token = ParseBearerTokenFromHeader(); var tokMgr = new TokenManager(_config); @@ -151,7 +160,7 @@ namespace Icarus.Controllers.V1 var song = Newtonsoft.Json.JsonConvert.DeserializeObject(up.SongFile); _logger.LogInformation($"Song title: {song.Title}"); - await _songMgr.SaveSongToFileSystem(up.SongData, up.CoverArtData, song); + _songMgr.SaveSongToFileSystem(up.SongData, up.CoverArtData, song); } } catch (Exception ex) diff --git a/Models/Song.cs b/Models/Song.cs index fe7107b..6c54c70 100644 --- a/Models/Song.cs +++ b/Models/Song.cs @@ -1,6 +1,7 @@ using System; using System.ComponentModel.DataAnnotations.Schema; using System.Linq; +using System.Threading.Tasks; using Newtonsoft.Json; @@ -79,6 +80,21 @@ namespace Icarus.Models s[random.Next(s.Length)]).ToArray()); var extension = ".mp3"; + return flag == 0 ? filename : $"{filename}{extension}"; + } + public async Task GenerateFilenameAsync(int flag = 0) + { + const int length = 25; + const string chars = "ABCDEF0123456789"; + var extension = ".mp3"; + var random = new Random(); + var filename = await Task.Run(() => + { + return new string(Enumerable.Repeat(chars, length).Select(s => + s[random.Next(s.Length)]).ToArray()); + }); + + return flag == 0 ? filename : $"{filename}{extension}"; } #endregion From e5bea187f4ef3171e0b611067352af3bc44f2709 Mon Sep 17 00:00:00 2001 From: Kun Deng Date: Sat, 27 Aug 2022 20:16:01 -0400 Subject: [PATCH 08/21] Token validation --- Controllers/Managers/TokenManager.cs | 31 +++++++++++++++++----------- Models/Token.cs | 10 +++++++++ 2 files changed, 29 insertions(+), 12 deletions(-) diff --git a/Controllers/Managers/TokenManager.cs b/Controllers/Managers/TokenManager.cs index c2ca547..610a891 100644 --- a/Controllers/Managers/TokenManager.cs +++ b/Controllers/Managers/TokenManager.cs @@ -128,7 +128,7 @@ namespace Icarus.Controllers.Managers var result = false; var token = DecodeToken(accessToken); - if (token == null) + if (token == null || token.Erroneous()) { return result; } @@ -148,19 +148,26 @@ namespace Icarus.Controllers.Managers var rsaParams = GetRSAPublic(_publicKey); Token tok = null; - using (var rsa = new RSACryptoServiceProvider()) + try { - rsa.ImportParameters(rsaParams); + using (var rsa = new RSACryptoServiceProvider()) + { + rsa.ImportParameters(rsaParams); - IJsonSerializer serializer = new JsonNetSerializer(); - IDateTimeProvider provider = new UtcDateTimeProvider(); - IJwtValidator validator = new JwtValidator(serializer, provider); - IBase64UrlEncoder urlEncoder = new JwtBase64UrlEncoder(); - var algorithm = new JWT.Algorithms.RS256Algorithm(rsa); - IJwtDecoder decoder = new JwtDecoder(serializer, validator, urlEncoder, algorithm); - - var json = decoder.Decode(accessToken); - tok = JsonConvert.DeserializeObject(json); + IJsonSerializer serializer = new JsonNetSerializer(); + IDateTimeProvider provider = new UtcDateTimeProvider(); + IJwtValidator validator = new JwtValidator(serializer, provider); + IBase64UrlEncoder urlEncoder = new JwtBase64UrlEncoder(); + var algorithm = new JWT.Algorithms.RS256Algorithm(rsa); + IJwtDecoder decoder = new JwtDecoder(serializer, validator, urlEncoder, algorithm); + + var json = decoder.Decode(accessToken); + tok = JsonConvert.DeserializeObject(json); + } + } + catch (Exception ex) + { + _logger.Error("An error occurred: {0}", ex.Message); } diff --git a/Models/Token.cs b/Models/Token.cs index b1d4214..bfa5889 100644 --- a/Models/Token.cs +++ b/Models/Token.cs @@ -41,6 +41,16 @@ namespace Icarus.Models return result; } + + public bool Erroneous() + { + var result = true; + Func isEmpty = a => string.IsNullOrEmpty(a); + result = (!isEmpty(Scope) && !isEmpty(Audience) && !isEmpty(Issuer) && + Expiration > 0 && Issued > 0) ? false : true; + + return result; + } #endregion } } \ No newline at end of file From 665407aac59e8745717e46d15c704e8ae51a598c Mon Sep 17 00:00:00 2001 From: Kun Deng Date: Sat, 27 Aug 2022 20:35:34 -0400 Subject: [PATCH 09/21] Code cleanup --- Authorization/Handlers/HasScopeHandler.cs | 13 +- Certs/Signing.cs | 9 +- Controllers/Managers/SongManager.cs | 30 ----- Controllers/Managers/TokenManager.cs | 10 -- Exceptions/InvalidCredentialsException.cs | 20 --- Models/Shared/User.cs | 21 --- Models/Shared/UserCredentials.cs | 8 -- Repositories/UserRepository.cs | 37 ------ Services/AuthenticationService.cs | 31 ----- Services/TokenService.cs | 82 ------------ Services/UserService.sc.cs | 53 -------- Startup.cs | 154 +--------------------- 12 files changed, 4 insertions(+), 464 deletions(-) delete mode 100644 Exceptions/InvalidCredentialsException.cs delete mode 100644 Models/Shared/User.cs delete mode 100644 Models/Shared/UserCredentials.cs delete mode 100644 Repositories/UserRepository.cs delete mode 100644 Services/AuthenticationService.cs delete mode 100644 Services/TokenService.cs delete mode 100644 Services/UserService.sc.cs diff --git a/Authorization/Handlers/HasScopeHandler.cs b/Authorization/Handlers/HasScopeHandler.cs index 82e54e6..8f51065 100644 --- a/Authorization/Handlers/HasScopeHandler.cs +++ b/Authorization/Handlers/HasScopeHandler.cs @@ -12,18 +12,7 @@ namespace Icarus.Authorization.Handlers { protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, HasScopeRequirement requirement) { - var issuer = requirement.Issuer; - var scope = requirement.Scope; - var claim = string.Empty; - // if (!context.User.HasClaim(c => c.Type == "scope" && c.Issuer == requirement.Issuer)) - if (!context.User.HasClaim(c => - { - var i = 0; - var b = 99; - claim = c.Type; - return c.Type == "scope"; - }) - ) + if (!context.User.HasClaim(c => c.Type == "scope" && c.Issuer == requirement.Issuer)) { return Task.CompletedTask; } diff --git a/Certs/Signing.cs b/Certs/Signing.cs index 4f53a26..55bfc8e 100644 --- a/Certs/Signing.cs +++ b/Certs/Signing.cs @@ -8,7 +8,6 @@ using Org.BouncyCastle.OpenSsl; namespace Icarus.Certs { - public class SigningIssuerCertificate : IDisposable { private readonly RSACryptoServiceProvider _rsa; @@ -18,7 +17,6 @@ namespace Icarus.Certs _rsa = new RSACryptoServiceProvider(); } - // public or private key? public RsaSecurityKey GetIssuerSigningKey(string publicKeyPath) { var file = publicKeyPath; @@ -47,22 +45,17 @@ namespace Icarus.Certs public class SigningAudienceCertificate : IDisposable { - // private readonly RSA _rsa; private readonly RSACryptoServiceProvider _rsa; public SigningAudienceCertificate() { _rsa = new RSACryptoServiceProvider(); } - // 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)) { diff --git a/Controllers/Managers/SongManager.cs b/Controllers/Managers/SongManager.cs index d0a39de..500b50b 100644 --- a/Controllers/Managers/SongManager.cs +++ b/Controllers/Managers/SongManager.cs @@ -180,18 +180,9 @@ namespace Icarus.Controllers.Managers DirectoryManager dirMgr = new DirectoryManager(_config, song); dirMgr.CreateDirectory(); - /** - await Task.Run(() => - { - System.IO.File.Delete(song.SongPath()); - }); - */ - var tempPath = song.SongPath(); song.Filename = song.GenerateFilename(1); var filePath = $"{dirMgr.SongDirectory}{song.Filename}"; - // $"{dirMgr.SongDirectory}"; - // filePath += $"{song.Filename}"; _logger.Info($"Absolute song path: {filePath}"); @@ -202,7 +193,6 @@ namespace Icarus.Controllers.Managers { var songBytes = System.IO.File.ReadAllBytes(tempPath); - // System.IO.File.WriteAllBytes(filePath, songBytes); _logger.Info("Saving song to the filesystem"); fileStream.Write(songBytes, 0, songBytes.Count()); @@ -267,26 +257,9 @@ namespace Icarus.Controllers.Managers var filePath = song.SongPath(); _logger.Info($"Absolute song path: {filePath}"); - if (System.IO.File.Exists(filePath)) - { - _logger.Info("Why does the file exist?"); - } - - if (System.IO.File.Exists(tempPath)) - { - // System.IO.File.Move(tempPath, filePath); - // _logger.Info("Moved song from temporary location to final location"); - } - using (var fileStream = new FileStream(filePath, FileMode.Create)) { var songBytes = System.IO.File.ReadAllBytes(tempPath); - // fileStream.ReadAsync(songBytes) - - if (System.IO.File.Exists(filePath)) - { - _logger.Info("Why does the file exist?"); - } try { @@ -297,9 +270,6 @@ namespace Icarus.Controllers.Managers } else { - // System.IO.File.WriteAllBytes(filePath, songBytes); - // - fileStream.Write(songBytes, 0, songBytes.Count()); _logger.Info("Saved song to filesystem: {0}", filePath); diff --git a/Controllers/Managers/TokenManager.cs b/Controllers/Managers/TokenManager.cs index 610a891..c00452f 100644 --- a/Controllers/Managers/TokenManager.cs +++ b/Controllers/Managers/TokenManager.cs @@ -21,16 +21,6 @@ using Icarus.Models; namespace Icarus.Controllers.Managers { #region Classes - /** - { - "scope":"download:songs read:song_details upload:songs delete:songs read:albums read:artists update:songs stream:songs read:genre read:year download:cover_art", - "exp":1661616071, - "aud":"http://localhost:5002", - "iss":"http://localhost:5002", - "iat":1661529671 -} - */ - public class TokenManager : BaseManager { #region Fields diff --git a/Exceptions/InvalidCredentialsException.cs b/Exceptions/InvalidCredentialsException.cs deleted file mode 100644 index 2145caa..0000000 --- a/Exceptions/InvalidCredentialsException.cs +++ /dev/null @@ -1,20 +0,0 @@ -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 deleted file mode 100644 index 5a467d1..0000000 --- a/Models/Shared/User.cs +++ /dev/null @@ -1,21 +0,0 @@ -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/Shared/UserCredentials.cs b/Models/Shared/UserCredentials.cs deleted file mode 100644 index 1b8bf3d..0000000 --- a/Models/Shared/UserCredentials.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Icarus.Models.Shared -{ - public class UserCredentials - { - public string Username { get; set; } - public string Password { get; set; } - } -} \ No newline at end of file diff --git a/Repositories/UserRepository.cs b/Repositories/UserRepository.cs deleted file mode 100644 index 073fbb2..0000000 --- a/Repositories/UserRepository.cs +++ /dev/null @@ -1,37 +0,0 @@ -// 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 deleted file mode 100644 index 61c1354..0000000 --- a/Services/AuthenticationService.cs +++ /dev/null @@ -1,31 +0,0 @@ -// 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 deleted file mode 100644 index 3c0e598..0000000 --- a/Services/TokenService.cs +++ /dev/null @@ -1,82 +0,0 @@ -// using JwtAuthentication.AsymmetricEncryption.Certificates; -using System; -using System.IdentityModel.Tokens.Jwt; -using System.Security.Claims; -using System.Linq; - -using Microsoft.IdentityModel.Tokens; -using Microsoft.Extensions.Configuration; - -using Icarus; -using Icarus.Certs; -using Icarus.Database.Contexts; -using Icarus.Repositories; -using Icarus.Models.Shared; -// using JwtAuthentication.Shared; -// using JwtAuthentication.Shared.Models; - -namespace Icarus.Services -{ - public class TokenService - { - private Microsoft.Extensions.Configuration.IConfiguration _configuration; - private readonly UserRepository userRepository; - private readonly UserContext _userContext; - private readonly Icarus.Certs.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 Icarus.Certs.SigningAudienceCertificate(); - _publickKeyPath = configuration["RSAKeys:PublicKeyPath"]; - } - */ - - public TokenService(Microsoft.Extensions.Configuration.IConfiguration configuration) - { - this._configuration = configuration; - // this.userRepository = userRepository; - this._userContext = new UserContext(configuration.GetConnectionString("DefaultConnection")); - signingAudienceCertificate = new Icarus.Certs.SigningAudienceCertificate(); - _publickKeyPath = configuration["RSAKeys:PublicKeyPath"]; - } - - public string GetToken(string username) - { - // var user = userRepository.GetUser(username); - var user = _userContext.Users.FirstOrDefault(usr => usr.Username.Equals(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 deleted file mode 100644 index 19684f4..0000000 --- a/Services/UserService.sc.cs +++ /dev/null @@ -1,53 +0,0 @@ -// using JwtAuthentication.Shared.Exceptions; -// using JwtAuthentication.Shared.Models; -using System.Linq; - -using Microsoft.Extensions.Configuration; - -using Icarus.Exceptions; -using Icarus.Models.Shared; -using Icarus.Services; -using Icarus.Repositories; -using Icarus.Database.Contexts; - -namespace Icarus.Services -{ - public class UserService - { - private Microsoft.Extensions.Configuration.IConfiguration _configuration; - private readonly UserRepository userRepository; - private readonly UserContext _userContext; - - public UserService(UserRepository userRepository, Microsoft.Extensions.Configuration.IConfiguration configuration) - { - this._configuration = configuration; - this.userRepository = userRepository; - } - - // public UserService(UserContext userContext, Microsoft.Extensions.Configuration.IConfiguration configuration) - public UserService(Microsoft.Extensions.Configuration.IConfiguration configuration) - { - this._configuration = configuration; - this._userContext = new UserContext(configuration.GetConnectionString("DefaultConnection")); - } - - public void ValidateCredentials(UserCredentials userCredentials) - { - // Icarus.Models.User user = userRepository.GetUser(userCredentials.Username); - var user = _userContext.Users.FirstOrDefault(usr => usr.Username.Equals(userCredentials.Username)); - bool isValid = user != null && AreValidCredentials(userCredentials, user); - - if (!isValid) - { - throw new InvalidCredentialsException(); - } - } - - private static bool AreValidCredentials(UserCredentials userCredentials, Icarus.Models.User user) - { - // TODO: Has the user provided password and compair it to the hash retrieved from - // the DB - return true; - } - } -} \ No newline at end of file diff --git a/Startup.cs b/Startup.cs index 05fb68d..6dea1e4 100644 --- a/Startup.cs +++ b/Startup.cs @@ -1,18 +1,7 @@ using System; -using System.Collections.Generic; -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; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.HttpsPolicy; -using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -23,84 +12,14 @@ 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 class ServiceStartup { public static IServiceCollection AddAsymmetricAuthentication(this IServiceCollection services, IConfiguration configuration) - // public static IServiceCollection AddAsymmetricAuthentication(this IServiceCollection services, IConfiguration configuration) { var issuerSigningCertificate = new Icarus.Certs.SigningIssuerCertificate(); RsaSecurityKey issuerSigningKey = issuerSigningCertificate.GetIssuerSigningKey(configuration["RSAKeys:PublicKeyPath"]); @@ -151,69 +70,8 @@ namespace Icarus var domain = $"https://{auth_id}/"; var audience = Configuration["Auth0:ApiIdentifier"]; - /** - services - .AddAuthentication(JwtBearerDefaults.AuthenticationScheme) - .AddJwtBearer(options => - { - options.Authority = domain; - options.Audience = audience; - }); - - services.AddAuthorization(options => - { - options.AddPolicy("download:songs", policy => - policy.Requirements - .Add(new HasScopeRequirement("download:songs", domain))); - - options.AddPolicy("download:cover_art", policy => - policy.Requirements - .Add(new HasScopeRequirement("download:cover_art", domain))); - - options.AddPolicy("upload:songs", policy => - policy.Requirements - .Add(new HasScopeRequirement("upload:songs", domain))); - - options.AddPolicy("delete:songs", policy => - policy.Requirements - .Add(new HasScopeRequirement("delete:songs", domain))); - - options.AddPolicy("read:song_details", policy => - policy.Requirements - .Add(new HasScopeRequirement("read:song_details", domain))); - - options.AddPolicy("update:songs", policy => - policy.Requirements - .Add(new HasScopeRequirement("update:songs", domain))); - - options.AddPolicy("read:artists", policy => - policy.Requirements - .Add(new HasScopeRequirement("read:artists", domain))); - - options.AddPolicy("read:albums", policy => - policy.Requirements - .Add(new HasScopeRequirement("read:albums", domain))); - - options.AddPolicy("read:genre", policy => - policy.Requirements - .Add(new HasScopeRequirement("read:genre", domain))); - - options.AddPolicy("read:year", policy => - policy.Requirements - .Add(new HasScopeRequirement("read:year", domain))); - - options.AddPolicy("stream:songs", policy => - policy.Requirements - .Add(new HasScopeRequirement("stream:songs", domain))); - }); - - - services.AddSingleton(); - */ - var connString = Configuration.GetConnectionString("DefaultConnection"); - services.AddDbContext(options => options.UseMySQL(connString)); services.AddDbContext(options => options.UseMySQL(connString)); services.AddDbContext(options => options.UseMySQL(connString)); @@ -223,20 +81,12 @@ namespace Icarus services.AddAsymmetricAuthentication(Configuration); - // services.AddTransient(au => new AuthenticationService(new UserService(new UserRepository(), Configuration), new TokenService(new UserRepository(), Configuration), Configuration)); + /** services.AddTransient(au => new AuthenticationService(new UserService(Configuration), new TokenService(Configuration), Configuration)); - // services.AddTransient(us => new UserService(new UserRepository(), Configuration)); services.AddTransient(us => new UserService(Configuration)); - // services.AddTransient(tk => new TokenService(new UserRepository(), Configuration)); services.AddTransient(tk => new TokenService(Configuration)); services.AddTransient(); services.AddTransient(uc => new UserContext(connString)); - /** - services.AddTransient((providers) => - { - return new Func((numParams) => - new UserContext(providers.GetRequiredService, numParams)); - }); */ services.AddControllers() From f8ec65fd2907fafe9e9331a10c3f25bb8946fe96 Mon Sep 17 00:00:00 2001 From: Kun Deng Date: Sat, 27 Aug 2022 21:35:49 -0400 Subject: [PATCH 10/21] Tokens are validated in respective endpoints --- Controllers/v1/AlbumController.cs | 17 ++++--- Controllers/v1/ArtistController.cs | 17 ++++--- Controllers/v1/BaseController.cs | 49 +++++++++++++++++++ Controllers/v1/CoverArtController.cs | 16 ++++-- Controllers/v1/GenreController.cs | 15 ++++-- .../v1/SongCompressedDataControllers.cs | 9 ++-- Controllers/v1/SongController.cs | 39 +++++---------- Controllers/v1/SongDataController.cs | 49 +++++-------------- Controllers/v1/SongStreamController.cs | 30 +----------- 9 files changed, 126 insertions(+), 115 deletions(-) create mode 100644 Controllers/v1/BaseController.cs diff --git a/Controllers/v1/AlbumController.cs b/Controllers/v1/AlbumController.cs index cdb6502..903494a 100644 --- a/Controllers/v1/AlbumController.cs +++ b/Controllers/v1/AlbumController.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Configuration; using System.Linq; using Microsoft.AspNetCore.Authorization; @@ -10,18 +9,16 @@ using Microsoft.Extensions.Logging; using Icarus.Models; using Icarus.Database.Contexts; -// using Icarus.Database.Repositories; namespace Icarus.Controllers.V1 { [Route("api/v1/album")] [ApiController] - public class AlbumController : ControllerBase + public class AlbumController : BaseController { #region Fields private readonly ILogger _logger; private string _connectionString; - private IConfiguration _config; #endregion @@ -41,9 +38,13 @@ namespace Icarus.Controllers.V1 #region HTTP Routes [HttpGet] - [Authorize("read:albums")] public IActionResult Get() { + if (!IsTokenValid("read:albums")) + { + return StatusCode(401, "Not allowed"); + } + List albums = new List(); var albumContext = new AlbumContext(_connectionString); @@ -57,9 +58,13 @@ namespace Icarus.Controllers.V1 } [HttpGet("{id}")] - [Authorize("read:albums")] public IActionResult Get(int id) { + if (!IsTokenValid("read:albums")) + { + return StatusCode(401, "Not allowed"); + } + Album album = new Album { AlbumID = id diff --git a/Controllers/v1/ArtistController.cs b/Controllers/v1/ArtistController.cs index 0531149..51e21b2 100644 --- a/Controllers/v1/ArtistController.cs +++ b/Controllers/v1/ArtistController.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Configuration; using System.Linq; using Microsoft.AspNetCore.Authorization; @@ -15,12 +13,11 @@ namespace Icarus.Controllers.V1 { [Route("api/v1/artist")] [ApiController] - public class ArtistController : ControllerBase + public class ArtistController : BaseController { #region Fields private readonly ILogger _logger; private string _connectionString; - private IConfiguration _config; #endregion @@ -40,9 +37,13 @@ namespace Icarus.Controllers.V1 #region HTTP Routes [HttpGet] - [Authorize("read:artists")] public IActionResult Get() { + if (!IsTokenValid("read:artists")) + { + return StatusCode(401, "Not allowed"); + } + var artistContext = new ArtistContext(_connectionString); var artists = artistContext.Artists.ToList(); @@ -54,9 +55,13 @@ namespace Icarus.Controllers.V1 } [HttpGet("{id}")] - [Authorize("read:artists")] public IActionResult Get(int id) { + if (!IsTokenValid("read:artists")) + { + return StatusCode(401, "Not allowed"); + } + Artist artist = new Artist { ArtistID = id diff --git a/Controllers/v1/BaseController.cs b/Controllers/v1/BaseController.cs new file mode 100644 index 0000000..911d273 --- /dev/null +++ b/Controllers/v1/BaseController.cs @@ -0,0 +1,49 @@ +using System; +using System.Linq; + +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Configuration; + +using Icarus.Controllers.Managers; + + +namespace Icarus.Controllers.V1 +{ + public class BaseController : ControllerBase + { + #region Fiends + protected IConfiguration _config; + #endregion + + + #region Methods + protected string ParseBearerTokenFromHeader() + { + var token = string.Empty; + const string tokenType = "Bearer"; + const string otherTokenType = "Jwt"; + + var req = Request; + var auth = req.Headers.Authorization; + var val = auth.ToString(); + + if ((val.Contains(tokenType) || val.Contains(otherTokenType)) && val.Split(" ").Count() > 1) + { + var split = val.Split(" "); + token = split[1]; + } + + + return token; + } + + protected bool IsTokenValid(string scope) + { + var token = ParseBearerTokenFromHeader(); + var tokMgr = new TokenManager(_config); + + return tokMgr.IsTokenValid(scope, token); + } + #endregion + } +} \ No newline at end of file diff --git a/Controllers/v1/CoverArtController.cs b/Controllers/v1/CoverArtController.cs index 103d1c3..482b6a4 100644 --- a/Controllers/v1/CoverArtController.cs +++ b/Controllers/v1/CoverArtController.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.IO; using System.Linq; using System.Threading.Tasks; @@ -17,12 +15,11 @@ namespace Icarus.Controllers.V1 { [Route("api/v1/coverart")] [ApiController] - public class CoverArtController : ControllerBase + public class CoverArtController : BaseController { #region Fields private readonly ILogger _logger; private string _connectionString; - private IConfiguration _config; #endregion @@ -39,6 +36,11 @@ namespace Icarus.Controllers.V1 #region HTTP Routes public IActionResult Get() { + if (!IsTokenValid("read:songs")) + { + return StatusCode(401, "Not allowed"); + } + var coverArtContext = new CoverArtContext(_connectionString); var coverArtRecords = coverArtContext.CoverArtImages.ToList(); @@ -56,9 +58,13 @@ namespace Icarus.Controllers.V1 } [HttpGet("{id}")] - [Authorize("download:cover_art")] public async Task Get(int id) { + if (!IsTokenValid("download:cover_art")) + { + return StatusCode(401, "Not allowed"); + } + var coverArt = new CoverArt { CoverArtID = id }; var coverArtContext = new CoverArtContext(_connectionString); diff --git a/Controllers/v1/GenreController.cs b/Controllers/v1/GenreController.cs index a53bc4e..8e29502 100644 --- a/Controllers/v1/GenreController.cs +++ b/Controllers/v1/GenreController.cs @@ -14,12 +14,11 @@ namespace Icarus.Controllers.V1 { [Route("api/v1/genre")] [ApiController] - public class GenreController : ControllerBase + public class GenreController : BaseController { #region Fields private readonly ILogger _logger; private string _connectionString; - private IConfiguration _config; #endregion @@ -39,9 +38,13 @@ namespace Icarus.Controllers.V1 #region HTTP Routes [HttpGet] - [Authorize("read:genre")] public IActionResult Get() { + if (!IsTokenValid("read:genre")) + { + return StatusCode(401, "Not allowed"); + } + var genres = new List(); var genreStore = new GenreContext(_connectionString); @@ -55,9 +58,13 @@ namespace Icarus.Controllers.V1 } [HttpGet("{id}")] - [Authorize("read:genre")] public IActionResult Get(int id) { + if (!IsTokenValid("read:genre")) + { + return StatusCode(401, "Not allowed"); + } + var genre = new Genre { GenreID = id diff --git a/Controllers/v1/SongCompressedDataControllers.cs b/Controllers/v1/SongCompressedDataControllers.cs index 97f36c8..cc96800 100644 --- a/Controllers/v1/SongCompressedDataControllers.cs +++ b/Controllers/v1/SongCompressedDataControllers.cs @@ -19,11 +19,10 @@ namespace Icarus.Controllers.V1 { [Route("api/v1/song/compressed/data")] [ApiController] - public class SongCompressedDataController : ControllerBase + public class SongCompressedDataController : BaseController { #region Fields private string _connectionString; - private IConfiguration _config; private string _songTempDir; private string _archiveDir; #endregion @@ -46,9 +45,13 @@ namespace Icarus.Controllers.V1 #region API Routes [HttpGet("{id}")] - [Authorize("download:songs")] public async Task Get(int id) { + if (!IsTokenValid("download:songs")) + { + return StatusCode(401, "Not allowed"); + } + var context = new SongContext(_connectionString); SongCompression cmp = new SongCompression(_archiveDir); diff --git a/Controllers/v1/SongController.cs b/Controllers/v1/SongController.cs index 77d755b..d94f78f 100644 --- a/Controllers/v1/SongController.cs +++ b/Controllers/v1/SongController.cs @@ -18,12 +18,11 @@ namespace Icarus.Controllers.V1 { [Route("api/v1/song")] [ApiController] - public class SongController : ControllerBase + public class SongController : BaseController { #region Fields private readonly ILogger _logger; private string _connectionString; - private IConfiguration _config; private SongManager _songMgr; #endregion @@ -44,35 +43,13 @@ namespace Icarus.Controllers.V1 #region Methods - private string ParseBearerTokenFromHeader() - { - var token = string.Empty; - const string tokenType = "Bearer"; - - var req = Request; - var auth = req.Headers.Authorization; - var val = auth.ToString(); - - if (val.Contains(tokenType) && val.Split(" ").Count() > 1) - { - var split = val.Split(" "); - token = split[1]; - } - - - return token; - } #region HTTP Endpoints [HttpGet] - // [Authorize("read:song_details")] public IActionResult Get() { - var token = ParseBearerTokenFromHeader(); - var tokMgr = new TokenManager(_config); - - if (!tokMgr.IsTokenValid("read:song_details", token)) + if (!IsTokenValid("read:song_details")) { return StatusCode(401, "Not allowed"); } @@ -92,9 +69,13 @@ namespace Icarus.Controllers.V1 } [HttpGet("{id}")] - [Authorize("read:song_details")] public IActionResult Get(int id) { + if (!IsTokenValid("read:song_details")) + { + return StatusCode(401, "Not allowed"); + } + var context = new SongContext(_connectionString); Song song = new Song { SongID = id }; @@ -108,10 +89,14 @@ namespace Icarus.Controllers.V1 return NotFound(); } - [Authorize("update:songs")] [HttpPut("{id}")] public IActionResult Put(int id, [FromBody] Song song) { + if (!IsTokenValid("update:songs")) + { + return StatusCode(401, "Not allowed"); + } + var context = new SongContext(_connectionString); song.SongID = id; diff --git a/Controllers/v1/SongDataController.cs b/Controllers/v1/SongDataController.cs index e44bebe..4760939 100644 --- a/Controllers/v1/SongDataController.cs +++ b/Controllers/v1/SongDataController.cs @@ -20,11 +20,10 @@ namespace Icarus.Controllers.V1 { [Route("api/v1/song/data")] [ApiController] - public class SongDataController : ControllerBase + public class SongDataController : BaseController { #region Fields private string _connectionString; - private IConfiguration _config; private ILogger _logger; private SongManager _songMgr; private string _songTempDir; @@ -47,33 +46,15 @@ namespace Icarus.Controllers.V1 #endregion - private string ParseBearerTokenFromHeader() - { - var token = string.Empty; - const string tokenType = "Bearer"; - const string otherTokenType = "Jwt"; - - var req = Request; - var auth = req.Headers.Authorization; - var val = auth.ToString(); - - if (val.Contains(tokenType) && val.Split(" ").Count() > 1 || - val.Contains(otherTokenType) && val.Split(" ").Count() > 1) - { - var split = val.Split(" "); - token = split[1]; - } - - - return token; - } - - [HttpGet("download/{id}")] [Route("private-scoped")] - [Authorize("download:songs")] public async Task Get(int id) { + if (!IsTokenValid("download:songs")) + { + return StatusCode(401, "Not allowed"); + } + var songContext = new SongContext(_connectionString); var songMetaData = songContext.RetrieveRecord(new Song { SongID = id}); @@ -96,13 +77,9 @@ namespace Icarus.Controllers.V1 // [HttpPost("upload"), DisableRequestSizeLimit] [Route("private-scoped")] - // [Authorize("upload:songs")] public IActionResult Post([FromForm(Name = "file")] List songData) { - var token = ParseBearerTokenFromHeader(); - var tokMgr = new TokenManager(_config); - - if (!tokMgr.IsTokenValid("upload:songs", token)) + if (!IsTokenValid("upload:songs")) { return StatusCode(401, "Not allowed"); } @@ -142,13 +119,9 @@ namespace Icarus.Controllers.V1 // [HttpPost("upload/with/data")] [Route("private-scoped")] - // [Authorize("upload:songs")] public IActionResult Post ([FromForm] UploadSongWithDataForm up) { - var token = ParseBearerTokenFromHeader(); - var tokMgr = new TokenManager(_config); - - if (!tokMgr.IsTokenValid("upload:songs", token)) + if (!IsTokenValid("upload:songs")) { return StatusCode(401, "Not allowed"); } @@ -172,9 +145,13 @@ namespace Icarus.Controllers.V1 } [HttpDelete("delete/{id}")] - [Authorize("delete:songs")] public IActionResult Delete(int id) { + if (!IsTokenValid("delete:songs")) + { + return StatusCode(401, "Not allowed"); + } + var songContext = new SongContext(_connectionString); var songMetaData = new Song{ SongID = id }; diff --git a/Controllers/v1/SongStreamController.cs b/Controllers/v1/SongStreamController.cs index 01a8c44..b273a08 100644 --- a/Controllers/v1/SongStreamController.cs +++ b/Controllers/v1/SongStreamController.cs @@ -20,12 +20,11 @@ namespace Icarus.Controllers.V1 { [Route("api/v1/song/stream")] [ApiController] - public class SongStreamController : ControllerBase + public class SongStreamController : BaseController { #region Fields private ILogger _logger; private string _connectionString; - private IConfiguration _config; #endregion @@ -43,39 +42,14 @@ namespace Icarus.Controllers.V1 #endregion - private string ParseBearerTokenFromHeader() - { - var token = string.Empty; - const string tokenType = "Bearer"; - - var req = Request; - var auth = req.Headers.Authorization; - var val = auth.ToString(); - - if (val.Contains(tokenType) && val.Split(" ").Count() > 1) - { - var split = val.Split(" "); - token = split[1]; - } - - - return token; - } - #region HTTP endpoints [HttpGet("{id}")] - // [Authorize("stream:songs")] public async Task Get(int id) { - var token = ParseBearerTokenFromHeader(); - var tokMgr = new TokenManager(_config); - - /** - if (!tokMgr.IsTokenValid("stream:songs", token)) + if (!IsTokenValid("stream:songs")) { return StatusCode(401, "Not allowed"); } - */ var context = new SongContext(_config.GetConnectionString("DefaultConnection")); From a423e6e22018538945091254cd597f011b9d0ca6 Mon Sep 17 00:00:00 2001 From: Kun Deng Date: Tue, 30 Aug 2022 21:39:47 -0400 Subject: [PATCH 11/21] Update README.md Updated to reflect removing dependence on Auth0. --- README.md | 91 ++++++++++++++----------------------------------------- 1 file changed, 23 insertions(+), 68 deletions(-) diff --git a/README.md b/README.md index a49349e..e24d59e 100644 --- a/README.md +++ b/README.md @@ -10,10 +10,8 @@ One can interface with Icarus the music server either by: * [IcarusDownloadManager](https://github.com/amazing-username/IcarusDownloadManager) - ## Built With - * C# [.NET](https://dotnet.microsoft.com/) 6 * [MySql](https://www.nuget.org/packages/MySql.Data/) * OpenSSL @@ -34,69 +32,19 @@ One can interface with Icarus the music server either by: * System.IdentityModel.Tokens.Jwt * [TagLib#](https://github.com/mono/taglib-sharp) -![image](https://user-images.githubusercontent.com/14333136/56252069-28532d00-6084-11e9-896d-1a3c378014ef.png) + ## Getting started + There are several things that need to be completed to properly setup and secure the API. -1. Auth0 API configuration -2. Creating RSA keys -3. API filesystem paths -4. Database connection string -5. Migrations +1. Creating RSA keys +2. API filesystem paths +3. Database connection string +4. Migrations -### Auth0 API configuration - -Securing Icarus is required, preventing the API from being publicly accessible. To do so, create an Auth0 account (it's free), for the sake of this section of the documentation, I will not go over how to create an Auth0 account. Once created, create a tentant and proceed to create an API -

- -

- -Create the API and enter an approrpiate name and identified. For the identified, append **api** like in the example -

- -

-Replace [domain] with the domain name of the created tenant. This can be found in the Default App from the Application menu. Replace [identifier] with the identifer root name in the appsettings environment file. Not the friendly name but the root name of the identifier, omitting the http protocol and the *api* path. - -```Json - "Auth0": { - "Domain": "[domain].auth0.com", - "ApiIdentifier": "https://[identifier]/api" - }, -``` - -For the sake of this section, I will not go over configuring the API to accept the signing algorithm since it has already been configured in the [Startip](Startup.cs).cs file. Click on permissions to create the permissions for the API. -

- -

- -The permissions ensure that a validated user can interact with the API with a token that has not expired. Ensure that the permissions match, the description can change but the permission identifier must match. -

- -

- -On the left side, click on Application and create a new Application. Choose the Machine to Machine Application -

- -

- -With the grant permissions you created from the API, enable all the permissions. This is important because if they are not enabled then even with a valid token the request will return 403 (unauthorized) -

- -

- -From the Application page, copy the client id and client secret. These values will be used for the API to interact with API. -

- -

-Enter the information in the corresponding appsettings json file -```Json - "Auth0": { - "ClientId":"", - "ClientSecret":"" - }, -``` ### Creating RSA keys + 1. Create private key ``` openssl genrsa -out private.pem 2048 @@ -115,6 +63,15 @@ Configure the key paths in the config files } ``` +Replace [domain] with the domain name that represent's your domain. Replace [identifier] with the identifer root name in the appsettings environment file. Not the friendly name but the root name of the identifier, omitting the http protocol and the *api* path. + +```Json +"Auth0": { + "Domain": "[domain].auth0.com", + "ApiIdentifier": "https://[identifier]/api" +}, +``` + ### API filesystem paths For the purposes of properly uploading, downloading, updating, deleting, and streaming songs the API filesystem paths must be configured. What is meant by this is that the `RootMusicPath` directory where all music will be stored must exist as well as the `ArchivePath` and `TemporaryMusicPath` paths. An example on a Linux system: @@ -122,12 +79,14 @@ For the purposes of properly uploading, downloading, updating, deleting, and str { "RootMusicPath": "/home/dev/null/music/", "TemporaryMusicPath": "/home/dev/null/music/temp/", - "ArchivePath": "/home/dev/null/music/archive/" + "ArchivePath": "/home/dev/null/music/archive/", + "CoverArtPath": "/home/dev/null/music/coverart/" } ``` * RootMusicPath - Where music will be stored in the following convention: *`Artist/Album/Songs`* * TemporaryMusicPath - Where music will be stored when uploding songs to the server until the metadata has been fully parsed and entered into the database. Upon completion the files will be deleted and moved to the appropriate path in the `RootMusicPath` * ArchivePath - When downloading compressed songs this is the path where songs will be compressed prior to dataa being read into memory, deleting the compressed file, and sending the compressed file from memory to the client +* CoverArtPath - Root directory where cover art will be saved to **Note**: The `TemporaryMusic` or `ArchivePath` does not have to be located in the `RootMusicPath`. Ensure that the permissions are properly set for all of the paths. @@ -135,15 +94,15 @@ For the purposes of properly uploading, downloading, updating, deleting, and str ### Database connection string In order for Database functionality to be operable, there must be a valid connection string and credentials with appropriate permissions. **At the moment there is only support for MySQL**. Depending on your environment `Release` or `Debug` you will need to edit the appsettings.json or appsettings.Development.json accordingly. An example of the fields to change are below: + ```Json { - "ConnectionStrings": { - "DefaultConnection": "Server=localhost;Database=my_db;Uid=admin;Pwd=toughpassword;" + "DefaultConnection": "Server=localhost;Database=my_db;Uid=admin;Pwd=toughpassword;" } - } ``` + * Server - The address or domain name of the MySQL server * Database - The database name * Uid - Username @@ -181,13 +140,9 @@ From this point the database has been successfully configured. Metadata and song Please read [CONTRIBUTING.md](CONTRIBUTING.md) for details on the code of conduct, and the process for submitting pull requests to the project. -## Versioning - -* [v0.1](https://github.com/amazing-username/Icarus/releases/tag/v0.1) - ## Authors -* **Kun Deng** - [amazing-username](https://github.com/amazing-username) +* [kdeng00](https://github.com/kdeng00) See also the list of [contributors](https://github.com/amazing-username/Icarus/graphs/contributors) who participated in this project. From 1bfd0a4931c2653d700e2954919644716c9aa146 Mon Sep 17 00:00:00 2001 From: Kun Deng Date: Tue, 30 Aug 2022 21:42:20 -0400 Subject: [PATCH 12/21] Update Icarus.csproj --- Icarus.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Icarus.csproj b/Icarus.csproj index 7c8b877..628bbcb 100644 --- a/Icarus.csproj +++ b/Icarus.csproj @@ -3,7 +3,7 @@ net6 InProcess - 0.1.9 + 0.1.10 From 77c6ee00eaf54c1646ff90e85dcb0121a7047d44 Mon Sep 17 00:00:00 2001 From: Kun Deng Date: Sun, 4 Sep 2022 18:39:48 -0400 Subject: [PATCH 13/21] JWT symmetric keys Updated config file to store neccessary information --- appsettings.Development.json | 6 ++++++ appsettings.json | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/appsettings.Development.json b/appsettings.Development.json index cd727fb..c8ed0de 100644 --- a/appsettings.Development.json +++ b/appsettings.Development.json @@ -16,6 +16,12 @@ "PrivateKeyPath": "", "PublicKeyPath": "" }, + "JWT": { + "Issuer": "", + "Audience": "", + "Secret": "", + "Subject": "" + }, "ConnectionStrings": { "DefaultConnection": "Server=;Database=;Uid=;Pwd=;" }, diff --git a/appsettings.json b/appsettings.json index 68eb482..71f2123 100644 --- a/appsettings.json +++ b/appsettings.json @@ -16,6 +16,12 @@ "PrivateKeyPath": "", "PublicKeyPath": "" }, + "JWT": { + "Issuer": "", + "Audience": "", + "Secret": "", + "Subject": "" + }, "ConnectionStrings": { "DefaultConnection": "Server=;Database=;Uid=;Pwd=;" }, From 00ef8d024298324cc277a43b4fab04719b0e5c84 Mon Sep 17 00:00:00 2001 From: Kun Deng Date: Sun, 4 Sep 2022 18:56:56 -0400 Subject: [PATCH 14/21] Added JWT bearer authentication --- Startup.cs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/Startup.cs b/Startup.cs index 6dea1e4..af349a3 100644 --- a/Startup.cs +++ b/Startup.cs @@ -1,5 +1,7 @@ using System; +using System.Text; +using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; @@ -72,6 +74,20 @@ namespace Icarus var connString = Configuration.GetConnectionString("DefaultConnection"); + services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(options => + { + options.RequireHttpsMetadata = false; + options.SaveToken = true; + options.TokenValidationParameters = new TokenValidationParameters() + { + ValidateIssuer = true, + ValidateAudience = true, + ValidAudience = Configuration["JWT:Audience"], + ValidIssuer = Configuration["JWT:Issuer"], + IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["JWT:Secret"])) + }; + }); + services.AddDbContext(options => options.UseMySQL(connString)); services.AddDbContext(options => options.UseMySQL(connString)); services.AddDbContext(options => options.UseMySQL(connString)); From d5944c470b9272c6b059b6ea9cf8dac174810dbf Mon Sep 17 00:00:00 2001 From: Kun Deng Date: Sun, 4 Sep 2022 20:34:33 -0400 Subject: [PATCH 15/21] Symmetric keys with jwt has been implemented --- Controllers/Managers/TokenManager.cs | 55 +++++++++++++++++++++++--- Controllers/v1/LoginController.cs | 49 ++++++++++++++--------- Controllers/v1/SongStreamController.cs | 3 ++ Startup.cs | 2 +- 4 files changed, 83 insertions(+), 26 deletions(-) diff --git a/Controllers/Managers/TokenManager.cs b/Controllers/Managers/TokenManager.cs index c00452f..08c0a15 100644 --- a/Controllers/Managers/TokenManager.cs +++ b/Controllers/Managers/TokenManager.cs @@ -4,10 +4,14 @@ using System.Linq; using System.Security.Claims; using System.Security.Cryptography; using System.Threading.Tasks; +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Text; using JWT; using JWT.Serializers; using Microsoft.Extensions.Configuration; +using Microsoft.IdentityModel.Tokens; using Newtonsoft.Json; using Org.BouncyCastle.Crypto; using Org.BouncyCastle.Crypto.Parameters; @@ -113,6 +117,40 @@ namespace Icarus.Controllers.Managers }; } + public LoginResult LoginSymmetric(User user) + { + var tokenResult = new TokenTierOne(); + tokenResult.TokenType = "Jwt"; + + var payload = Payload(); + var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_config["JWT:Secret"])); + var signIn = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); + var token = new JwtSecurityToken( + _config["JWT:Issuer"], + _config["JWT:Audience"], + payload, + expires: DateTime.UtcNow.AddMinutes(30), + signingCredentials: signIn); + + tokenResult.AccessToken = new JwtSecurityTokenHandler().WriteToken(token); + + var expClaim = payload.FirstOrDefault(cl => + { + return cl.Type.Equals("exp"); + }); + + var expiredDate = DateTime.Parse(expClaim.Value); + var exp = Math.Floor((expiredDate - DateTime.UnixEpoch).TotalSeconds); + tokenResult.Expiration = Convert.ToInt32(exp); + + return new LoginResult + { + UserID = user.UserID, Username = user.Username, Token = tokenResult.AccessToken, + TokenType = tokenResult.TokenType, Expiration = tokenResult.Expiration, + Message = "Successfully retrieved token" + }; + } + public bool IsTokenValid(string scope, string accessToken) { var result = false; @@ -201,23 +239,28 @@ namespace Icarus.Controllers.Managers private List Payload() { - const int expLimit = 24; + var expLimit = 30; var currentDate = DateTime.Now; - var expiredDate = currentDate.AddHours(expLimit); + var expiredDate = currentDate.AddMinutes(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 subject = _config["JWT:Subject"]; 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") + // new System.Security.Claims.Claim("exp", $"{expires}", "integer"), + new System.Security.Claims.Claim(JwtRegisteredClaimNames.Exp, expiredDate.ToString()), + // new System.Security.Claims.Claim("aud", $"{audience}", "string"), + new System.Security.Claims.Claim(JwtRegisteredClaimNames.Aud, audience), + // new System.Security.Claims.Claim("iss", $"{issuer}", "string"), + new System.Security.Claims.Claim(JwtRegisteredClaimNames.Iss, issuer), + new Claim(JwtRegisteredClaimNames.Sub, subject), + new System.Security.Claims.Claim(JwtRegisteredClaimNames.Iat, currentDate.ToString()) }; return claim; diff --git a/Controllers/v1/LoginController.cs b/Controllers/v1/LoginController.cs index 78ee567..881e3d9 100644 --- a/Controllers/v1/LoginController.cs +++ b/Controllers/v1/LoginController.cs @@ -55,34 +55,45 @@ namespace Icarus.Controllers.V1 Username = user.Username }; - if (context.Users.FirstOrDefault(usr => usr.Username.Equals(user.Username)) != null) + try { - user = context.Users.FirstOrDefault(usr => usr.Username.Equals(user.Username)); - - var validatePass = new PasswordEncryption(); - var validated = validatePass.VerifyPassword(user, password); - if (!validated) + if (context.Users.FirstOrDefault(usr => usr.Username.Equals(user.Username)) != null) { - loginRes.Message = message; - _logger.LogInformation(message); + user = context.Users.FirstOrDefault(usr => usr.Username.Equals(user.Username)); + + var validatePass = new PasswordEncryption(); + var validated = validatePass.VerifyPassword(user, password); + if (!validated) + { + loginRes.Message = message; + _logger.LogInformation(message); + + return Ok(loginRes); + } + + _logger.LogInformation("Successfully validated user credentials"); + + TokenManager tk = new TokenManager(_config); + + // loginRes = tk.LogIn(user); + loginRes = tk.LoginSymmetric(user); return Ok(loginRes); } + else + { + loginRes.Message = message; - _logger.LogInformation("Successfully validated user credentials"); - - TokenManager tk = new TokenManager(_config); - - loginRes = tk.LogIn(user); - - return Ok(loginRes); + return NotFound(loginRes); + } } - else + catch (Exception ex) { - loginRes.Message = message; - - return NotFound(loginRes); + _logger.LogError("An error occurred: {0}", ex.Message); + _logger.LogError("Inner Exception: {0}", ex.InnerException.Message); } + + return NotFound(loginRes); } #endregion } diff --git a/Controllers/v1/SongStreamController.cs b/Controllers/v1/SongStreamController.cs index b273a08..4479800 100644 --- a/Controllers/v1/SongStreamController.cs +++ b/Controllers/v1/SongStreamController.cs @@ -20,6 +20,7 @@ namespace Icarus.Controllers.V1 { [Route("api/v1/song/stream")] [ApiController] + [Authorize] public class SongStreamController : BaseController { #region Fields @@ -46,10 +47,12 @@ namespace Icarus.Controllers.V1 [HttpGet("{id}")] public async Task Get(int id) { + /** if (!IsTokenValid("stream:songs")) { return StatusCode(401, "Not allowed"); } + */ var context = new SongContext(_config.GetConnectionString("DefaultConnection")); diff --git a/Startup.cs b/Startup.cs index af349a3..7462694 100644 --- a/Startup.cs +++ b/Startup.cs @@ -95,7 +95,7 @@ namespace Icarus services.AddDbContext(options => options.UseMySQL(connString)); services.AddDbContext(options => options.UseMySQL(connString)); - services.AddAsymmetricAuthentication(Configuration); + // services.AddAsymmetricAuthentication(Configuration); /** services.AddTransient(au => new AuthenticationService(new UserService(Configuration), new TokenService(Configuration), Configuration)); From 05b5de0939b6f06a68843f5bf1d1ceaafbcff7dc Mon Sep 17 00:00:00 2001 From: Kun Deng Date: Sun, 4 Sep 2022 20:45:17 -0400 Subject: [PATCH 16/21] Cleanup --- Controllers/Managers/TokenManager.cs | 3 -- Controllers/v1/AlbumController.cs | 11 +----- Controllers/v1/ArtistController.cs | 6 +-- Controllers/v1/CoverArtController.cs | 11 +----- Controllers/v1/GenreController.cs | 11 +----- Controllers/v1/LoginController.cs | 4 -- .../v1/SongCompressedDataControllers.cs | 6 +-- Controllers/v1/SongController.cs | 16 +------- Controllers/v1/SongDataController.cs | 21 +--------- Controllers/v1/SongStreamController.cs | 7 ---- Startup.cs | 39 ------------------- 11 files changed, 7 insertions(+), 128 deletions(-) diff --git a/Controllers/Managers/TokenManager.cs b/Controllers/Managers/TokenManager.cs index 08c0a15..4f819de 100644 --- a/Controllers/Managers/TokenManager.cs +++ b/Controllers/Managers/TokenManager.cs @@ -253,11 +253,8 @@ namespace Icarus.Controllers.Managers 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(JwtRegisteredClaimNames.Exp, expiredDate.ToString()), - // new System.Security.Claims.Claim("aud", $"{audience}", "string"), new System.Security.Claims.Claim(JwtRegisteredClaimNames.Aud, audience), - // new System.Security.Claims.Claim("iss", $"{issuer}", "string"), new System.Security.Claims.Claim(JwtRegisteredClaimNames.Iss, issuer), new Claim(JwtRegisteredClaimNames.Sub, subject), new System.Security.Claims.Claim(JwtRegisteredClaimNames.Iat, currentDate.ToString()) diff --git a/Controllers/v1/AlbumController.cs b/Controllers/v1/AlbumController.cs index 903494a..8a3a259 100644 --- a/Controllers/v1/AlbumController.cs +++ b/Controllers/v1/AlbumController.cs @@ -14,6 +14,7 @@ namespace Icarus.Controllers.V1 { [Route("api/v1/album")] [ApiController] + [Authorize] public class AlbumController : BaseController { #region Fields @@ -40,11 +41,6 @@ namespace Icarus.Controllers.V1 [HttpGet] public IActionResult Get() { - if (!IsTokenValid("read:albums")) - { - return StatusCode(401, "Not allowed"); - } - List albums = new List(); var albumContext = new AlbumContext(_connectionString); @@ -60,11 +56,6 @@ namespace Icarus.Controllers.V1 [HttpGet("{id}")] public IActionResult Get(int id) { - if (!IsTokenValid("read:albums")) - { - return StatusCode(401, "Not allowed"); - } - Album album = new Album { AlbumID = id diff --git a/Controllers/v1/ArtistController.cs b/Controllers/v1/ArtistController.cs index 51e21b2..a27f48a 100644 --- a/Controllers/v1/ArtistController.cs +++ b/Controllers/v1/ArtistController.cs @@ -13,6 +13,7 @@ namespace Icarus.Controllers.V1 { [Route("api/v1/artist")] [ApiController] + [Authorize] public class ArtistController : BaseController { #region Fields @@ -39,11 +40,6 @@ namespace Icarus.Controllers.V1 [HttpGet] public IActionResult Get() { - if (!IsTokenValid("read:artists")) - { - return StatusCode(401, "Not allowed"); - } - var artistContext = new ArtistContext(_connectionString); var artists = artistContext.Artists.ToList(); diff --git a/Controllers/v1/CoverArtController.cs b/Controllers/v1/CoverArtController.cs index 482b6a4..2dd422a 100644 --- a/Controllers/v1/CoverArtController.cs +++ b/Controllers/v1/CoverArtController.cs @@ -15,6 +15,7 @@ namespace Icarus.Controllers.V1 { [Route("api/v1/coverart")] [ApiController] + [Authorize] public class CoverArtController : BaseController { #region Fields @@ -36,11 +37,6 @@ namespace Icarus.Controllers.V1 #region HTTP Routes public IActionResult Get() { - if (!IsTokenValid("read:songs")) - { - return StatusCode(401, "Not allowed"); - } - var coverArtContext = new CoverArtContext(_connectionString); var coverArtRecords = coverArtContext.CoverArtImages.ToList(); @@ -60,11 +56,6 @@ namespace Icarus.Controllers.V1 [HttpGet("{id}")] public async Task Get(int id) { - if (!IsTokenValid("download:cover_art")) - { - return StatusCode(401, "Not allowed"); - } - var coverArt = new CoverArt { CoverArtID = id }; var coverArtContext = new CoverArtContext(_connectionString); diff --git a/Controllers/v1/GenreController.cs b/Controllers/v1/GenreController.cs index 8e29502..ae553e6 100644 --- a/Controllers/v1/GenreController.cs +++ b/Controllers/v1/GenreController.cs @@ -14,6 +14,7 @@ namespace Icarus.Controllers.V1 { [Route("api/v1/genre")] [ApiController] + [Authorize] public class GenreController : BaseController { #region Fields @@ -40,11 +41,6 @@ namespace Icarus.Controllers.V1 [HttpGet] public IActionResult Get() { - if (!IsTokenValid("read:genre")) - { - return StatusCode(401, "Not allowed"); - } - var genres = new List(); var genreStore = new GenreContext(_connectionString); @@ -60,11 +56,6 @@ namespace Icarus.Controllers.V1 [HttpGet("{id}")] public IActionResult Get(int id) { - if (!IsTokenValid("read:genre")) - { - return StatusCode(401, "Not allowed"); - } - var genre = new Genre { GenreID = id diff --git a/Controllers/v1/LoginController.cs b/Controllers/v1/LoginController.cs index 881e3d9..0458b4c 100644 --- a/Controllers/v1/LoginController.cs +++ b/Controllers/v1/LoginController.cs @@ -1,8 +1,5 @@ using System; -using System.Collections.Generic; -using System.Configuration; using System.Linq; -using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Configuration; @@ -75,7 +72,6 @@ namespace Icarus.Controllers.V1 TokenManager tk = new TokenManager(_config); - // loginRes = tk.LogIn(user); loginRes = tk.LoginSymmetric(user); return Ok(loginRes); diff --git a/Controllers/v1/SongCompressedDataControllers.cs b/Controllers/v1/SongCompressedDataControllers.cs index cc96800..6045caf 100644 --- a/Controllers/v1/SongCompressedDataControllers.cs +++ b/Controllers/v1/SongCompressedDataControllers.cs @@ -19,6 +19,7 @@ namespace Icarus.Controllers.V1 { [Route("api/v1/song/compressed/data")] [ApiController] + [Authorize] public class SongCompressedDataController : BaseController { #region Fields @@ -47,11 +48,6 @@ namespace Icarus.Controllers.V1 [HttpGet("{id}")] public async Task Get(int id) { - if (!IsTokenValid("download:songs")) - { - return StatusCode(401, "Not allowed"); - } - var context = new SongContext(_connectionString); SongCompression cmp = new SongCompression(_archiveDir); diff --git a/Controllers/v1/SongController.cs b/Controllers/v1/SongController.cs index d94f78f..05cc9f2 100644 --- a/Controllers/v1/SongController.cs +++ b/Controllers/v1/SongController.cs @@ -18,6 +18,7 @@ namespace Icarus.Controllers.V1 { [Route("api/v1/song")] [ApiController] + [Authorize] public class SongController : BaseController { #region Fields @@ -49,11 +50,6 @@ namespace Icarus.Controllers.V1 [HttpGet] public IActionResult Get() { - if (!IsTokenValid("read:song_details")) - { - return StatusCode(401, "Not allowed"); - } - List songs = new List(); Console.WriteLine("Attemtping to retrieve songs"); _logger.LogInformation("Attempting to retrieve songs"); @@ -71,11 +67,6 @@ namespace Icarus.Controllers.V1 [HttpGet("{id}")] public IActionResult Get(int id) { - if (!IsTokenValid("read:song_details")) - { - return StatusCode(401, "Not allowed"); - } - var context = new SongContext(_connectionString); Song song = new Song { SongID = id }; @@ -92,11 +83,6 @@ namespace Icarus.Controllers.V1 [HttpPut("{id}")] public IActionResult Put(int id, [FromBody] Song song) { - if (!IsTokenValid("update:songs")) - { - return StatusCode(401, "Not allowed"); - } - var context = new SongContext(_connectionString); song.SongID = id; diff --git a/Controllers/v1/SongDataController.cs b/Controllers/v1/SongDataController.cs index 4760939..ab64a50 100644 --- a/Controllers/v1/SongDataController.cs +++ b/Controllers/v1/SongDataController.cs @@ -20,6 +20,7 @@ namespace Icarus.Controllers.V1 { [Route("api/v1/song/data")] [ApiController] + [Authorize] public class SongDataController : BaseController { #region Fields @@ -50,11 +51,6 @@ namespace Icarus.Controllers.V1 [Route("private-scoped")] public async Task Get(int id) { - if (!IsTokenValid("download:songs")) - { - return StatusCode(401, "Not allowed"); - } - var songContext = new SongContext(_connectionString); var songMetaData = songContext.RetrieveRecord(new Song { SongID = id}); @@ -79,11 +75,6 @@ namespace Icarus.Controllers.V1 [Route("private-scoped")] public IActionResult Post([FromForm(Name = "file")] List songData) { - if (!IsTokenValid("upload:songs")) - { - return StatusCode(401, "Not allowed"); - } - try { // Console.WriteLine("Uploading song..."); @@ -121,11 +112,6 @@ namespace Icarus.Controllers.V1 [Route("private-scoped")] public IActionResult Post ([FromForm] UploadSongWithDataForm up) { - if (!IsTokenValid("upload:songs")) - { - return StatusCode(401, "Not allowed"); - } - try { if (up.SongData.Length > 0 && up.CoverArtData.Length > 0 && !string.IsNullOrEmpty(up.SongFile)) @@ -147,11 +133,6 @@ namespace Icarus.Controllers.V1 [HttpDelete("delete/{id}")] public IActionResult Delete(int id) { - if (!IsTokenValid("delete:songs")) - { - return StatusCode(401, "Not allowed"); - } - var songContext = new SongContext(_connectionString); var songMetaData = new Song{ SongID = id }; diff --git a/Controllers/v1/SongStreamController.cs b/Controllers/v1/SongStreamController.cs index 4479800..967a74b 100644 --- a/Controllers/v1/SongStreamController.cs +++ b/Controllers/v1/SongStreamController.cs @@ -47,13 +47,6 @@ namespace Icarus.Controllers.V1 [HttpGet("{id}")] public async Task Get(int id) { - /** - if (!IsTokenValid("stream:songs")) - { - return StatusCode(401, "Not allowed"); - } - */ - var context = new SongContext(_config.GetConnectionString("DefaultConnection")); var song = context.Songs.FirstOrDefault(sng => sng.SongID == id); diff --git a/Startup.cs b/Startup.cs index 7462694..88603bd 100644 --- a/Startup.cs +++ b/Startup.cs @@ -19,35 +19,6 @@ using Icarus.Database.Contexts; namespace Icarus { - public static class ServiceStartup - { - public static IServiceCollection AddAsymmetricAuthentication(this IServiceCollection services, IConfiguration configuration) - { - var issuerSigningCertificate = new Icarus.Certs.SigningIssuerCertificate(); - RsaSecurityKey issuerSigningKey = issuerSigningCertificate.GetIssuerSigningKey(configuration["RSAKeys:PublicKeyPath"]); - - services.AddAuthentication(authOptions => - { - }) - .AddJwtBearer(options => - { - options.TokenValidationParameters = new TokenValidationParameters - { - IssuerSigningKey = issuerSigningKey, - }; - }); - - return services; - } - - private static bool LifetimeValidator(DateTime? notBefore, - DateTime? expires, - SecurityToken securityToken, - TokenValidationParameters validationParameters) - { - return expires != null && expires > DateTime.UtcNow; - } - } public class Startup { #region Constructors @@ -95,16 +66,6 @@ namespace Icarus services.AddDbContext(options => options.UseMySQL(connString)); services.AddDbContext(options => options.UseMySQL(connString)); - // services.AddAsymmetricAuthentication(Configuration); - - /** - services.AddTransient(au => new AuthenticationService(new UserService(Configuration), new TokenService(Configuration), Configuration)); - services.AddTransient(us => new UserService(Configuration)); - services.AddTransient(tk => new TokenService(Configuration)); - services.AddTransient(); - services.AddTransient(uc => new UserContext(connString)); - */ - services.AddControllers() .AddNewtonsoftJson(); } From 11fe1f29e1fbd9205a70aa0e3a6fd28567acf426 Mon Sep 17 00:00:00 2001 From: Kun Deng Date: Sun, 4 Sep 2022 20:50:35 -0400 Subject: [PATCH 17/21] Updated Readme --- README.md | 30 ++++++++++++++---------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index e24d59e..4062380 100644 --- a/README.md +++ b/README.md @@ -37,32 +37,27 @@ One can interface with Icarus the music server either by: ## Getting started There are several things that need to be completed to properly setup and secure the API. -1. Creating RSA keys +#### 1. Creating RSA keys +1. JWT Information 2. API filesystem paths 3. Database connection string 4. Migrations -### Creating RSA keys +### JWT Information -1. Create private key -``` -openssl genrsa -out private.pem 2048 -``` -2. Create public key -``` -openssl rsa -in private -pubout -out public.pem -``` - -Configure the key paths in the config files +Configure JWT information. Notably the Secret ```Json -"RSAKeys": { - "PrivateKeyPath": "", - "PublicKeyPath": "" -} + "JWT": { + "Issuer": "IcarusAPI", + "Audience": "IcarusAPIClient", + "Secret": "X1I9TbwaBG3RiwLiCJ69lHGxLFoNODDE", + "Subject": "Authorization" + }, ``` + Replace [domain] with the domain name that represent's your domain. Replace [identifier] with the identifer root name in the appsettings environment file. Not the friendly name but the root name of the identifier, omitting the http protocol and the *api* path. ```Json @@ -72,6 +67,9 @@ Replace [domain] with the domain name that represent's your domain. Replace [ide }, ``` +**Note**: The Auth0 section is likely to be changed or removed in future releases. + + ### API filesystem paths For the purposes of properly uploading, downloading, updating, deleting, and streaming songs the API filesystem paths must be configured. What is meant by this is that the `RootMusicPath` directory where all music will be stored must exist as well as the `ArchivePath` and `TemporaryMusicPath` paths. An example on a Linux system: From 664b0d520b91314a81b5bf7de380e625919e8ec1 Mon Sep 17 00:00:00 2001 From: Kun Deng Date: Sun, 4 Sep 2022 20:56:37 -0400 Subject: [PATCH 18/21] Updated Readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4062380..d57c2e5 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ Configure JWT information. Notably the Secret "JWT": { "Issuer": "IcarusAPI", "Audience": "IcarusAPIClient", - "Secret": "X1I9TbwaBG3RiwLiCJ69lHGxLFoNODDE", + "Secret": "Manaiswhatyouthinkitis", "Subject": "Authorization" }, ``` From 38c056cc9961f5f9a02e05fb870bb984e75cb545 Mon Sep 17 00:00:00 2001 From: Kun Deng Date: Mon, 5 Sep 2022 16:58:10 -0400 Subject: [PATCH 19/21] Swagger docs --- Controllers/v1/AlbumController.cs | 4 +-- Controllers/v1/ArtistController.cs | 4 +-- Controllers/v1/BaseController.cs | 2 ++ Controllers/v1/CoverArtController.cs | 7 +++-- Controllers/v1/GenreController.cs | 4 +-- Controllers/v1/LoginController.cs | 3 +- Controllers/v1/RegisterController.cs | 2 +- Controllers/v1/SongController.cs | 6 ++-- Controllers/v1/SongDataController.cs | 38 +++++++++++++------------ Controllers/v1/SongStreamController.cs | 3 +- Icarus.csproj | 1 + Properties/launchSettings.json | 5 ++-- Startup.cs | 39 ++++++++++++++++++++++++++ 13 files changed, 83 insertions(+), 35 deletions(-) diff --git a/Controllers/v1/AlbumController.cs b/Controllers/v1/AlbumController.cs index 8a3a259..7980f71 100644 --- a/Controllers/v1/AlbumController.cs +++ b/Controllers/v1/AlbumController.cs @@ -39,7 +39,7 @@ namespace Icarus.Controllers.V1 #region HTTP Routes [HttpGet] - public IActionResult Get() + public IActionResult GetAlbums() { List albums = new List(); @@ -54,7 +54,7 @@ namespace Icarus.Controllers.V1 } [HttpGet("{id}")] - public IActionResult Get(int id) + public IActionResult GetAlbum(int id) { Album album = new Album { diff --git a/Controllers/v1/ArtistController.cs b/Controllers/v1/ArtistController.cs index a27f48a..219e6c5 100644 --- a/Controllers/v1/ArtistController.cs +++ b/Controllers/v1/ArtistController.cs @@ -38,7 +38,7 @@ namespace Icarus.Controllers.V1 #region HTTP Routes [HttpGet] - public IActionResult Get() + public IActionResult GetArtists() { var artistContext = new ArtistContext(_connectionString); @@ -51,7 +51,7 @@ namespace Icarus.Controllers.V1 } [HttpGet("{id}")] - public IActionResult Get(int id) + public IActionResult GetArtist(int id) { if (!IsTokenValid("read:artists")) { diff --git a/Controllers/v1/BaseController.cs b/Controllers/v1/BaseController.cs index 911d273..9cec68a 100644 --- a/Controllers/v1/BaseController.cs +++ b/Controllers/v1/BaseController.cs @@ -17,6 +17,7 @@ namespace Icarus.Controllers.V1 #region Methods + [ApiExplorerSettings(IgnoreApi = true)] protected string ParseBearerTokenFromHeader() { var token = string.Empty; @@ -37,6 +38,7 @@ namespace Icarus.Controllers.V1 return token; } + [ApiExplorerSettings(IgnoreApi = true)] protected bool IsTokenValid(string scope) { var token = ParseBearerTokenFromHeader(); diff --git a/Controllers/v1/CoverArtController.cs b/Controllers/v1/CoverArtController.cs index 2dd422a..4d3ef29 100644 --- a/Controllers/v1/CoverArtController.cs +++ b/Controllers/v1/CoverArtController.cs @@ -35,7 +35,8 @@ namespace Icarus.Controllers.V1 #region HTTP Routes - public IActionResult Get() + [HttpGet] + public IActionResult GetCoverArts() { var coverArtContext = new CoverArtContext(_connectionString); @@ -54,7 +55,7 @@ namespace Icarus.Controllers.V1 } [HttpGet("{id}")] - public async Task Get(int id) + public IActionResult GetCoverArt(int id) { var coverArt = new CoverArt { CoverArtID = id }; @@ -65,7 +66,7 @@ namespace Icarus.Controllers.V1 if (coverArt != null) { _logger.LogInformation("Found cover art record"); - var coverArtBytes = await System.IO.File.ReadAllBytesAsync( + var coverArtBytes = System.IO.File.ReadAllBytes( coverArt.ImagePath); return File(coverArtBytes, "application/x-msdownload", diff --git a/Controllers/v1/GenreController.cs b/Controllers/v1/GenreController.cs index ae553e6..8fbd4ff 100644 --- a/Controllers/v1/GenreController.cs +++ b/Controllers/v1/GenreController.cs @@ -39,7 +39,7 @@ namespace Icarus.Controllers.V1 #region HTTP Routes [HttpGet] - public IActionResult Get() + public IActionResult RetrieveGenres() { var genres = new List(); @@ -54,7 +54,7 @@ namespace Icarus.Controllers.V1 } [HttpGet("{id}")] - public IActionResult Get(int id) + public IActionResult RetrieveGenre(int id) { var genre = new Genre { diff --git a/Controllers/v1/LoginController.cs b/Controllers/v1/LoginController.cs index 0458b4c..8446063 100644 --- a/Controllers/v1/LoginController.cs +++ b/Controllers/v1/LoginController.cs @@ -38,7 +38,8 @@ namespace Icarus.Controllers.V1 #region HTTP endpoints - public IActionResult Post([FromBody] User user) + [HttpPost] + public IActionResult Login([FromBody] User user) { var context = new UserContext(_connectionString); diff --git a/Controllers/v1/RegisterController.cs b/Controllers/v1/RegisterController.cs index e1352d8..202b728 100644 --- a/Controllers/v1/RegisterController.cs +++ b/Controllers/v1/RegisterController.cs @@ -37,7 +37,7 @@ namespace Icarus.Controllers.V1 #endregion [HttpPost] - public IActionResult Post([FromBody] User user) + public IActionResult RegisterUser([FromBody] User user) { PasswordEncryption pe = new PasswordEncryption(); user.Password = pe.HashPassword(user); diff --git a/Controllers/v1/SongController.cs b/Controllers/v1/SongController.cs index 05cc9f2..39be0cf 100644 --- a/Controllers/v1/SongController.cs +++ b/Controllers/v1/SongController.cs @@ -48,7 +48,7 @@ namespace Icarus.Controllers.V1 [HttpGet] - public IActionResult Get() + public IActionResult RetrieveSongs() { List songs = new List(); Console.WriteLine("Attemtping to retrieve songs"); @@ -65,7 +65,7 @@ namespace Icarus.Controllers.V1 } [HttpGet("{id}")] - public IActionResult Get(int id) + public IActionResult RetrieveSong(int id) { var context = new SongContext(_connectionString); @@ -81,7 +81,7 @@ namespace Icarus.Controllers.V1 } [HttpPut("{id}")] - public IActionResult Put(int id, [FromBody] Song song) + public IActionResult UpdateSong(int id, [FromBody] Song song) { var context = new SongContext(_connectionString); diff --git a/Controllers/v1/SongDataController.cs b/Controllers/v1/SongDataController.cs index ab64a50..f459ef0 100644 --- a/Controllers/v1/SongDataController.cs +++ b/Controllers/v1/SongDataController.cs @@ -48,13 +48,13 @@ namespace Icarus.Controllers.V1 [HttpGet("download/{id}")] - [Route("private-scoped")] - public async Task Get(int id) + // [Route("private-scoped")] + public IActionResult Download(int id) { var songContext = new SongContext(_connectionString); var songMetaData = songContext.RetrieveRecord(new Song { SongID = id}); - var song = await _songMgr.RetrieveSong(songMetaData); + var song = _songMgr.RetrieveSong(songMetaData).Result; return File(song.Data, "application/x-msdownload", songMetaData.Filename); } @@ -72,8 +72,9 @@ namespace Icarus.Controllers.V1 // Cover art // [HttpPost("upload"), DisableRequestSizeLimit] - [Route("private-scoped")] - public IActionResult Post([FromForm(Name = "file")] List songData) + // [ConflictingActionsResolver] + // [Route("private-scoped")] + public IActionResult Upload([FromForm(Name = "file")] List songData) { try { @@ -109,8 +110,9 @@ namespace Icarus.Controllers.V1 // as well as the cover art // [HttpPost("upload/with/data")] - [Route("private-scoped")] - public IActionResult Post ([FromForm] UploadSongWithDataForm up) + // [ConflictingActionsResolver] + // [Route("private-scoped")] + public IActionResult UploadWithData([FromForm] UploadSongWithDataForm up) { try { @@ -131,7 +133,7 @@ namespace Icarus.Controllers.V1 } [HttpDelete("delete/{id}")] - public IActionResult Delete(int id) + public IActionResult DeleteSong(int id) { var songContext = new SongContext(_connectionString); @@ -156,15 +158,15 @@ namespace Icarus.Controllers.V1 } - public class UploadSongWithDataForm - { - [FromForm(Name = "file")] - public IFormFile SongData { get; set; } - // TODO: Think about making this optional and if it is not provided, use the stock cover art - [FromForm(Name = "cover")] - public IFormFile CoverArtData { get; set; } - [FromForm(Name = "metadata")] - public string SongFile { get; set; } - } + public class UploadSongWithDataForm + { + [FromForm(Name = "file")] + public IFormFile SongData { get; set; } + // TODO: Think about making this optional and if it is not provided, use the stock cover art + [FromForm(Name = "cover")] + public IFormFile CoverArtData { get; set; } + [FromForm(Name = "metadata")] + public string SongFile { get; set; } + } } } diff --git a/Controllers/v1/SongStreamController.cs b/Controllers/v1/SongStreamController.cs index 967a74b..ad8ef4d 100644 --- a/Controllers/v1/SongStreamController.cs +++ b/Controllers/v1/SongStreamController.cs @@ -44,8 +44,9 @@ namespace Icarus.Controllers.V1 #region HTTP endpoints + // [HttpGet] [HttpGet("{id}")] - public async Task Get(int id) + public async Task StreamSong(int id) { var context = new SongContext(_config.GetConnectionString("DefaultConnection")); diff --git a/Icarus.csproj b/Icarus.csproj index 628bbcb..7e626cd 100644 --- a/Icarus.csproj +++ b/Icarus.csproj @@ -25,6 +25,7 @@ + diff --git a/Properties/launchSettings.json b/Properties/launchSettings.json index 82a1227..b71d9cb 100644 --- a/Properties/launchSettings.json +++ b/Properties/launchSettings.json @@ -12,7 +12,7 @@ "IIS Express": { "commandName": "IISExpress", "launchBrowser": true, - "launchUrl": "api/values", + "launchUrl": "swagger", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } @@ -20,7 +20,8 @@ "Icarus": { "commandName": "Project", "launchBrowser": true, - "launchUrl": "api/values", + "dotnetRuneMessage": true, + "launchUrl": "swagger", "applicationUrl": "http://localhost:5002", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" diff --git a/Startup.cs b/Startup.cs index 88603bd..89c50d1 100644 --- a/Startup.cs +++ b/Startup.cs @@ -1,15 +1,18 @@ using System; using System.Text; +using System.Linq; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Microsoft.EntityFrameworkCore; using Microsoft.IdentityModel.Tokens; +using Microsoft.OpenApi.Models; using Newtonsoft.Json; using NLog; using NLog.Web; @@ -68,12 +71,48 @@ namespace Icarus services.AddControllers() .AddNewtonsoftJson(); + + services.AddEndpointsApiExplorer(); + services.AddSwaggerGen(c => + { + c.ResolveConflictingActions(apiDescriptions => apiDescriptions.First()); + + c.SwaggerDoc("v1", new OpenApiInfo { Title = "Icarus", Version = "v1" }); + c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme() + { + Name = "Authorization", + Scheme = "Bearer", + BearerFormat = "JWT", + Type = SecuritySchemeType.ApiKey, + In = ParameterLocation.Header, + Description = "Bearer *Auth Token*", + }); + c.AddSecurityRequirement(new OpenApiSecurityRequirement + { + { + new OpenApiSecurityScheme + { + Reference = new OpenApiReference + { + Type = ReferenceType.SecurityScheme, + Id = "Bearer" + } + }, + new string[] {} + } + }); + }); } // Called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { // NOTE: Dev-related configuration can be done when env.IsDevelopment() evaluated to true + if (env.IsDevelopment()) + { + app.UseSwagger(); + app.UseSwaggerUI(); + } app.UseRouting(); app.UseAuthentication(); From 73bfff39400b56dba5ad9f49e59e1116053e273c Mon Sep 17 00:00:00 2001 From: Kun Deng Date: Mon, 5 Sep 2022 17:02:25 -0400 Subject: [PATCH 20/21] Cleanup --- Controllers/v1/GenreController.cs | 4 ++-- Controllers/v1/SongCompressedDataControllers.cs | 2 +- Controllers/v1/SongController.cs | 4 ++-- Controllers/v1/SongDataController.cs | 7 +------ Controllers/v1/SongStreamController.cs | 1 - 5 files changed, 6 insertions(+), 12 deletions(-) diff --git a/Controllers/v1/GenreController.cs b/Controllers/v1/GenreController.cs index 8fbd4ff..d5a86c1 100644 --- a/Controllers/v1/GenreController.cs +++ b/Controllers/v1/GenreController.cs @@ -39,7 +39,7 @@ namespace Icarus.Controllers.V1 #region HTTP Routes [HttpGet] - public IActionResult RetrieveGenres() + public IActionResult GetGenres() { var genres = new List(); @@ -54,7 +54,7 @@ namespace Icarus.Controllers.V1 } [HttpGet("{id}")] - public IActionResult RetrieveGenre(int id) + public IActionResult GetGenre(int id) { var genre = new Genre { diff --git a/Controllers/v1/SongCompressedDataControllers.cs b/Controllers/v1/SongCompressedDataControllers.cs index 6045caf..789a5d4 100644 --- a/Controllers/v1/SongCompressedDataControllers.cs +++ b/Controllers/v1/SongCompressedDataControllers.cs @@ -46,7 +46,7 @@ namespace Icarus.Controllers.V1 #region API Routes [HttpGet("{id}")] - public async Task Get(int id) + public async Task DownloadCompressedSong(int id) { var context = new SongContext(_connectionString); diff --git a/Controllers/v1/SongController.cs b/Controllers/v1/SongController.cs index 39be0cf..c7ebe3d 100644 --- a/Controllers/v1/SongController.cs +++ b/Controllers/v1/SongController.cs @@ -48,7 +48,7 @@ namespace Icarus.Controllers.V1 [HttpGet] - public IActionResult RetrieveSongs() + public IActionResult GetSongs() { List songs = new List(); Console.WriteLine("Attemtping to retrieve songs"); @@ -65,7 +65,7 @@ namespace Icarus.Controllers.V1 } [HttpGet("{id}")] - public IActionResult RetrieveSong(int id) + public IActionResult GetSong(int id) { var context = new SongContext(_connectionString); diff --git a/Controllers/v1/SongDataController.cs b/Controllers/v1/SongDataController.cs index f459ef0..48341b6 100644 --- a/Controllers/v1/SongDataController.cs +++ b/Controllers/v1/SongDataController.cs @@ -48,7 +48,6 @@ namespace Icarus.Controllers.V1 [HttpGet("download/{id}")] - // [Route("private-scoped")] public IActionResult Download(int id) { var songContext = new SongContext(_connectionString); @@ -72,8 +71,6 @@ namespace Icarus.Controllers.V1 // Cover art // [HttpPost("upload"), DisableRequestSizeLimit] - // [ConflictingActionsResolver] - // [Route("private-scoped")] public IActionResult Upload([FromForm(Name = "file")] List songData) { try @@ -110,8 +107,6 @@ namespace Icarus.Controllers.V1 // as well as the cover art // [HttpPost("upload/with/data")] - // [ConflictingActionsResolver] - // [Route("private-scoped")] public IActionResult UploadWithData([FromForm] UploadSongWithDataForm up) { try @@ -162,7 +157,7 @@ namespace Icarus.Controllers.V1 { [FromForm(Name = "file")] public IFormFile SongData { get; set; } - // TODO: Think about making this optional and if it is not provided, use the stock cover art + // NOTE: Think about making this optional and if it is not provided, use the stock cover art [FromForm(Name = "cover")] public IFormFile CoverArtData { get; set; } [FromForm(Name = "metadata")] diff --git a/Controllers/v1/SongStreamController.cs b/Controllers/v1/SongStreamController.cs index ad8ef4d..de92076 100644 --- a/Controllers/v1/SongStreamController.cs +++ b/Controllers/v1/SongStreamController.cs @@ -44,7 +44,6 @@ namespace Icarus.Controllers.V1 #region HTTP endpoints - // [HttpGet] [HttpGet("{id}")] public async Task StreamSong(int id) { From c191d7ee7a88fd066b16dd210d7aec4d8b7675d5 Mon Sep 17 00:00:00 2001 From: Kun Deng Date: Mon, 5 Sep 2022 17:07:26 -0400 Subject: [PATCH 21/21] Update README.md Note about swagger --- README.md | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index d57c2e5..c3bb084 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,9 @@ One can interface with Icarus the music server either by: ## Getting started There are several things that need to be completed to properly setup and secure the API. -#### 1. Creating RSA keys +This API uses OpenAPI Specification 3.0. After configuring the API, launch the software +and navigate your browser to https://localhost:5001/swagger to view the endpoints. + 1. JWT Information 2. API filesystem paths 3. Database connection string @@ -138,11 +140,6 @@ From this point the database has been successfully configured. Metadata and song Please read [CONTRIBUTING.md](CONTRIBUTING.md) for details on the code of conduct, and the process for submitting pull requests to the project. -## Authors - -* [kdeng00](https://github.com/kdeng00) - -See also the list of [contributors](https://github.com/amazing-username/Icarus/graphs/contributors) who participated in this project. ## License