Merge pull request #80 from kdeng00/v0.1.20

V0.1.20
This commit was merged in pull request #80.
This commit is contained in:
Kun Deng
2022-09-05 17:12:57 -04:00
committed by GitHub
18 changed files with 205 additions and 204 deletions
+46 -6
View File
@@ -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,25 @@ namespace Icarus.Controllers.Managers
private List<Claim> 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<System.Security.Claims.Claim>()
{
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(JwtRegisteredClaimNames.Exp, expiredDate.ToString()),
new System.Security.Claims.Claim(JwtRegisteredClaimNames.Aud, audience),
new System.Security.Claims.Claim(JwtRegisteredClaimNames.Iss, issuer),
new Claim(JwtRegisteredClaimNames.Sub, subject),
new System.Security.Claims.Claim(JwtRegisteredClaimNames.Iat, currentDate.ToString())
};
return claim;
+3 -12
View File
@@ -14,6 +14,7 @@ namespace Icarus.Controllers.V1
{
[Route("api/v1/album")]
[ApiController]
[Authorize]
public class AlbumController : BaseController
{
#region Fields
@@ -38,13 +39,8 @@ namespace Icarus.Controllers.V1
#region HTTP Routes
[HttpGet]
public IActionResult Get()
public IActionResult GetAlbums()
{
if (!IsTokenValid("read:albums"))
{
return StatusCode(401, "Not allowed");
}
List<Album> albums = new List<Album>();
var albumContext = new AlbumContext(_connectionString);
@@ -58,13 +54,8 @@ namespace Icarus.Controllers.V1
}
[HttpGet("{id}")]
public IActionResult Get(int id)
public IActionResult GetAlbum(int id)
{
if (!IsTokenValid("read:albums"))
{
return StatusCode(401, "Not allowed");
}
Album album = new Album
{
AlbumID = id
+3 -7
View File
@@ -13,6 +13,7 @@ namespace Icarus.Controllers.V1
{
[Route("api/v1/artist")]
[ApiController]
[Authorize]
public class ArtistController : BaseController
{
#region Fields
@@ -37,13 +38,8 @@ namespace Icarus.Controllers.V1
#region HTTP Routes
[HttpGet]
public IActionResult Get()
public IActionResult GetArtists()
{
if (!IsTokenValid("read:artists"))
{
return StatusCode(401, "Not allowed");
}
var artistContext = new ArtistContext(_connectionString);
var artists = artistContext.Artists.ToList();
@@ -55,7 +51,7 @@ namespace Icarus.Controllers.V1
}
[HttpGet("{id}")]
public IActionResult Get(int id)
public IActionResult GetArtist(int id)
{
if (!IsTokenValid("read:artists"))
{
+2
View File
@@ -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();
+5 -13
View File
@@ -15,6 +15,7 @@ namespace Icarus.Controllers.V1
{
[Route("api/v1/coverart")]
[ApiController]
[Authorize]
public class CoverArtController : BaseController
{
#region Fields
@@ -34,13 +35,9 @@ namespace Icarus.Controllers.V1
#region HTTP Routes
public IActionResult Get()
[HttpGet]
public IActionResult GetCoverArts()
{
if (!IsTokenValid("read:songs"))
{
return StatusCode(401, "Not allowed");
}
var coverArtContext = new CoverArtContext(_connectionString);
var coverArtRecords = coverArtContext.CoverArtImages.ToList();
@@ -58,13 +55,8 @@ namespace Icarus.Controllers.V1
}
[HttpGet("{id}")]
public async Task<IActionResult> Get(int id)
public IActionResult GetCoverArt(int id)
{
if (!IsTokenValid("download:cover_art"))
{
return StatusCode(401, "Not allowed");
}
var coverArt = new CoverArt { CoverArtID = id };
var coverArtContext = new CoverArtContext(_connectionString);
@@ -74,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",
+3 -12
View File
@@ -14,6 +14,7 @@ namespace Icarus.Controllers.V1
{
[Route("api/v1/genre")]
[ApiController]
[Authorize]
public class GenreController : BaseController
{
#region Fields
@@ -38,13 +39,8 @@ namespace Icarus.Controllers.V1
#region HTTP Routes
[HttpGet]
public IActionResult Get()
public IActionResult GetGenres()
{
if (!IsTokenValid("read:genre"))
{
return StatusCode(401, "Not allowed");
}
var genres = new List<Genre>();
var genreStore = new GenreContext(_connectionString);
@@ -58,13 +54,8 @@ namespace Icarus.Controllers.V1
}
[HttpGet("{id}")]
public IActionResult Get(int id)
public IActionResult GetGenre(int id)
{
if (!IsTokenValid("read:genre"))
{
return StatusCode(401, "Not allowed");
}
var genre = new Genre
{
GenreID = id
+13 -5
View File
@@ -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;
@@ -41,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);
@@ -55,6 +53,8 @@ namespace Icarus.Controllers.V1
Username = user.Username
};
try
{
if (context.Users.FirstOrDefault(usr => usr.Username.Equals(user.Username)) != null)
{
user = context.Users.FirstOrDefault(usr => usr.Username.Equals(user.Username));
@@ -73,7 +73,7 @@ namespace Icarus.Controllers.V1
TokenManager tk = new TokenManager(_config);
loginRes = tk.LogIn(user);
loginRes = tk.LoginSymmetric(user);
return Ok(loginRes);
}
@@ -84,6 +84,14 @@ namespace Icarus.Controllers.V1
return NotFound(loginRes);
}
}
catch (Exception ex)
{
_logger.LogError("An error occurred: {0}", ex.Message);
_logger.LogError("Inner Exception: {0}", ex.InnerException.Message);
}
return NotFound(loginRes);
}
#endregion
}
}
+1 -1
View File
@@ -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);
@@ -19,6 +19,7 @@ namespace Icarus.Controllers.V1
{
[Route("api/v1/song/compressed/data")]
[ApiController]
[Authorize]
public class SongCompressedDataController : BaseController
{
#region Fields
@@ -45,13 +46,8 @@ namespace Icarus.Controllers.V1
#region API Routes
[HttpGet("{id}")]
public async Task<IActionResult> Get(int id)
public async Task<IActionResult> DownloadCompressedSong(int id)
{
if (!IsTokenValid("download:songs"))
{
return StatusCode(401, "Not allowed");
}
var context = new SongContext(_connectionString);
SongCompression cmp = new SongCompression(_archiveDir);
+4 -18
View File
@@ -18,6 +18,7 @@ namespace Icarus.Controllers.V1
{
[Route("api/v1/song")]
[ApiController]
[Authorize]
public class SongController : BaseController
{
#region Fields
@@ -47,13 +48,8 @@ namespace Icarus.Controllers.V1
[HttpGet]
public IActionResult Get()
public IActionResult GetSongs()
{
if (!IsTokenValid("read:song_details"))
{
return StatusCode(401, "Not allowed");
}
List<Song> songs = new List<Song>();
Console.WriteLine("Attemtping to retrieve songs");
_logger.LogInformation("Attempting to retrieve songs");
@@ -69,13 +65,8 @@ namespace Icarus.Controllers.V1
}
[HttpGet("{id}")]
public IActionResult Get(int id)
public IActionResult GetSong(int id)
{
if (!IsTokenValid("read:song_details"))
{
return StatusCode(401, "Not allowed");
}
var context = new SongContext(_connectionString);
Song song = new Song { SongID = id };
@@ -90,13 +81,8 @@ namespace Icarus.Controllers.V1
}
[HttpPut("{id}")]
public IActionResult Put(int id, [FromBody] Song song)
public IActionResult UpdateSong(int id, [FromBody] Song song)
{
if (!IsTokenValid("update:songs"))
{
return StatusCode(401, "Not allowed");
}
var context = new SongContext(_connectionString);
song.SongID = id;
+7 -29
View File
@@ -20,6 +20,7 @@ namespace Icarus.Controllers.V1
{
[Route("api/v1/song/data")]
[ApiController]
[Authorize]
public class SongDataController : BaseController
{
#region Fields
@@ -47,18 +48,12 @@ namespace Icarus.Controllers.V1
[HttpGet("download/{id}")]
[Route("private-scoped")]
public async Task<IActionResult> Get(int id)
public IActionResult Download(int id)
{
if (!IsTokenValid("download:songs"))
{
return StatusCode(401, "Not allowed");
}
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);
}
@@ -76,14 +71,8 @@ namespace Icarus.Controllers.V1
// Cover art
//
[HttpPost("upload"), DisableRequestSizeLimit]
[Route("private-scoped")]
public IActionResult Post([FromForm(Name = "file")] List<IFormFile> songData)
public IActionResult Upload([FromForm(Name = "file")] List<IFormFile> songData)
{
if (!IsTokenValid("upload:songs"))
{
return StatusCode(401, "Not allowed");
}
try
{
// Console.WriteLine("Uploading song...");
@@ -118,14 +107,8 @@ namespace Icarus.Controllers.V1
// as well as the cover art
//
[HttpPost("upload/with/data")]
[Route("private-scoped")]
public IActionResult Post ([FromForm] UploadSongWithDataForm up)
public IActionResult UploadWithData([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))
@@ -145,13 +128,8 @@ namespace Icarus.Controllers.V1
}
[HttpDelete("delete/{id}")]
public IActionResult Delete(int id)
public IActionResult DeleteSong(int id)
{
if (!IsTokenValid("delete:songs"))
{
return StatusCode(401, "Not allowed");
}
var songContext = new SongContext(_connectionString);
var songMetaData = new Song{ SongID = id };
@@ -179,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")]
+2 -6
View File
@@ -20,6 +20,7 @@ namespace Icarus.Controllers.V1
{
[Route("api/v1/song/stream")]
[ApiController]
[Authorize]
public class SongStreamController : BaseController
{
#region Fields
@@ -44,13 +45,8 @@ namespace Icarus.Controllers.V1
#region HTTP endpoints
[HttpGet("{id}")]
public async Task<IActionResult> Get(int id)
public async Task<IActionResult> StreamSong(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);
+1
View File
@@ -25,6 +25,7 @@
<PackageReference Include="Portable.BouncyCastle" Version="1.9.0" />
<PackageReference Include="RestSharp" Version="106.15.0" />
<PackageReference Include="SevenZip" Version="19.0.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="6.15.0" />
<PackageReference Include="taglib" Version="2.1.0" />
</ItemGroup>
+3 -2
View File
@@ -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"
+16 -21
View File
@@ -37,32 +37,29 @@ 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
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": "Manaiswhatyouthinkitis",
"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 +69,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:
@@ -140,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
+55 -39
View File
@@ -1,13 +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;
@@ -17,35 +22,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
@@ -72,6 +48,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<SongContext>(options => options.UseMySQL(connString));
services.AddDbContext<AlbumContext>(options => options.UseMySQL(connString));
services.AddDbContext<ArtistContext>(options => options.UseMySQL(connString));
@@ -79,24 +69,50 @@ namespace Icarus
services.AddDbContext<GenreContext>(options => options.UseMySQL(connString));
services.AddDbContext<CoverArtContext>(options => options.UseMySQL(connString));
services.AddAsymmetricAuthentication(Configuration);
/**
services.AddTransient<AuthenticationService>(au => new AuthenticationService(new UserService(Configuration), new TokenService(Configuration), Configuration));
services.AddTransient<UserService>(us => new UserService(Configuration));
services.AddTransient<TokenService>(tk => new TokenService(Configuration));
services.AddTransient<UserRepository>();
services.AddTransient<UserContext>(uc => new UserContext(connString));
*/
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();
+6
View File
@@ -16,6 +16,12 @@
"PrivateKeyPath": "",
"PublicKeyPath": ""
},
"JWT": {
"Issuer": "",
"Audience": "",
"Secret": "",
"Subject": ""
},
"ConnectionStrings": {
"DefaultConnection": "Server=;Database=;Uid=;Pwd=;"
},
+6
View File
@@ -16,6 +16,12 @@
"PrivateKeyPath": "",
"PublicKeyPath": ""
},
"JWT": {
"Issuer": "",
"Audience": "",
"Secret": "",
"Subject": ""
},
"ConnectionStrings": {
"DefaultConnection": "Server=;Database=;Uid=;Pwd=;"
},