diff --git a/src/Ombi.Core/IdentityResolver/IUserIdentityManager.cs b/src/Ombi.Core/IdentityResolver/IUserIdentityManager.cs deleted file mode 100644 index 0a47828bc..000000000 --- a/src/Ombi.Core/IdentityResolver/IUserIdentityManager.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Ombi.Core.Models; -using System.Collections.Generic; -using System.Threading.Tasks; - -namespace Ombi.Core.IdentityResolver -{ - public interface IUserIdentityManager - { - Task CreateUser(UserDto user); - - Task CredentialsValid(string username, string password); - - Task GetUser(string username); - Task GetUser(int userId); - - Task> GetUsers(); - - Task DeleteUser(UserDto user); - - Task UpdateUser(UserDto userDto); - } -} \ No newline at end of file diff --git a/src/Ombi.Core/IdentityResolver/OmbiProfileService.cs b/src/Ombi.Core/IdentityResolver/OmbiProfileService.cs new file mode 100644 index 000000000..301480b19 --- /dev/null +++ b/src/Ombi.Core/IdentityResolver/OmbiProfileService.cs @@ -0,0 +1,54 @@ +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; +using IdentityModel; +using IdentityServer4.Extensions; +using IdentityServer4.Models; +using IdentityServer4.Services; +using Microsoft.AspNetCore.Identity; +using Microsoft.EntityFrameworkCore; +using Ombi.Store.Entities; + +namespace Ombi.Core.IdentityResolver +{ + public class OmbiProfileService : IProfileService + { + public OmbiProfileService(UserManager um) + { + UserManager = um; + } + + private UserManager UserManager { get; } + + public async Task GetProfileDataAsync(ProfileDataRequestContext context) + { + + if (context.RequestedClaimTypes.Any()) + { + var user = await UserManager.Users.FirstOrDefaultAsync(x => x.UserName == context.Subject.GetSubjectId()); + if (user != null) + { + var roles = await UserManager.GetRolesAsync(user); + var claims = new List + { + new Claim(JwtClaimTypes.Name, user.UserName), + new Claim(JwtClaimTypes.Email, user.Email) + }; + + foreach (var role in roles) + { + claims.Add(new Claim(JwtClaimTypes.Role, role)); + } + context.AddFilteredClaims(claims); + context.IssuedClaims.AddRange(claims); + } + } + } + + public Task IsActiveAsync(IsActiveContext context) + { + return Task.FromResult(0); + } + } +} \ No newline at end of file diff --git a/src/Ombi.Core/IdentityResolver/ResourceOwnerPasswordValidator.cs b/src/Ombi.Core/IdentityResolver/ResourceOwnerPasswordValidator.cs new file mode 100644 index 000000000..015d5f19c --- /dev/null +++ b/src/Ombi.Core/IdentityResolver/ResourceOwnerPasswordValidator.cs @@ -0,0 +1,103 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; +using IdentityServer4.Models; +using IdentityServer4.Validation; +using Microsoft.AspNetCore.Identity; +using Microsoft.EntityFrameworkCore; +using Ombi.Api.Emby.Models; +using Ombi.Api.Plex; +using Ombi.Api.Plex.Models; +using Ombi.Core.Settings; +using Ombi.Core.Settings.Models.External; +using Ombi.Store.Entities; + +namespace Ombi.Core.IdentityResolver +{ + public class OmbiOwnerPasswordValidator : IResourceOwnerPasswordValidator + { + public OmbiOwnerPasswordValidator(UserManager um, IPlexApi api, + ISettingsService settings) + { + UserManager = um; + Api = api; + PlexSettings = settings; + } + + private UserManager UserManager { get; } + private IPlexApi Api { get; } + private ISettingsService PlexSettings { get; } + + public async Task ValidateAsync(ResourceOwnerPasswordValidationContext context) + { + var users = UserManager.Users; + if (await LocalUser(context, users)) + { + return; + } + if (await PlexUser(context, users)) + { + return; + } + if (await EmbyUser(context, users)) + { + return; + } + } + + private async Task PlexUser(ResourceOwnerPasswordValidationContext context, IQueryable users) + { + var signInResult = await Api.SignIn(new UserRequest {login = context.UserName, password = context.Password}); + if (signInResult.user == null) + { + return false; + } + + // Do we have a local user? + var user = await users.FirstOrDefaultAsync(x => x.UserName == context.UserName && x.UserType == UserType.PlexUser); + + throw new NotImplementedException(); // TODO finish + } + + private Task EmbyUser(ResourceOwnerPasswordValidationContext context, IQueryable users) + { + throw new NotImplementedException(); + } + + public async Task LocalUser(ResourceOwnerPasswordValidationContext context, IQueryable users) + { + var user = await users.FirstOrDefaultAsync(x => x.UserName == context.UserName && x.UserType == UserType.LocalUser); + + if (user == null) + { + //context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, "Username or password is incorrect"); + return false; + } + + var passwordValid = await UserManager.CheckPasswordAsync(user, context.Password); + if (!passwordValid) + { + context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, "Username or password is incorrect"); + return true; + + } + + var roles = await UserManager.GetRolesAsync(user); + var claims = new List + { + new Claim(ClaimTypes.Name, user.UserName), + new Claim(ClaimTypes.Email, user.Email) + }; + + foreach (var role in roles) + { + claims.Add(new Claim(ClaimTypes.Role, role)); + } + context.Result = new GrantValidationResult(user.UserName, "password", claims); + + return true; + } + } +} \ No newline at end of file diff --git a/src/Ombi.Core/IdentityResolver/UserIdentityManager.cs b/src/Ombi.Core/IdentityResolver/UserIdentityManager.cs deleted file mode 100644 index 37c0c2ab5..000000000 --- a/src/Ombi.Core/IdentityResolver/UserIdentityManager.cs +++ /dev/null @@ -1,114 +0,0 @@ -using AutoMapper; -using Hangfire; -using Microsoft.AspNetCore.Cryptography.KeyDerivation; -using Ombi.Core.Models; -using Ombi.Store.Entities; -using Ombi.Store.Repository; -using System; -using System.Collections.Generic; -using System.Security.Claims; -using System.Security.Cryptography; -using System.Threading.Tasks; - -namespace Ombi.Core.IdentityResolver -{ - public class UserIdentityManager : IUserIdentityManager - { - public UserIdentityManager(IUserRepository userRepository, IMapper mapper, ITokenRepository token) - { - UserRepository = userRepository; - Mapper = mapper; - TokenRepository = token; - } - - private IMapper Mapper { get; } - private IUserRepository UserRepository { get; } - private ITokenRepository TokenRepository { get; } - - public async Task CredentialsValid(string username, string password) - { - var user = await UserRepository.GetUser(username); - if (user == null) return false; - - var hash = HashPassword(password, user.Salt); - - return hash.HashedPass.Equals(user.Password); - } - - public async Task GetUser(string username) - { - return Mapper.Map(await UserRepository.GetUser(username)); - } - public async Task GetUser(int userId) - { - return Mapper.Map(await UserRepository.GetUser(userId)); - } - - public async Task> GetUsers() - { - return Mapper.Map>(await UserRepository.GetUsers()); - } - - public async Task CreateUser(UserDto userDto) - { - var user = Mapper.Map(userDto); - user.Claims.RemoveAll(x => x.Type == ClaimTypes.Country); // This is a hack around the Mapping Profile - var result = HashPassword(Guid.NewGuid().ToString("N")); // Since we do not allow the admin to set up the password. We send an email to the user - user.Password = result.HashedPass; - user.Salt = result.Salt; - await UserRepository.CreateUser(user); - - await TokenRepository.CreateToken(new EmailTokens - { - UserId = user.Id, - ValidUntil = DateTime.UtcNow.AddDays(7), - }); - - //BackgroundJob.Enqueue(() => ); - - return Mapper.Map(user); - } - - public async Task DeleteUser(UserDto user) - { - await UserRepository.DeleteUser(Mapper.Map(user)); - } - - public async Task UpdateUser(UserDto userDto) - { - userDto.Claims.RemoveAll(x => x.Type == ClaimTypes.Country); // This is a hack around the Mapping Profile - var user = Mapper.Map(userDto); - return Mapper.Map(await UserRepository.UpdateUser(user)); - } - - private UserHash HashPassword(string password) - { - // generate a 128-bit salt using a secure PRNG - var salt = new byte[128 / 8]; - using (var rng = RandomNumberGenerator.Create()) - { - 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, - salt, - KeyDerivationPrf.HMACSHA1, - 10000, - 256 / 8)); - - 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/src/Ombi.Core/Ombi.Core.csproj b/src/Ombi.Core/Ombi.Core.csproj index 38caf6f3d..025ca093d 100644 --- a/src/Ombi.Core/Ombi.Core.csproj +++ b/src/Ombi.Core/Ombi.Core.csproj @@ -35,4 +35,13 @@ + + + ..\..\..\..\..\.nuget\packages\identitymodel\2.8.1\lib\netstandard1.4\IdentityModel.dll + + + ..\..\..\..\..\.nuget\packages\identityserver4\1.5.2\lib\netstandard1.4\IdentityServer4.dll + + + \ No newline at end of file diff --git a/src/Ombi/Ombi.csproj b/src/Ombi/Ombi.csproj index fb9ddf459..693748cbf 100644 --- a/src/Ombi/Ombi.csproj +++ b/src/Ombi/Ombi.csproj @@ -19,7 +19,6 @@ - diff --git a/src/Ombi/Startup.cs b/src/Ombi/Startup.cs index 5c1f6fa71..3dc8b40ec 100644 --- a/src/Ombi/Startup.cs +++ b/src/Ombi/Startup.cs @@ -6,6 +6,8 @@ using AutoMapper.EquivalencyExpression; using Hangfire; using Hangfire.MemoryStorage; using Hangfire.SQLite; +using IdentityServer4.Services; +using IdentityServer4.Validation; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; @@ -21,6 +23,7 @@ using Microsoft.Extensions.Options; using Microsoft.Extensions.PlatformAbstractions; using Microsoft.IdentityModel.Tokens; using Ombi.Config; +using Ombi.Core.IdentityResolver; using Ombi.DependencyInjection; using Ombi.Mapping; using Ombi.Schedule; @@ -83,7 +86,9 @@ namespace Ombi .AddInMemoryIdentityResources(IdentityConfig.GetIdentityResources()) .AddInMemoryApiResources(IdentityConfig.GetApiResources()) .AddInMemoryClients(IdentityConfig.GetClients()) - .AddAspNetIdentity(); + .AddAspNetIdentity() + .Services.AddTransient() + .AddTransient(); services.Configure(options => { @@ -112,7 +117,7 @@ namespace Ombi { Version = "v1", Title = "Ombi Api", - Description = "The API for Ombi, most of these calls require an auth token that you can get from calling POST:\"api/v1/token/\" with the body of: \n {\n\"username\":\"YOURUSERNAME\",\n\"password\":\"YOURPASSWORD\"\n} \n" + + Description = "The API for Ombi, most of these calls require an auth token that you can get from calling POST:\"/connect/token/\" with the body of: \n {\n\"username\":\"YOURUSERNAME\",\n\"password\":\"YOURPASSWORD\"\n} \n" + "You can then use the returned token in the JWT Token field e.g. \"Bearer Token123xxff\"", Contact = new Contact { @@ -133,7 +138,7 @@ namespace Ombi Console.WriteLine(e); } - c.AddSecurityDefinition("Authentication",new ApiKeyScheme()); + c.AddSecurityDefinition("Authentication", new ApiKeyScheme()); c.OperationFilter(); c.DescribeAllParametersInCamelCase(); }); diff --git a/src/Ombi/package-lock.json b/src/Ombi/package-lock.json index bf1e8c95b..494366238 100644 --- a/src/Ombi/package-lock.json +++ b/src/Ombi/package-lock.json @@ -58,11 +58,6 @@ "resolved": "https://registry.npmjs.org/@angular/router/-/router-4.1.3.tgz", "integrity": "sha1-3a/UaufMyLH3SQT/tF85TkRiUhY=" }, - "@covalent/core": { - "version": "1.0.0-beta.4", - "resolved": "https://registry.npmjs.org/@covalent/core/-/core-1.0.0-beta.4.tgz", - "integrity": "sha1-Gn/qZg0JVmPJzqC0etWHJRMFBMI=" - }, "@ng-bootstrap/ng-bootstrap": { "version": "1.0.0-alpha.26", "resolved": "https://registry.npmjs.org/@ng-bootstrap/ng-bootstrap/-/ng-bootstrap-1.0.0-alpha.26.tgz",