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

This commit is contained in:
Jamie.Rees 2017-07-18 16:08:40 +01:00
parent 0c38e42fec
commit 818acd6452
8 changed files with 174 additions and 145 deletions

View file

@ -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);
}
}

View file

@ -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);
}
}
}

View file

@ -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;
}
}
}

View file

@ -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; }
}
}
}

View file

@ -35,4 +35,13 @@
<Folder Include="Models\Requests\Tv\" /> <Folder Include="Models\Requests\Tv\" />
</ItemGroup> </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> </Project>

View file

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

View file

@ -6,6 +6,8 @@ using AutoMapper.EquivalencyExpression;
using Hangfire; using Hangfire;
using Hangfire.MemoryStorage; using Hangfire.MemoryStorage;
using Hangfire.SQLite; using Hangfire.SQLite;
using IdentityServer4.Services;
using IdentityServer4.Validation;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
@ -21,6 +23,7 @@ using Microsoft.Extensions.Options;
using Microsoft.Extensions.PlatformAbstractions; using Microsoft.Extensions.PlatformAbstractions;
using Microsoft.IdentityModel.Tokens; using Microsoft.IdentityModel.Tokens;
using Ombi.Config; using Ombi.Config;
using Ombi.Core.IdentityResolver;
using Ombi.DependencyInjection; using Ombi.DependencyInjection;
using Ombi.Mapping; using Ombi.Mapping;
using Ombi.Schedule; using Ombi.Schedule;
@ -83,7 +86,9 @@ namespace Ombi
.AddInMemoryIdentityResources(IdentityConfig.GetIdentityResources()) .AddInMemoryIdentityResources(IdentityConfig.GetIdentityResources())
.AddInMemoryApiResources(IdentityConfig.GetApiResources()) .AddInMemoryApiResources(IdentityConfig.GetApiResources())
.AddInMemoryClients(IdentityConfig.GetClients()) .AddInMemoryClients(IdentityConfig.GetClients())
.AddAspNetIdentity<OmbiUser>(); .AddAspNetIdentity<OmbiUser>()
.Services.AddTransient<IResourceOwnerPasswordValidator, OmbiOwnerPasswordValidator>()
.AddTransient<IProfileService, OmbiProfileService>();
services.Configure<IdentityOptions>(options => services.Configure<IdentityOptions>(options =>
{ {
@ -112,7 +117,7 @@ namespace Ombi
{ {
Version = "v1", Version = "v1",
Title = "Ombi Api", 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\"", "You can then use the returned token in the JWT Token field e.g. \"Bearer Token123xxff\"",
Contact = new Contact Contact = new Contact
{ {
@ -133,7 +138,7 @@ namespace Ombi
Console.WriteLine(e); Console.WriteLine(e);
} }
c.AddSecurityDefinition("Authentication",new ApiKeyScheme()); c.AddSecurityDefinition("Authentication", new ApiKeyScheme());
c.OperationFilter<SwaggerOperationFilter>(); c.OperationFilter<SwaggerOperationFilter>();
c.DescribeAllParametersInCamelCase(); c.DescribeAllParametersInCamelCase();
}); });

View file

@ -58,11 +58,6 @@
"resolved": "https://registry.npmjs.org/@angular/router/-/router-4.1.3.tgz", "resolved": "https://registry.npmjs.org/@angular/router/-/router-4.1.3.tgz",
"integrity": "sha1-3a/UaufMyLH3SQT/tF85TkRiUhY=" "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": { "@ng-bootstrap/ng-bootstrap": {
"version": "1.0.0-alpha.26", "version": "1.0.0-alpha.26",
"resolved": "https://registry.npmjs.org/@ng-bootstrap/ng-bootstrap/-/ng-bootstrap-1.0.0-alpha.26.tgz", "resolved": "https://registry.npmjs.org/@ng-bootstrap/ng-bootstrap/-/ng-bootstrap-1.0.0-alpha.26.tgz",