Merge pull request #77 from kdeng00/jwt_symmetric_keys

JWT symmetric keys.

This closes #73.
This commit was merged in pull request #77.
This commit is contained in:
Kun Deng
2022-09-04 20:58:13 -04:00
committed by GitHub
14 changed files with 125 additions and 163 deletions
+46 -6
View File
@@ -4,10 +4,14 @@ 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.Security.Claims;
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;
@@ -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) public bool IsTokenValid(string scope, string accessToken)
{ {
var result = false; var result = false;
@@ -201,23 +239,25 @@ 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;
+1 -10
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
@@ -40,11 +41,6 @@ namespace Icarus.Controllers.V1
[HttpGet] [HttpGet]
public IActionResult Get() public IActionResult Get()
{ {
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);
@@ -60,11 +56,6 @@ namespace Icarus.Controllers.V1
[HttpGet("{id}")] [HttpGet("{id}")]
public IActionResult Get(int id) public IActionResult Get(int id)
{ {
if (!IsTokenValid("read:albums"))
{
return StatusCode(401, "Not allowed");
}
Album album = new Album Album album = new Album
{ {
AlbumID = id AlbumID = id
+1 -5
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
@@ -39,11 +40,6 @@ namespace Icarus.Controllers.V1
[HttpGet] [HttpGet]
public IActionResult Get() public IActionResult Get()
{ {
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();
+1 -10
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
@@ -36,11 +37,6 @@ namespace Icarus.Controllers.V1
#region HTTP Routes #region HTTP Routes
public IActionResult Get() public IActionResult Get()
{ {
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();
@@ -60,11 +56,6 @@ namespace Icarus.Controllers.V1
[HttpGet("{id}")] [HttpGet("{id}")]
public async Task<IActionResult> Get(int id) public async Task<IActionResult> Get(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);
+1 -10
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
@@ -40,11 +41,6 @@ namespace Icarus.Controllers.V1
[HttpGet] [HttpGet]
public IActionResult Get() public IActionResult Get()
{ {
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);
@@ -60,11 +56,6 @@ namespace Icarus.Controllers.V1
[HttpGet("{id}")] [HttpGet("{id}")]
public IActionResult Get(int id) public IActionResult Get(int id)
{ {
if (!IsTokenValid("read:genre"))
{
return StatusCode(401, "Not allowed");
}
var genre = new Genre var genre = new Genre
{ {
GenreID = id GenreID = id
+29 -22
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;
@@ -55,34 +52,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
} }
@@ -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
@@ -47,11 +48,6 @@ namespace Icarus.Controllers.V1
[HttpGet("{id}")] [HttpGet("{id}")]
public async Task<IActionResult> Get(int id) public async Task<IActionResult> Get(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);
+1 -15
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
@@ -49,11 +50,6 @@ namespace Icarus.Controllers.V1
[HttpGet] [HttpGet]
public IActionResult Get() public IActionResult Get()
{ {
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");
@@ -71,11 +67,6 @@ namespace Icarus.Controllers.V1
[HttpGet("{id}")] [HttpGet("{id}")]
public IActionResult Get(int id) public IActionResult Get(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 };
@@ -92,11 +83,6 @@ namespace Icarus.Controllers.V1
[HttpPut("{id}")] [HttpPut("{id}")]
public IActionResult Put(int id, [FromBody] Song song) public IActionResult Put(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;
+1 -20
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
@@ -50,11 +51,6 @@ namespace Icarus.Controllers.V1
[Route("private-scoped")] [Route("private-scoped")]
public async Task<IActionResult> Get(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});
@@ -79,11 +75,6 @@ namespace Icarus.Controllers.V1
[Route("private-scoped")] [Route("private-scoped")]
public IActionResult Post([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...");
@@ -121,11 +112,6 @@ namespace Icarus.Controllers.V1
[Route("private-scoped")] [Route("private-scoped")]
public IActionResult Post ([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))
@@ -147,11 +133,6 @@ namespace Icarus.Controllers.V1
[HttpDelete("delete/{id}")] [HttpDelete("delete/{id}")]
public IActionResult Delete(int id) public IActionResult Delete(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 };
+1 -5
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
@@ -46,11 +47,6 @@ namespace Icarus.Controllers.V1
[HttpGet("{id}")] [HttpGet("{id}")]
public async Task<IActionResult> Get(int id) public async Task<IActionResult> Get(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);
+14 -16
View File
@@ -37,32 +37,27 @@ 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 #### 1. Creating RSA keys
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 +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 ### 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:
+16 -39
View File
@@ -1,5 +1,7 @@
using System; using System;
using System.Text;
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;
@@ -17,35 +19,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 +45,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,16 +66,6 @@ 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();
} }
+6
View File
@@ -16,6 +16,12 @@
"PrivateKeyPath": "", "PrivateKeyPath": "",
"PublicKeyPath": "" "PublicKeyPath": ""
}, },
"JWT": {
"Issuer": "",
"Audience": "",
"Secret": "",
"Subject": ""
},
"ConnectionStrings": { "ConnectionStrings": {
"DefaultConnection": "Server=;Database=;Uid=;Pwd=;" "DefaultConnection": "Server=;Database=;Uid=;Pwd=;"
}, },
+6
View File
@@ -16,6 +16,12 @@
"PrivateKeyPath": "", "PrivateKeyPath": "",
"PublicKeyPath": "" "PublicKeyPath": ""
}, },
"JWT": {
"Issuer": "",
"Audience": "",
"Secret": "",
"Subject": ""
},
"ConnectionStrings": { "ConnectionStrings": {
"DefaultConnection": "Server=;Database=;Uid=;Pwd=;" "DefaultConnection": "Server=;Database=;Uid=;Pwd=;"
}, },