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:
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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();
|
||||
}
|
||||
|
||||
@@ -16,6 +16,12 @@
|
||||
"PrivateKeyPath": "",
|
||||
"PublicKeyPath": ""
|
||||
},
|
||||
"JWT": {
|
||||
"Issuer": "",
|
||||
"Audience": "",
|
||||
"Secret": "",
|
||||
"Subject": ""
|
||||
},
|
||||
"ConnectionStrings": {
|
||||
"DefaultConnection": "Server=;Database=;Uid=;Pwd=;"
|
||||
},
|
||||
|
||||
@@ -16,6 +16,12 @@
|
||||
"PrivateKeyPath": "",
|
||||
"PublicKeyPath": ""
|
||||
},
|
||||
"JWT": {
|
||||
"Issuer": "",
|
||||
"Audience": "",
|
||||
"Secret": "",
|
||||
"Subject": ""
|
||||
},
|
||||
"ConnectionStrings": {
|
||||
"DefaultConnection": "Server=;Database=;Uid=;Pwd=;"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user