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.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;
+1 -10
View File
@@ -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<Album> albums = new List<Album>();
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
+1 -5
View File
@@ -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();
+1 -10
View File
@@ -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<IActionResult> Get(int id)
{
if (!IsTokenValid("download:cover_art"))
{
return StatusCode(401, "Not allowed");
}
var coverArt = new CoverArt { CoverArtID = id };
var coverArtContext = new CoverArtContext(_connectionString);
+1 -10
View File
@@ -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<Genre>();
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
+29 -22
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;
@@ -55,34 +52,44 @@ 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.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
}
@@ -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<IActionResult> Get(int id)
{
if (!IsTokenValid("download:songs"))
{
return StatusCode(401, "Not allowed");
}
var context = new SongContext(_connectionString);
SongCompression cmp = new SongCompression(_archiveDir);
+1 -15
View File
@@ -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<Song> songs = new List<Song>();
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;
+1 -20
View File
@@ -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<IActionResult> 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<IFormFile> 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 };
+1 -5
View File
@@ -20,6 +20,7 @@ namespace Icarus.Controllers.V1
{
[Route("api/v1/song/stream")]
[ApiController]
[Authorize]
public class SongStreamController : BaseController
{
#region Fields
@@ -46,11 +47,6 @@ namespace Icarus.Controllers.V1
[HttpGet("{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 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
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": "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 +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:
+16 -39
View File
@@ -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;
@@ -17,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
@@ -72,6 +45,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,16 +66,6 @@ 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();
}
+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=;"
},