#1456 #865 Started on allowing Plex Users to sign in through the new authentication server

pull/1488/head
Jamie.Rees 7 years ago
parent 0c38e42fec
commit 818acd6452

@ -1,22 +0,0 @@
using Ombi.Core.Models;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Ombi.Core.IdentityResolver
{
public interface IUserIdentityManager
{
Task<UserDto> CreateUser(UserDto user);
Task<bool> CredentialsValid(string username, string password);
Task<UserDto> GetUser(string username);
Task<UserDto> GetUser(int userId);
Task<IEnumerable<UserDto>> GetUsers();
Task DeleteUser(UserDto user);
Task<UserDto> UpdateUser(UserDto userDto);
}
}

@ -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<OmbiUser> um)
{
UserManager = um;
}
private UserManager<OmbiUser> 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<Claim>
{
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);
}
}
}

@ -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<OmbiUser> um, IPlexApi api,
ISettingsService<PlexSettings> settings)
{
UserManager = um;
Api = api;
PlexSettings = settings;
}
private UserManager<OmbiUser> UserManager { get; }
private IPlexApi Api { get; }
private ISettingsService<PlexSettings> 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<bool> PlexUser(ResourceOwnerPasswordValidationContext context, IQueryable<OmbiUser> 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<bool> EmbyUser(ResourceOwnerPasswordValidationContext context, IQueryable<OmbiUser> users)
{
throw new NotImplementedException();
}
public async Task<bool> LocalUser(ResourceOwnerPasswordValidationContext context, IQueryable<OmbiUser> 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<Claim>
{
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;
}
}
}

@ -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<bool> 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<UserDto> GetUser(string username)
{
return Mapper.Map<UserDto>(await UserRepository.GetUser(username));
}
public async Task<UserDto> GetUser(int userId)
{
return Mapper.Map<UserDto>(await UserRepository.GetUser(userId));
}
public async Task<IEnumerable<UserDto>> GetUsers()
{
return Mapper.Map<List<UserDto>>(await UserRepository.GetUsers());
}
public async Task<UserDto> CreateUser(UserDto userDto)
{
var user = Mapper.Map<User>(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<UserDto>(user);
}
public async Task DeleteUser(UserDto user)
{
await UserRepository.DeleteUser(Mapper.Map<User>(user));
}
public async Task<UserDto> UpdateUser(UserDto userDto)
{
userDto.Claims.RemoveAll(x => x.Type == ClaimTypes.Country); // This is a hack around the Mapping Profile
var user = Mapper.Map<User>(userDto);
return Mapper.Map<UserDto>(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; }
}
}
}

@ -35,4 +35,13 @@
<Folder Include="Models\Requests\Tv\" />
</ItemGroup>
<ItemGroup>
<Reference Include="IdentityModel">
<HintPath>..\..\..\..\..\.nuget\packages\identitymodel\2.8.1\lib\netstandard1.4\IdentityModel.dll</HintPath>
</Reference>
<Reference Include="IdentityServer4">
<HintPath>..\..\..\..\..\.nuget\packages\identityserver4\1.5.2\lib\netstandard1.4\IdentityServer4.dll</HintPath>
</Reference>
</ItemGroup>
</Project>

@ -19,7 +19,6 @@
<ItemGroup>
<!-- Files not to show in IDE -->
<Content Remove="package-lock.json" />
<Compile Remove="wwwroot\dist\**" />
<!-- Files not to publish (note that the 'dist' subfolders are re-added below) -->

@ -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<OmbiUser>();
.AddAspNetIdentity<OmbiUser>()
.Services.AddTransient<IResourceOwnerPasswordValidator, OmbiOwnerPasswordValidator>()
.AddTransient<IProfileService, OmbiProfileService>();
services.Configure<IdentityOptions>(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<SwaggerOperationFilter>();
c.DescribeAllParametersInCamelCase();
});

@ -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",

Loading…
Cancel
Save