mirror of
https://github.com/Ombi-app/Ombi.git
synced 2025-07-13 00:32:57 -07:00
This commit is contained in:
parent
0c38e42fec
commit
818acd6452
8 changed files with 174 additions and 145 deletions
|
@ -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);
|
||||
}
|
||||
}
|
54
src/Ombi.Core/IdentityResolver/OmbiProfileService.cs
Normal file
54
src/Ombi.Core/IdentityResolver/OmbiProfileService.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
103
src/Ombi.Core/IdentityResolver/ResourceOwnerPasswordValidator.cs
Normal file
103
src/Ombi.Core/IdentityResolver/ResourceOwnerPasswordValidator.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
});
|
||||
|
|
5
src/Ombi/package-lock.json
generated
5
src/Ombi/package-lock.json
generated
|
@ -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…
Add table
Add a link
Reference in a new issue