Compare commits

..

24 Commits

Author SHA1 Message Date
Kun Deng 37cfda84b5 Merge pull request #86 from kdeng00/deprecate_asymmetric_keys
Deprecating asymmetric keys. Closes #84
2022-12-16 14:09:42 -05:00
Kun Deng 3ee17a77a8 Merge pull request #83 from kdeng00/dependabot/nuget/Newtonsoft.Json-13.0.2
Bump Newtonsoft.Json from 13.0.1 to 13.0.2
2022-12-16 14:07:06 -05:00
kdeng00 522be59973 Removing deprecated call
Removing method call that has been deprecated from one of the Controller classes
2022-12-16 13:56:56 -05:00
kdeng00 712a0a9a4a Updated config file 2022-12-16 13:54:18 -05:00
kdeng00 74569d85b6 Deprecating asymmetric keys
Starting work to deprecate using public and private keys to sign tokens
2022-12-16 13:48:15 -05:00
Kun Deng b660f8361f Merge pull request #85 from kdeng00/link_token_with_user
Token modification.  Closes #75
2022-12-16 13:04:37 -05:00
kdeng00 d460b5d5d6 Token modification
Addresses #84. Adding user ID to the access token.
2022-12-16 12:59:11 -05:00
dependabot[bot] 4a1c1f1f78 Bump Newtonsoft.Json from 13.0.1 to 13.0.2
Bumps [Newtonsoft.Json](https://github.com/JamesNK/Newtonsoft.Json) from 13.0.1 to 13.0.2.
- [Release notes](https://github.com/JamesNK/Newtonsoft.Json/releases)
- [Commits](https://github.com/JamesNK/Newtonsoft.Json/compare/13.0.1...13.0.2)

---
updated-dependencies:
- dependency-name: Newtonsoft.Json
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-12-08 15:46:41 +00:00
Kun Deng 66b5944f92 Merge pull request #81 from kdeng00/pre-release
Pre release
2022-09-05 17:13:11 -04:00
Kun Deng 80628292e2 Merge pull request #80 from kdeng00/v0.1.20
V0.1.20
2022-09-05 17:12:57 -04:00
Kun Deng 57f4ec1261 Merge pull request #78 from kdeng00/add_swagger_docs
Swagger docs. Close #71
2022-09-05 17:09:23 -04:00
Kun Deng c191d7ee7a Update README.md
Note about swagger
2022-09-05 17:07:26 -04:00
Kun Deng 73bfff3940 Cleanup 2022-09-05 17:02:25 -04:00
Kun Deng 38c056cc99 Swagger docs 2022-09-05 16:58:10 -04:00
Kun Deng 1643f78720 Delete v0.2.yml 2022-09-04 21:29:10 -04:00
Kun Deng 06bb52dfa5 v0.2 Branch 2022-09-04 21:23:11 -04:00
Kun Deng ab89d1602f Create dotnet.yml 2022-09-04 21:20:53 -04:00
Kun Deng 640971dea8 Merge pull request #77 from kdeng00/jwt_symmetric_keys
JWT symmetric keys.

This closes #73.
2022-09-04 20:58:13 -04:00
Kun Deng 664b0d520b Updated Readme 2022-09-04 20:56:37 -04:00
Kun Deng 11fe1f29e1 Updated Readme 2022-09-04 20:50:35 -04:00
Kun Deng 05b5de0939 Cleanup 2022-09-04 20:45:17 -04:00
Kun Deng d5944c470b Symmetric keys with jwt has been implemented 2022-09-04 20:34:33 -04:00
Kun Deng 00ef8d0242 Added JWT bearer authentication 2022-09-04 18:56:56 -04:00
Kun Deng 77c6ee00ea JWT symmetric keys
Updated config file to store neccessary information
2022-09-04 18:39:48 -04:00
19 changed files with 243 additions and 228 deletions
+25
View File
@@ -0,0 +1,25 @@
name: .NET
on:
push:
branches: [ "master" ]
pull_request:
branches: [ "master" ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup .NET
uses: actions/setup-dotnet@v2
with:
dotnet-version: 6.0.x
- name: Restore dependencies
run: dotnet restore
- name: Build
run: dotnet build --no-restore
- name: Test
run: dotnet test --no-build --verbosity normal
+57 -17
View File
@@ -4,10 +4,13 @@ using System.Linq;
using System.Security.Claims; using System.Security.Claims;
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.IdentityModel.Tokens.Jwt;
using System.Text;
using JWT; using JWT;
using JWT.Serializers; using JWT.Serializers;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.IdentityModel.Tokens;
using Newtonsoft.Json; using Newtonsoft.Json;
using Org.BouncyCastle.Crypto; using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.Crypto.Parameters; using Org.BouncyCastle.Crypto.Parameters;
@@ -26,10 +29,10 @@ namespace Icarus.Controllers.Managers
#region Fields #region Fields
private string _clientId; private string _clientId;
private string _clientSecret; private string _clientSecret;
private string _privateKeyPath; private string _privateKeyPath = string.Empty;
private string _privateKey; private string _privateKey = string.Empty;
private string _publicKeyPath; private string _publicKeyPath = string.Empty;
private string _publicKey; private string _publicKey = string.Empty;
private string _audience; private string _audience;
private string _grantType; private string _grantType;
private string _url; private string _url;
@@ -85,6 +88,7 @@ namespace Icarus.Controllers.Managers
} }
[Obsolete("Asymmetric key signing for tokens have been deprecated")]
public LoginResult LogIn(User user) public LoginResult LogIn(User user)
{ {
var tokenResult = new TokenTierOne(); var tokenResult = new TokenTierOne();
@@ -113,6 +117,43 @@ namespace Icarus.Controllers.Managers
}; };
} }
public LoginResult LoginSymmetric(User user)
{
var tokenResult = new TokenTierOne();
tokenResult.TokenType = "Jwt";
var payload = Payload();
payload.Add(new System.Security.Claims.Claim("user_id", user.UserID.ToString(), ClaimValueTypes.Integer));
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"
};
}
[Obsolete("Asymmetric key signing for tokens have been deprecated")]
public bool IsTokenValid(string scope, string accessToken) public bool IsTokenValid(string scope, string accessToken)
{ {
var result = false; var result = false;
@@ -133,7 +174,8 @@ namespace Icarus.Controllers.Managers
return result; return result;
} }
public Token? DecodeToken(string accessToken) [Obsolete("Asymmetric key signing for tokens have been deprecated")]
public Token DecodeToken(string accessToken)
{ {
var rsaParams = GetRSAPublic(_publicKey); var rsaParams = GetRSAPublic(_publicKey);
Token tok = null; Token tok = null;
@@ -201,28 +243,31 @@ namespace Icarus.Controllers.Managers
private List<Claim> Payload() private List<Claim> Payload()
{ {
const int expLimit = 24; var expLimit = 30;
var currentDate = DateTime.Now; var currentDate = DateTime.Now;
var expiredDate = currentDate.AddHours(expLimit); var expiredDate = currentDate.AddMinutes(expLimit);
var issued = Math.Floor((currentDate - DateTime.UnixEpoch).TotalSeconds); var issued = Math.Floor((currentDate - DateTime.UnixEpoch).TotalSeconds);
var expires = Math.Floor((expiredDate - DateTime.UnixEpoch).TotalSeconds); var expires = Math.Floor((expiredDate - DateTime.UnixEpoch).TotalSeconds);
var issuer = "https://soaricarus.auth0.com"; var issuer = "https://soaricarus.auth0.com";
issuer = "http://localhost:5002"; issuer = "http://localhost:5002";
var audience = "https://icarus/api"; var audience = "https://icarus/api";
audience = "http://localhost:5002"; audience = "http://localhost:5002";
var subject = _config["JWT:Subject"];
var claim = new List<System.Security.Claims.Claim>() var claim = new List<System.Security.Claims.Claim>()
{ {
new System.Security.Claims.Claim("scope", AllScopes(), "string"), 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 System.Security.Claims.Claim("iat", $"{issued}", "integer") new Claim(JwtRegisteredClaimNames.Sub, subject),
new System.Security.Claims.Claim(JwtRegisteredClaimNames.Iat, currentDate.ToString())
}; };
return claim; return claim;
} }
[Obsolete("Asymmetric key signing for tokens have been deprecated")]
private string CreateToken(List<Claim> claims, string privateKey) private string CreateToken(List<Claim> claims, string privateKey)
{ {
var token = string.Empty; var token = string.Empty;
@@ -282,6 +327,7 @@ namespace Icarus.Controllers.Managers
return token; return token;
} }
[Obsolete("Asymmetric key signing for tokens have been deprecated")]
private RSAParameters GetRSAPublic(string publicKey) private RSAParameters GetRSAPublic(string publicKey)
{ {
using (var tr = new System.IO.StringReader(publicKey)) using (var tr = new System.IO.StringReader(publicKey))
@@ -321,12 +367,6 @@ namespace Icarus.Controllers.Managers
_audience = _config["Auth0:ApiIdentifier"]; _audience = _config["Auth0:ApiIdentifier"];
_grantType = "client_credentials"; _grantType = "client_credentials";
_url = $"https://{_config["Auth0:Domain"]}"; _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();
} }
#region Testing Methods #region Testing Methods
+3 -12
View File
@@ -14,6 +14,7 @@ namespace Icarus.Controllers.V1
{ {
[Route("api/v1/album")] [Route("api/v1/album")]
[ApiController] [ApiController]
[Authorize]
public class AlbumController : BaseController public class AlbumController : BaseController
{ {
#region Fields #region Fields
@@ -38,13 +39,8 @@ namespace Icarus.Controllers.V1
#region HTTP Routes #region HTTP Routes
[HttpGet] [HttpGet]
public IActionResult Get() public IActionResult GetAlbums()
{ {
if (!IsTokenValid("read:albums"))
{
return StatusCode(401, "Not allowed");
}
List<Album> albums = new List<Album>(); List<Album> albums = new List<Album>();
var albumContext = new AlbumContext(_connectionString); var albumContext = new AlbumContext(_connectionString);
@@ -58,13 +54,8 @@ namespace Icarus.Controllers.V1
} }
[HttpGet("{id}")] [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 Album album = new Album
{ {
AlbumID = id AlbumID = id
+3 -12
View File
@@ -13,6 +13,7 @@ namespace Icarus.Controllers.V1
{ {
[Route("api/v1/artist")] [Route("api/v1/artist")]
[ApiController] [ApiController]
[Authorize]
public class ArtistController : BaseController public class ArtistController : BaseController
{ {
#region Fields #region Fields
@@ -37,13 +38,8 @@ namespace Icarus.Controllers.V1
#region HTTP Routes #region HTTP Routes
[HttpGet] [HttpGet]
public IActionResult Get() public IActionResult GetArtists()
{ {
if (!IsTokenValid("read:artists"))
{
return StatusCode(401, "Not allowed");
}
var artistContext = new ArtistContext(_connectionString); var artistContext = new ArtistContext(_connectionString);
var artists = artistContext.Artists.ToList(); var artists = artistContext.Artists.ToList();
@@ -55,13 +51,8 @@ namespace Icarus.Controllers.V1
} }
[HttpGet("{id}")] [HttpGet("{id}")]
public IActionResult Get(int id) public IActionResult GetArtist(int id)
{ {
if (!IsTokenValid("read:artists"))
{
return StatusCode(401, "Not allowed");
}
Artist artist = new Artist Artist artist = new Artist
{ {
ArtistID = id ArtistID = id
+4
View File
@@ -17,6 +17,8 @@ namespace Icarus.Controllers.V1
#region Methods #region Methods
[ApiExplorerSettings(IgnoreApi = true)]
[Obsolete("Asymmetric key signing for tokens have been deprecated")]
protected string ParseBearerTokenFromHeader() protected string ParseBearerTokenFromHeader()
{ {
var token = string.Empty; var token = string.Empty;
@@ -37,6 +39,8 @@ namespace Icarus.Controllers.V1
return token; return token;
} }
[ApiExplorerSettings(IgnoreApi = true)]
[Obsolete("Asymmetric key signing for tokens have been deprecated")]
protected bool IsTokenValid(string scope) protected bool IsTokenValid(string scope)
{ {
var token = ParseBearerTokenFromHeader(); var token = ParseBearerTokenFromHeader();
+5 -13
View File
@@ -15,6 +15,7 @@ namespace Icarus.Controllers.V1
{ {
[Route("api/v1/coverart")] [Route("api/v1/coverart")]
[ApiController] [ApiController]
[Authorize]
public class CoverArtController : BaseController public class CoverArtController : BaseController
{ {
#region Fields #region Fields
@@ -34,13 +35,9 @@ namespace Icarus.Controllers.V1
#region HTTP Routes #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 coverArtContext = new CoverArtContext(_connectionString);
var coverArtRecords = coverArtContext.CoverArtImages.ToList(); var coverArtRecords = coverArtContext.CoverArtImages.ToList();
@@ -58,13 +55,8 @@ namespace Icarus.Controllers.V1
} }
[HttpGet("{id}")] [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 coverArt = new CoverArt { CoverArtID = id };
var coverArtContext = new CoverArtContext(_connectionString); var coverArtContext = new CoverArtContext(_connectionString);
@@ -74,7 +66,7 @@ namespace Icarus.Controllers.V1
if (coverArt != null) if (coverArt != null)
{ {
_logger.LogInformation("Found cover art record"); _logger.LogInformation("Found cover art record");
var coverArtBytes = await System.IO.File.ReadAllBytesAsync( var coverArtBytes = System.IO.File.ReadAllBytes(
coverArt.ImagePath); coverArt.ImagePath);
return File(coverArtBytes, "application/x-msdownload", return File(coverArtBytes, "application/x-msdownload",
+3 -12
View File
@@ -14,6 +14,7 @@ namespace Icarus.Controllers.V1
{ {
[Route("api/v1/genre")] [Route("api/v1/genre")]
[ApiController] [ApiController]
[Authorize]
public class GenreController : BaseController public class GenreController : BaseController
{ {
#region Fields #region Fields
@@ -38,13 +39,8 @@ namespace Icarus.Controllers.V1
#region HTTP Routes #region HTTP Routes
[HttpGet] [HttpGet]
public IActionResult Get() public IActionResult GetGenres()
{ {
if (!IsTokenValid("read:genre"))
{
return StatusCode(401, "Not allowed");
}
var genres = new List<Genre>(); var genres = new List<Genre>();
var genreStore = new GenreContext(_connectionString); var genreStore = new GenreContext(_connectionString);
@@ -58,13 +54,8 @@ namespace Icarus.Controllers.V1
} }
[HttpGet("{id}")] [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 var genre = new Genre
{ {
GenreID = id GenreID = id
+31 -23
View File
@@ -1,8 +1,5 @@
using System; using System;
using System.Collections.Generic;
using System.Configuration;
using System.Linq; using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
@@ -41,7 +38,8 @@ namespace Icarus.Controllers.V1
#region HTTP endpoints #region HTTP endpoints
public IActionResult Post([FromBody] User user) [HttpPost]
public IActionResult Login([FromBody] User user)
{ {
var context = new UserContext(_connectionString); var context = new UserContext(_connectionString);
@@ -55,34 +53,44 @@ namespace Icarus.Controllers.V1
Username = user.Username 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)); if (context.Users.FirstOrDefault(usr => usr.Username.Equals(user.Username)) != null)
var validatePass = new PasswordEncryption();
var validated = validatePass.VerifyPassword(user, password);
if (!validated)
{ {
loginRes.Message = message; user = context.Users.FirstOrDefault(usr => usr.Username.Equals(user.Username));
_logger.LogInformation(message);
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.LoginSymmetric(user);
return Ok(loginRes); return Ok(loginRes);
} }
else
{
loginRes.Message = message;
_logger.LogInformation("Successfully validated user credentials"); return NotFound(loginRes);
}
TokenManager tk = new TokenManager(_config);
loginRes = tk.LogIn(user);
return Ok(loginRes);
} }
else catch (Exception ex)
{ {
loginRes.Message = message; _logger.LogError("An error occurred: {0}", ex.Message);
_logger.LogError("Inner Exception: {0}", ex.InnerException.Message);
return NotFound(loginRes);
} }
return NotFound(loginRes);
} }
#endregion #endregion
} }
+1 -1
View File
@@ -37,7 +37,7 @@ namespace Icarus.Controllers.V1
#endregion #endregion
[HttpPost] [HttpPost]
public IActionResult Post([FromBody] User user) public IActionResult RegisterUser([FromBody] User user)
{ {
PasswordEncryption pe = new PasswordEncryption(); PasswordEncryption pe = new PasswordEncryption();
user.Password = pe.HashPassword(user); user.Password = pe.HashPassword(user);
@@ -19,6 +19,7 @@ namespace Icarus.Controllers.V1
{ {
[Route("api/v1/song/compressed/data")] [Route("api/v1/song/compressed/data")]
[ApiController] [ApiController]
[Authorize]
public class SongCompressedDataController : BaseController public class SongCompressedDataController : BaseController
{ {
#region Fields #region Fields
@@ -45,13 +46,8 @@ namespace Icarus.Controllers.V1
#region API Routes #region API Routes
[HttpGet("{id}")] [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); var context = new SongContext(_connectionString);
SongCompression cmp = new SongCompression(_archiveDir); SongCompression cmp = new SongCompression(_archiveDir);
+4 -18
View File
@@ -18,6 +18,7 @@ namespace Icarus.Controllers.V1
{ {
[Route("api/v1/song")] [Route("api/v1/song")]
[ApiController] [ApiController]
[Authorize]
public class SongController : BaseController public class SongController : BaseController
{ {
#region Fields #region Fields
@@ -47,13 +48,8 @@ namespace Icarus.Controllers.V1
[HttpGet] [HttpGet]
public IActionResult Get() public IActionResult GetSongs()
{ {
if (!IsTokenValid("read:song_details"))
{
return StatusCode(401, "Not allowed");
}
List<Song> songs = new List<Song>(); List<Song> songs = new List<Song>();
Console.WriteLine("Attemtping to retrieve songs"); Console.WriteLine("Attemtping to retrieve songs");
_logger.LogInformation("Attempting to retrieve songs"); _logger.LogInformation("Attempting to retrieve songs");
@@ -69,13 +65,8 @@ namespace Icarus.Controllers.V1
} }
[HttpGet("{id}")] [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); var context = new SongContext(_connectionString);
Song song = new Song { SongID = id }; Song song = new Song { SongID = id };
@@ -90,13 +81,8 @@ namespace Icarus.Controllers.V1
} }
[HttpPut("{id}")] [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); var context = new SongContext(_connectionString);
song.SongID = id; song.SongID = id;
+16 -38
View File
@@ -20,6 +20,7 @@ namespace Icarus.Controllers.V1
{ {
[Route("api/v1/song/data")] [Route("api/v1/song/data")]
[ApiController] [ApiController]
[Authorize]
public class SongDataController : BaseController public class SongDataController : BaseController
{ {
#region Fields #region Fields
@@ -47,18 +48,12 @@ namespace Icarus.Controllers.V1
[HttpGet("download/{id}")] [HttpGet("download/{id}")]
[Route("private-scoped")] public IActionResult Download(int id)
public async Task<IActionResult> Get(int id)
{ {
if (!IsTokenValid("download:songs"))
{
return StatusCode(401, "Not allowed");
}
var songContext = new SongContext(_connectionString); var songContext = new SongContext(_connectionString);
var songMetaData = songContext.RetrieveRecord(new Song { SongID = id}); 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); return File(song.Data, "application/x-msdownload", songMetaData.Filename);
} }
@@ -76,14 +71,8 @@ namespace Icarus.Controllers.V1
// Cover art // Cover art
// //
[HttpPost("upload"), DisableRequestSizeLimit] [HttpPost("upload"), DisableRequestSizeLimit]
[Route("private-scoped")] public IActionResult Upload([FromForm(Name = "file")] List<IFormFile> songData)
public IActionResult Post([FromForm(Name = "file")] List<IFormFile> songData)
{ {
if (!IsTokenValid("upload:songs"))
{
return StatusCode(401, "Not allowed");
}
try try
{ {
// Console.WriteLine("Uploading song..."); // Console.WriteLine("Uploading song...");
@@ -118,14 +107,8 @@ namespace Icarus.Controllers.V1
// as well as the cover art // as well as the cover art
// //
[HttpPost("upload/with/data")] [HttpPost("upload/with/data")]
[Route("private-scoped")] public IActionResult UploadWithData([FromForm] UploadSongWithDataForm up)
public IActionResult Post ([FromForm] UploadSongWithDataForm up)
{ {
if (!IsTokenValid("upload:songs"))
{
return StatusCode(401, "Not allowed");
}
try try
{ {
if (up.SongData.Length > 0 && up.CoverArtData.Length > 0 && !string.IsNullOrEmpty(up.SongFile)) if (up.SongData.Length > 0 && up.CoverArtData.Length > 0 && !string.IsNullOrEmpty(up.SongFile))
@@ -145,13 +128,8 @@ namespace Icarus.Controllers.V1
} }
[HttpDelete("delete/{id}")] [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 songContext = new SongContext(_connectionString);
var songMetaData = new Song{ SongID = id }; var songMetaData = new Song{ SongID = id };
@@ -175,15 +153,15 @@ namespace Icarus.Controllers.V1
} }
public class UploadSongWithDataForm public class UploadSongWithDataForm
{ {
[FromForm(Name = "file")] [FromForm(Name = "file")]
public IFormFile SongData { get; set; } 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")] [FromForm(Name = "cover")]
public IFormFile CoverArtData { get; set; } public IFormFile CoverArtData { get; set; }
[FromForm(Name = "metadata")] [FromForm(Name = "metadata")]
public string SongFile { get; set; } public string SongFile { get; set; }
} }
} }
} }
+2 -6
View File
@@ -20,6 +20,7 @@ namespace Icarus.Controllers.V1
{ {
[Route("api/v1/song/stream")] [Route("api/v1/song/stream")]
[ApiController] [ApiController]
[Authorize]
public class SongStreamController : BaseController public class SongStreamController : BaseController
{ {
#region Fields #region Fields
@@ -44,13 +45,8 @@ namespace Icarus.Controllers.V1
#region HTTP endpoints #region HTTP endpoints
[HttpGet("{id}")] [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 context = new SongContext(_config.GetConnectionString("DefaultConnection"));
var song = context.Songs.FirstOrDefault(sng => sng.SongID == id); var song = context.Songs.FirstOrDefault(sng => sng.SongID == id);
+2 -1
View File
@@ -20,11 +20,12 @@
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
</PackageReference> </PackageReference>
<PackageReference Include="MySql.EntityFrameworkCore" Version="5.0.8" /> <PackageReference Include="MySql.EntityFrameworkCore" Version="5.0.8" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.2" />
<PackageReference Include="NLog.Web.AspNetCore" Version="4.14.0" /> <PackageReference Include="NLog.Web.AspNetCore" Version="4.14.0" />
<PackageReference Include="Portable.BouncyCastle" Version="1.9.0" /> <PackageReference Include="Portable.BouncyCastle" Version="1.9.0" />
<PackageReference Include="RestSharp" Version="106.15.0" /> <PackageReference Include="RestSharp" Version="106.15.0" />
<PackageReference Include="SevenZip" Version="19.0.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="System.IdentityModel.Tokens.Jwt" Version="6.15.0" />
<PackageReference Include="taglib" Version="2.1.0" /> <PackageReference Include="taglib" Version="2.1.0" />
</ItemGroup> </ItemGroup>
+3 -2
View File
@@ -12,7 +12,7 @@
"IIS Express": { "IIS Express": {
"commandName": "IISExpress", "commandName": "IISExpress",
"launchBrowser": true, "launchBrowser": true,
"launchUrl": "api/values", "launchUrl": "swagger",
"environmentVariables": { "environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development" "ASPNETCORE_ENVIRONMENT": "Development"
} }
@@ -20,7 +20,8 @@
"Icarus": { "Icarus": {
"commandName": "Project", "commandName": "Project",
"launchBrowser": true, "launchBrowser": true,
"launchUrl": "api/values", "dotnetRuneMessage": true,
"launchUrl": "swagger",
"applicationUrl": "http://localhost:5002", "applicationUrl": "http://localhost:5002",
"environmentVariables": { "environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development" "ASPNETCORE_ENVIRONMENT": "Development"
+16 -21
View File
@@ -37,32 +37,29 @@ One can interface with Icarus the music server either by:
## Getting started ## Getting started
There are several things that need to be completed to properly setup and secure the API. 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 2. API filesystem paths
3. Database connection string 3. Database connection string
4. Migrations 4. Migrations
### Creating RSA keys ### JWT Information
1. Create private key Configure JWT information. Notably the Secret
```
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 ```Json
"RSAKeys": { "JWT": {
"PrivateKeyPath": "", "Issuer": "IcarusAPI",
"PublicKeyPath": "" "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. 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 ```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 ### 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: 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. 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 ## License
+55 -39
View File
@@ -1,13 +1,18 @@
using System; using System;
using System.Text;
using System.Linq;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens; using Microsoft.IdentityModel.Tokens;
using Microsoft.OpenApi.Models;
using Newtonsoft.Json; using Newtonsoft.Json;
using NLog; using NLog;
using NLog.Web; using NLog.Web;
@@ -17,35 +22,6 @@ using Icarus.Database.Contexts;
namespace Icarus 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 public class Startup
{ {
#region Constructors #region Constructors
@@ -72,6 +48,20 @@ namespace Icarus
var connString = Configuration.GetConnectionString("DefaultConnection"); 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<SongContext>(options => options.UseMySQL(connString));
services.AddDbContext<AlbumContext>(options => options.UseMySQL(connString)); services.AddDbContext<AlbumContext>(options => options.UseMySQL(connString));
services.AddDbContext<ArtistContext>(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<GenreContext>(options => options.UseMySQL(connString));
services.AddDbContext<CoverArtContext>(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() services.AddControllers()
.AddNewtonsoftJson(); .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. // Called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env) public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{ {
// NOTE: Dev-related configuration can be done when env.IsDevelopment() evaluated to true // NOTE: Dev-related configuration can be done when env.IsDevelopment() evaluated to true
if (env.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseRouting(); app.UseRouting();
app.UseAuthentication(); app.UseAuthentication();
+5 -3
View File
@@ -12,9 +12,11 @@
"ClientId": "", "ClientId": "",
"ClientSecret": "" "ClientSecret": ""
}, },
"RSAKeys": { "JWT": {
"PrivateKeyPath": "", "Issuer": "",
"PublicKeyPath": "" "Audience": "",
"Secret": "",
"Subject": ""
}, },
"ConnectionStrings": { "ConnectionStrings": {
"DefaultConnection": "Server=;Database=;Uid=;Pwd=;" "DefaultConnection": "Server=;Database=;Uid=;Pwd=;"
+5 -3
View File
@@ -12,9 +12,11 @@
"ClientId": "", "ClientId": "",
"ClientSecret": "" "ClientSecret": ""
}, },
"RSAKeys": { "JWT": {
"PrivateKeyPath": "", "Issuer": "",
"PublicKeyPath": "" "Audience": "",
"Secret": "",
"Subject": ""
}, },
"ConnectionStrings": { "ConnectionStrings": {
"DefaultConnection": "Server=;Database=;Uid=;Pwd=;" "DefaultConnection": "Server=;Database=;Uid=;Pwd=;"