diff --git a/Ombi/Ombi.Core/IdentityResolver/IUserIdentityManager.cs b/Ombi/Ombi.Core/IdentityResolver/IUserIdentityManager.cs index 78facfc3e..89c6d8ecd 100644 --- a/Ombi/Ombi.Core/IdentityResolver/IUserIdentityManager.cs +++ b/Ombi/Ombi.Core/IdentityResolver/IUserIdentityManager.cs @@ -1,14 +1,14 @@ using System.Collections.Generic; using System.Threading.Tasks; -using Ombi.Store.Entities; +using Ombi.Core.Models; namespace Ombi.Core.IdentityResolver { public interface IUserIdentityManager { - Task CreateUser(User user); + Task CreateUser(UserDto user); Task CredentialsValid(string username, string password); - Task GetUser(string username); - Task> GetUsers(); + Task GetUser(string username); + Task> GetUsers(); } } \ No newline at end of file diff --git a/Ombi/Ombi.Core/IdentityResolver/UserIdentityManager.cs b/Ombi/Ombi.Core/IdentityResolver/UserIdentityManager.cs index 4f26b7027..4b40a8788 100644 --- a/Ombi/Ombi.Core/IdentityResolver/UserIdentityManager.cs +++ b/Ombi/Ombi.Core/IdentityResolver/UserIdentityManager.cs @@ -29,7 +29,9 @@ using System; using System.Collections.Generic; using System.Security.Cryptography; using System.Threading.Tasks; +using AutoMapper; using Microsoft.AspNetCore.Cryptography.KeyDerivation; +using Ombi.Core.Models; using Ombi.Store.Entities; using Ombi.Store.Repository; @@ -37,38 +39,43 @@ namespace Ombi.Core.IdentityResolver { public class UserIdentityManager : IUserIdentityManager { - public UserIdentityManager(IUserRepository userRepository) + public UserIdentityManager(IUserRepository userRepository, IMapper mapper) { UserRepository = userRepository; + Mapper = mapper; } + private IMapper Mapper { get; } private IUserRepository UserRepository { get; } public async Task CredentialsValid(string username, string password) { var user = await UserRepository.GetUser(username); - var hashedPass = HashPassword(password); + var hash = HashPassword(password, user.Salt); - return hashedPass.Equals(user.Password); + return hash.HashedPass.Equals(user.Password); } - public async Task GetUser(string username) + public async Task GetUser(string username) { - return await UserRepository.GetUser(username); + return Mapper.Map(await UserRepository.GetUser(username)); } - public async Task> GetUsers() + public async Task> GetUsers() { - return await UserRepository.GetUsers(); + return Mapper.Map>(await UserRepository.GetUsers()); } - public async Task CreateUser(User user) + public async Task CreateUser(UserDto userDto) { - user.Password = HashPassword(user.Password); + var user = Mapper.Map(userDto); + var result = HashPassword(user.Password); + user.Password = result.HashedPass; + user.Salt = result.Salt; await UserRepository.CreateUser(user); } - private string HashPassword(string password) + private UserHash HashPassword(string password) { // generate a 128-bit salt using a secure PRNG byte[] salt = new byte[128 / 8]; @@ -76,6 +83,12 @@ namespace Ombi.Core.IdentityResolver { rng.GetBytes(salt); } + return HashPassword(password, salt); + } + + + private UserHash HashPassword(string password, byte[] salt) + { // derive a 256-bit subkey (use HMACSHA1 with 10,000 iterations) var hashed = Convert.ToBase64String(KeyDerivation.Pbkdf2( password: password, @@ -84,7 +97,13 @@ namespace Ombi.Core.IdentityResolver iterationCount: 10000, numBytesRequested: 256 / 8)); - return hashed; + return new UserHash { HashedPass = hashed, Salt = salt }; + } + + private class UserHash + { + public string HashedPass { get; set; } + public byte[] Salt { get; set; } } } } \ No newline at end of file diff --git a/Ombi/Ombi.Core/Models/UserDto.cs b/Ombi/Ombi.Core/Models/UserDto.cs new file mode 100644 index 000000000..3ca6b5cf7 --- /dev/null +++ b/Ombi/Ombi.Core/Models/UserDto.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; +using System.Security.Claims; + +namespace Ombi.Core.Models +{ + public class UserDto + { + public int Id { get; set; } + public string Username { get; set; } + public string Alias { get; set; } + public List Claims { get; set; } + public string EmailAddress { get; set; } + public string Password { get; set; } + public byte[] Salt { get; set; } + public UserType UserType { get; set; } + } + public enum UserType + { + LocalUser = 1, + PlexUser = 2, + EmbyUser = 3, + } +} \ No newline at end of file diff --git a/Ombi/Ombi.Core/Ombi.Core.csproj b/Ombi/Ombi.Core/Ombi.Core.csproj index 0312ace74..d6bb44f88 100644 --- a/Ombi/Ombi.Core/Ombi.Core.csproj +++ b/Ombi/Ombi.Core/Ombi.Core.csproj @@ -6,6 +6,8 @@ + + diff --git a/Ombi/Ombi.DependencyInjection/IocExtensions.cs b/Ombi/Ombi.DependencyInjection/IocExtensions.cs index 596b810f3..6ccaca0f8 100644 --- a/Ombi/Ombi.DependencyInjection/IocExtensions.cs +++ b/Ombi/Ombi.DependencyInjection/IocExtensions.cs @@ -25,6 +25,7 @@ namespace Ombi.DependencyInjection services.RegisterApi(); services.RegisterServices(); services.RegisterStore(); + services.RegisterIdentity(); return services; } @@ -56,6 +57,7 @@ namespace Ombi.DependencyInjection public static IServiceCollection RegisterServices(this IServiceCollection services) { services.AddTransient(); + return services; } diff --git a/Ombi/Ombi.DependencyInjection/Ombi.DependencyInjection.csproj b/Ombi/Ombi.DependencyInjection/Ombi.DependencyInjection.csproj index a98f5c6aa..1e12ffd1a 100644 --- a/Ombi/Ombi.DependencyInjection/Ombi.DependencyInjection.csproj +++ b/Ombi/Ombi.DependencyInjection/Ombi.DependencyInjection.csproj @@ -5,8 +5,6 @@ - - diff --git a/Ombi/Ombi.Helpers/ClaimConverter.cs b/Ombi/Ombi.Helpers/ClaimConverter.cs new file mode 100644 index 000000000..d8603b9a4 --- /dev/null +++ b/Ombi/Ombi.Helpers/ClaimConverter.cs @@ -0,0 +1,33 @@ +using System; +using System.Security.Claims; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace Ombi.Helpers +{ + public class ClaimConverter : JsonConverter + { + public override bool CanConvert(Type objectType) + { + return (objectType == typeof(Claim)); + } + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + JObject jo = JObject.Load(reader); + string type = (string)jo["Type"]; + string value = (string)jo["Value"]; + string valueType = (string)jo["ValueType"]; + string issuer = (string)jo["Issuer"]; + string originalIssuer = (string)jo["OriginalIssuer"]; + return new Claim(type, value, valueType, issuer, originalIssuer); + } + + public override bool CanWrite => false; + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + throw new NotImplementedException(); + } + } +} \ No newline at end of file diff --git a/Ombi/Ombi.Helpers/Ombi.Helpers.csproj b/Ombi/Ombi.Helpers/Ombi.Helpers.csproj index 498e443c1..00920ab0c 100644 --- a/Ombi/Ombi.Helpers/Ombi.Helpers.csproj +++ b/Ombi/Ombi.Helpers/Ombi.Helpers.csproj @@ -8,4 +8,13 @@ + + + ..\..\..\..\..\..\..\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.6.1\System.IdentityModel.dll + + + ..\..\..\..\..\.nuget\packages\system.security.claims\4.3.0\ref\netstandard1.3\System.Security.Claims.dll + + + \ No newline at end of file diff --git a/Ombi/Ombi.Mapping.Maps/Ombi.Mapping.Maps.csproj b/Ombi/Ombi.Mapping.Maps/Ombi.Mapping.Maps.csproj new file mode 100644 index 000000000..19d3d3815 --- /dev/null +++ b/Ombi/Ombi.Mapping.Maps/Ombi.Mapping.Maps.csproj @@ -0,0 +1,19 @@ + + + + netstandard1.6 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Ombi/Ombi.Mapping.Maps/OmbiProfile.cs b/Ombi/Ombi.Mapping.Maps/OmbiProfile.cs new file mode 100644 index 000000000..b79d9f760 --- /dev/null +++ b/Ombi/Ombi.Mapping.Maps/OmbiProfile.cs @@ -0,0 +1,15 @@ +using System; +using AutoMapper; + +namespace Ombi.Mapping.Maps +{ + public class OmbiProfile : Profile + { + public OmbiProfile() + { + // Add as many of these lines as you need to map your objects + + } + } + +} diff --git a/Ombi/Ombi.Mapping/AutoMapperProfile.cs b/Ombi/Ombi.Mapping/AutoMapperProfile.cs new file mode 100644 index 000000000..5b3423389 --- /dev/null +++ b/Ombi/Ombi.Mapping/AutoMapperProfile.cs @@ -0,0 +1,20 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace Ombi.Mapping +{ + public static class AutoMapperProfile + { + public static IServiceCollection AddOmbiMappingProfile(this IServiceCollection services) + { + var config = new AutoMapper.MapperConfiguration(cfg => + { + cfg.AddProfile(new OmbiProfile()); + }); + + var mapper = config.CreateMapper(); + services.AddSingleton(mapper); + + return services; + } + } +} \ No newline at end of file diff --git a/Ombi/Ombi.Mapping/Ombi.Mapping.csproj b/Ombi/Ombi.Mapping/Ombi.Mapping.csproj new file mode 100644 index 000000000..ce044ce17 --- /dev/null +++ b/Ombi/Ombi.Mapping/Ombi.Mapping.csproj @@ -0,0 +1,19 @@ + + + + netstandard1.6 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Ombi/Ombi.Mapping/OmbiProfile.cs b/Ombi/Ombi.Mapping/OmbiProfile.cs new file mode 100644 index 000000000..6a2121106 --- /dev/null +++ b/Ombi/Ombi.Mapping/OmbiProfile.cs @@ -0,0 +1,15 @@ +using System; +using AutoMapper; +using Ombi.Core.Models; +using Ombi.Store.Entities; + +namespace Ombi.Mapping +{ + public class OmbiProfile : Profile + { + public OmbiProfile() + { + CreateMap().ReverseMap(); + } + } +} diff --git a/Ombi/Ombi.Store/Entities/User.cs b/Ombi/Ombi.Store/Entities/User.cs index 534d062a2..3b080da81 100644 --- a/Ombi/Ombi.Store/Entities/User.cs +++ b/Ombi/Ombi.Store/Entities/User.cs @@ -25,7 +25,12 @@ // ************************************************************************/ #endregion +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations.Schema; using System.Security.Claims; +using Newtonsoft.Json; +using Ombi.Helpers; + namespace Ombi.Store.Entities { @@ -33,10 +38,17 @@ namespace Ombi.Store.Entities { public string Username { get; set; } public string Alias { get; set; } - public Claim[] Claims { get; set; } + public string ClaimsSerialized { get; set; } public string EmailAddress { get; set; } public string Password { get; set; } + public byte[] Salt { get; set; } public UserType UserType { get; set; } + + [NotMapped] + public List Claims { + get => JsonConvert.DeserializeObject>(ClaimsSerialized, new ClaimConverter()); + set => ClaimsSerialized = JsonConvert.SerializeObject(value, new ClaimConverter()); + } } public enum UserType diff --git a/Ombi/Ombi.Store/Ombi.Store.csproj b/Ombi/Ombi.Store/Ombi.Store.csproj index d8eb4db16..6a83ce47c 100644 --- a/Ombi/Ombi.Store/Ombi.Store.csproj +++ b/Ombi/Ombi.Store/Ombi.Store.csproj @@ -6,7 +6,6 @@ - @@ -15,4 +14,7 @@ + + + \ No newline at end of file diff --git a/Ombi/Ombi.sln b/Ombi/Ombi.sln index bd42c7e32..4519f2dc4 100644 --- a/Ombi/Ombi.sln +++ b/Ombi/Ombi.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 -VisualStudioVersion = 15.0.26228.10 +VisualStudioVersion = 15.0.26403.0 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ombi", "Ombi\Ombi.csproj", "{C987AA67-AFE1-468F-ACD3-EAD5A48E1F6A}" EndProject @@ -26,6 +26,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ombi.Store", "Ombi.Store\Om EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ombi.DependencyInjection", "Ombi.DependencyInjection\Ombi.DependencyInjection.csproj", "{B39E4558-C557-48E7-AA74-19C5CD809617}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ombi.Mapping", "Ombi.Mapping\Ombi.Mapping.csproj", "{63E63511-1C7F-4162-8F92-8F7391B3C8A3}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Mappers", "Mappers", "{025FB189-2FFB-4F43-A64B-6F1B5A0D2065}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "DI", "DI", "{410F36CF-9C60-428A-B191-6FD90610991A}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -60,6 +66,10 @@ Global {B39E4558-C557-48E7-AA74-19C5CD809617}.Debug|Any CPU.Build.0 = Debug|Any CPU {B39E4558-C557-48E7-AA74-19C5CD809617}.Release|Any CPU.ActiveCfg = Release|Any CPU {B39E4558-C557-48E7-AA74-19C5CD809617}.Release|Any CPU.Build.0 = Release|Any CPU + {63E63511-1C7F-4162-8F92-8F7391B3C8A3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {63E63511-1C7F-4162-8F92-8F7391B3C8A3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {63E63511-1C7F-4162-8F92-8F7391B3C8A3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {63E63511-1C7F-4162-8F92-8F7391B3C8A3}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -67,5 +77,7 @@ Global GlobalSection(NestedProjects) = preSolution {132DA282-5894-4570-8916-D8C18ED2CE84} = {9293CA11-360A-4C20-A674-B9E794431BF5} {EA31F915-31F9-4318-B521-1500CDF40DDF} = {9293CA11-360A-4C20-A674-B9E794431BF5} + {B39E4558-C557-48E7-AA74-19C5CD809617} = {410F36CF-9C60-428A-B191-6FD90610991A} + {63E63511-1C7F-4162-8F92-8F7391B3C8A3} = {025FB189-2FFB-4F43-A64B-6F1B5A0D2065} EndGlobalSection EndGlobal diff --git a/Ombi/Ombi/Auth/TokenProviderMiddleware.cs b/Ombi/Ombi/Auth/TokenProviderMiddleware.cs index cf21abd08..5fcf82375 100644 --- a/Ombi/Ombi/Auth/TokenProviderMiddleware.cs +++ b/Ombi/Ombi/Auth/TokenProviderMiddleware.cs @@ -20,10 +20,11 @@ namespace Ombi.Auth private readonly RequestDelegate _next; private readonly TokenProviderOptions _options; private readonly JsonSerializerSettings _serializerSettings; + private readonly IUserIdentityManager _identityManager; public TokenProviderMiddleware( RequestDelegate next, - IOptions options) + IOptions options, IUserIdentityManager manager) { _next = next; _options = options.Value; @@ -33,6 +34,7 @@ namespace Ombi.Auth { Formatting = Formatting.Indented }; + _identityManager = manager; } public Task Invoke(HttpContext context) @@ -66,7 +68,7 @@ namespace Ombi.Auth userInfo = JsonConvert.DeserializeObject(body); } - var identity = await _options.IdentityResolver(userInfo.Username, userInfo.Password, new UserIdentityManager(new UserRepository(new OmbiContext()))); + var identity = await _options.IdentityResolver(userInfo.Username, userInfo.Password, _identityManager); if (identity == null) { context.Response.StatusCode = 400; diff --git a/Ombi/Ombi/Controllers/IdentityController.cs b/Ombi/Ombi/Controllers/IdentityController.cs new file mode 100644 index 000000000..30b47310c --- /dev/null +++ b/Ombi/Ombi/Controllers/IdentityController.cs @@ -0,0 +1,26 @@ +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Ombi.Core.IdentityResolver; +using Ombi.Core.Models; + +namespace Ombi.Controllers +{ + [Authorize] + public class IdentityController : BaseV1ApiController + { + public IdentityController(IUserIdentityManager identity) + { + IdentityManager = identity; + } + + private IUserIdentityManager IdentityManager { get; } + + [HttpGet] + public async Task GetUser() + { + return await IdentityManager.GetUser(this.HttpContext.User.Identity.Name); + } + + } +} diff --git a/Ombi/Ombi/Ombi.csproj b/Ombi/Ombi/Ombi.csproj index efeae93f4..b8336f669 100644 --- a/Ombi/Ombi/Ombi.csproj +++ b/Ombi/Ombi/Ombi.csproj @@ -8,16 +8,11 @@ - - auth - Copy.module.js - PreserveNewest - PreserveNewest - @@ -33,8 +28,7 @@ - - + @@ -52,6 +46,7 @@ + diff --git a/Ombi/Ombi/Startup.Auth.cs b/Ombi/Ombi/Startup.Auth.cs index 21ba6d509..3005dc941 100644 --- a/Ombi/Ombi/Startup.Auth.cs +++ b/Ombi/Ombi/Startup.Auth.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Security.Claims; using System.Security.Principal; using System.Text; @@ -8,6 +9,7 @@ using Microsoft.Extensions.Options; using Microsoft.IdentityModel.Tokens; using Ombi.Auth; using Ombi.Core.IdentityResolver; +using Ombi.Core.Models; namespace Ombi { @@ -53,23 +55,20 @@ namespace Ombi TokenValidationParameters = tokenValidationParameters }); - app.UseCookieAuthentication(new CookieAuthenticationOptions - { - AutomaticAuthenticate = true, - AutomaticChallenge = true, - AuthenticationScheme = "Cookie", - CookieName = Configuration.GetSection("TokenAuthentication:CookieName").Value, - TicketDataFormat = new CustomJwtDataFormat( - SecurityAlgorithms.HmacSha256, - tokenValidationParameters) - }); - app.UseMiddleware(Options.Create(tokenProviderOptions)); } private async Task GetIdentity(string username, string password, IUserIdentityManager userIdentityManager) { + //await userIdentityManager.CreateUser(new UserDto + //{ + // Username = "a", + // Password = "a", + // Claims = new List() { new Claim(ClaimTypes.Role, "Admin")}, + // UserType = UserType.LocalUser, + //}); + var validLogin = await userIdentityManager.CredentialsValid(username, password); if (!validLogin) { diff --git a/Ombi/Ombi/Startup.cs b/Ombi/Ombi/Startup.cs index 24ef16b31..9c153ba7a 100644 --- a/Ombi/Ombi/Startup.cs +++ b/Ombi/Ombi/Startup.cs @@ -1,24 +1,12 @@ -using System; -using System.IO; -using System.Linq; -using System.Text; -using IdentityServer4.EntityFramework.DbContexts; +using AutoMapper; using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Diagnostics; using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.StaticFiles; -using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.Logging; -using Microsoft.IdentityModel.Tokens; -using Newtonsoft.Json; -using Ombi.Auth; using Ombi.DependencyInjection; -using Ombi.Models; -using Ombi.Store.Context; +using Ombi.Mapping; namespace Ombi { @@ -42,6 +30,8 @@ namespace Ombi { // Add framework services. services.AddMvc(); + services.AddOmbiMappingProfile(); + services.AddAutoMapper(); services.RegisterDependencies(); // Ioc and EF } diff --git a/Ombi/Ombi/Views/Shared/_Layout.cshtml b/Ombi/Ombi/Views/Shared/_Layout.cshtml index 1cc395747..fafd4a355 100644 --- a/Ombi/Ombi/Views/Shared/_Layout.cshtml +++ b/Ombi/Ombi/Views/Shared/_Layout.cshtml @@ -3,7 +3,7 @@ - @ViewData["Title"] - Ombi + Ombi diff --git a/Ombi/Ombi/appsettings.json b/Ombi/Ombi/appsettings.json index b0f80f89d..f3d45d9cc 100644 --- a/Ombi/Ombi/appsettings.json +++ b/Ombi/Ombi/appsettings.json @@ -9,7 +9,7 @@ "SecretKey": "secretkey_secretkey123!", "Issuer": "DemoIssuer", "Audience": "DemoAudience", - "TokenPath": "/api/token/", + "TokenPath": "/api/v1/token/", "CookieName": "access_token" } } diff --git a/Ombi/Ombi/wwwroot/app/auth/auth.service.ts b/Ombi/Ombi/wwwroot/app/auth/auth.service.ts index 8bb2f69b3..98624fabe 100644 --- a/Ombi/Ombi/wwwroot/app/auth/auth.service.ts +++ b/Ombi/Ombi/wwwroot/app/auth/auth.service.ts @@ -13,7 +13,7 @@ import { Http } from '@angular/http'; @Injectable() export class AuthService extends ServiceHelpers { constructor(http: Http) { - super(http, '/api/token'); + super(http, '/api/v1/token'); } login(login:IUserLogin) : Observable {