Removed IdentityServer, it was overkill #865

pull/1488/head
Jamie.Rees 7 years ago
parent 7645aff996
commit 046211e017

@ -7,14 +7,12 @@ using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Security.Claims;
using System.Security.Principal;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Ombi.Core.Engine.Interfaces;
using Ombi.Core.IdentityResolver;
using Ombi.Core.Rule.Interfaces;
using Ombi.Store.Entities.Requests;

@ -1,53 +0,0 @@
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)
};
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);
}
}
}

@ -1,156 +0,0 @@
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 Microsoft.Extensions.Logging;
using Ombi.Api.Emby;
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.Helpers;
using Ombi.Settings.Settings.Models;
using Ombi.Store.Entities;
using Ombi.Store.Repository;
namespace Ombi.Core.IdentityResolver
{
public class OmbiOwnerPasswordValidator : IResourceOwnerPasswordValidator
{
public OmbiOwnerPasswordValidator(UserManager<OmbiUser> um, IPlexApi plexApi, IEmbyApi embyApi,
ISettingsService<PlexSettings> settings, ISettingsService<OmbiSettings> ombiSettings,
ISettingsService<EmbySettings> embySettings, IAuditRepository log)
{
UserManager = um;
PlexApi = plexApi;
PlexSettings = settings;
OmbiSettings = ombiSettings;
EmbyApi = embyApi;
EmbySettings = embySettings;
Audit = log;
}
private UserManager<OmbiUser> UserManager { get; }
private IPlexApi PlexApi { get; }
private IEmbyApi EmbyApi{ get; }
private ISettingsService<PlexSettings> PlexSettings { get; }
private ISettingsService<EmbySettings> EmbySettings { get; }
private ISettingsService<OmbiSettings> OmbiSettings { get; }
private IAuditRepository Audit { get; }
public async Task ValidateAsync(ResourceOwnerPasswordValidationContext context)
{
await Audit.Record(AuditType.None, AuditArea.Authentication, $"User {context.UserName} attempted to login", context.UserName);
var users = UserManager.Users;
if (await LocalUser(context, users))
{
return;
}
var ombi = await OmbiSettings.GetSettingsAsync();
if (ombi.AllowExternalUsersToAuthenticate)
{
if (await PlexUser(context, users))
{
return;
}
if (await EmbyUser(context, users))
{
return;
}
}
await Audit.Record(AuditType.Fail, AuditArea.Authentication, $"User {context.UserName} failed to login", context.UserName);
context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, "Username or password is incorrect");
}
private async Task<bool> PlexUser(ResourceOwnerPasswordValidationContext context, IQueryable<OmbiUser> users)
{
var signInResult = await PlexApi.SignIn(new UserRequest {login = context.UserName, password = context.Password});
if (signInResult?.user == null)
{
return false;
}
// Do we have a local user?
return await GetUserDetails(context, users, UserType.PlexUser);
}
private async Task<bool> EmbyUser(ResourceOwnerPasswordValidationContext context, IQueryable<OmbiUser> users)
{
var embySettings = await EmbySettings.GetSettingsAsync();
var signInResult = await EmbyApi.LogIn(context.UserName, context.Password, embySettings.ApiKey,
embySettings.FullUri);
if (string.IsNullOrEmpty(signInResult?.Name))
{
return false;
}
return await GetUserDetails(context, users, UserType.EmbyUser);
}
private async Task<bool> GetUserDetails(ResourceOwnerPasswordValidationContext context, IQueryable<OmbiUser> users, UserType userType)
{
var user = await users.FirstOrDefaultAsync(x => x.UserName == context.UserName && x.UserType == userType);
if (user != null)
{
var roles = await UserManager.GetRolesAsync(user);
var claims = new List<Claim>
{
new Claim(ClaimTypes.Name, user.UserName)
};
foreach (var role in roles)
{
claims.Add(new Claim(ClaimTypes.Role, role));
}
context.Result = new GrantValidationResult(user.UserName, "password", claims);
return true;
}
// Create the user?
return true;
}
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)
{
return false;
}
var passwordValid = await UserManager.CheckPasswordAsync(user, context.Password);
if (!passwordValid)
{
await Audit.Record(AuditType.Fail, AuditArea.Authentication, $"User {context.UserName} failed to login", context.UserName);
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)
};
foreach (var role in roles)
{
claims.Add(new Claim(ClaimTypes.Role, role));
}
context.Result = new GrantValidationResult(user.UserName, "password", claims);
await Audit.Record(AuditType.Success, AuditArea.Authentication, $"User {context.UserName} has logged in", context.UserName);
return true;
}
}
}

@ -9,7 +9,6 @@
<PackageReference Include="AutoMapper" Version="6.1.0" />
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="2.0.1" />
<PackageReference Include="Hangfire" Version="1.6.14" />
<PackageReference Include="IdentityServer4" Version="1.5.2" />
<PackageReference Include="Microsoft.AspNetCore.Cryptography.KeyDerivation" Version="1.1.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="1.1.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Design" Version="1.1.2" />

@ -14,7 +14,6 @@ using Ombi.Api.TvMaze;
using Ombi.Core;
using Ombi.Core.Engine;
using Ombi.Core.Engine.Interfaces;
using Ombi.Core.IdentityResolver;
using Ombi.Core.Models.Requests;
using Ombi.Core.Notifications;
using Ombi.Core.Rule;

@ -7,28 +7,21 @@ import { IUserLogin, ILocalUser } from './IUserLogin';
import { tokenNotExpired, JwtHelper } from 'angular2-jwt';
import { Http, Headers, URLSearchParams } from '@angular/http';
import { Http, Headers } from '@angular/http';
@Injectable()
export class AuthService extends ServiceHelpers {
constructor(http: Http) {
super(http, '/connect/token');
super(http, '/api/v1/token');
}
jwtHelper: JwtHelper = new JwtHelper();
login(login: IUserLogin): Observable<any> {
this.headers = new Headers();
this.headers.append('Content-Type', 'application/x-www-form-urlencoded');
let data = new URLSearchParams();
data.append('client_id', 'frontend');
data.append('scope', 'api');
data.append('client_secret', 'secret');
data.append('grant_type', 'password');
data.append('username', login.username);
data.append('password', login.password);
return this.http.post(`${this.url}/`, data.toString(), { headers: this.headers })
this.headers.append('Content-Type', 'application/json');
return this.http.post(`${this.url}/`, JSON.stringify(login), { headers: this.headers })
.map(this.extractData);
}

@ -1,4 +1,8 @@

<img class="landing-header" src="/images/logo.png" width="300" />
<div class="landing-block shadow">
<div class="media">
<div id="contentBody" class="media-body">
<h4 class="media-heading landing-title">Create the Admin account</h4>
<small>This account will be used to configure your settings and also manage all of the requests. Note: this should not be the same as your Plex/Emby account (you can change this later in the User Management Settings)</small>
<div class="form-group">
@ -17,4 +21,7 @@
<div style="text-align: center; margin-top: 20px">
<button (click)="createUser()" type="submit" class="btn btn-success-outline">Finish</button>
</div>
</div>
</div>
</div>
</div>

@ -1,4 +1,8 @@

<img class="landing-header" src="/images/logo.png" width="300" />
<div class="landing-block shadow">
<div class="media">
<div id="contentBody" class="media-body">
<h4 class="media-heading landing-title">Emby Authentication</h4>
<div *ngIf="embySettings">
<div class="form-group">
@ -29,4 +33,7 @@
<div style="text-align: center; margin-top: 20px">
<a (click)="save()" id="embyApiKeySave" class="btn btn-primary-outline">Next <div id="spinner"></div></a>
</div>
</div>
</div>
</div>
</div>

@ -1,21 +1,29 @@
 <div>
<h4 class="media-heading landing-title wizard-heading" id="statusTitle">Please choose your media server</h4>
<div class="form-group">
<div class="row">
<a (click)="emby()" id="embyImg">
<img class="wizard-img" src="/images/emby-logo-dark.jpg" />
</a>
</div>
<div class="row">
<a (click)="plex()" id="plexImg">
<img class="wizard-img" src="/images/plex-logo-reversed.png" />
</a>

<img class="landing-header" src="/images/logo.png" width="300" />
<div class="landing-block shadow">
<div class="media">
<div id="contentBody" class="media-body">
<h4 class="media-heading landing-title wizard-heading" id="statusTitle">Please choose your media server</h4>
<div class="form-group">
<div class="row">
<a (click)="emby()" id="embyImg">
<img class="wizard-img" src="/images/emby-logo-dark.jpg" />
</a>
</div>
<div class="row">
<a (click)="plex()" id="plexImg">
<img class="wizard-img" src="/images/plex-logo-reversed.png" />
</a>
</div>
<div class="row">
<button (click)="skip()" class="btn btn-primary-outline wizard-img" id="plexImg">
Skip
</button>
</div>
</div>
</div>
<div class="row">
<button (click)="skip()" class="btn btn-primary-outline wizard-img" id="plexImg">
Skip
</button>
</div>
</div>
</div>
</div>

@ -3,7 +3,6 @@ import { Router } from '@angular/router';
@Component({
templateUrl: './mediaserver.component.html',
})
export class MediaServerComponent {
@ -19,8 +18,7 @@ export class MediaServerComponent {
this.router.navigate(['Wizard/Emby']);
}
skip()
{
skip() {
this.router.navigate(['Wizard/CreateAdmin']);
}
}

@ -1,4 +1,9 @@
<h4 class="media-heading landing-title">Plex Authentication</h4>

<img class="landing-header" src="/images/logo.png" width="300" />
<div class="landing-block shadow">
<div class="media">
<div id="contentBody" class="media-body">
<h4 class="media-heading landing-title">Plex Authentication</h4>
<div class="form-group">
<label for="username" class="control-label">Username and Password</label>
<div>
@ -14,4 +19,7 @@
<div style="text-align: center; margin-top: 20px">
<button (click)="requestAuthToken()" class="btn btn-primary-outline">Request Token <i class="fa fa-key"></i></button>
</div>
</div>
</div>
</div>
</div>

@ -0,0 +1,106 @@
using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
using System.Linq;
using System.Security.Claims;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using Ombi.Models;
using Ombi.Models.Identity;
using Ombi.Store.Entities;
using Ombi.Store.Repository;
namespace Ombi.Controllers
{
[ApiV1]
public class TokenController
{
public TokenController(UserManager<OmbiUser> um, IOptions<TokenAuthentication> ta,
IApplicationConfigRepository config)
{
UserManager = um;
TokenAuthenticationOptions = ta.Value;
Config = config;
}
private TokenAuthentication TokenAuthenticationOptions { get; }
private IApplicationConfigRepository Config { get; }
private UserManager<OmbiUser> UserManager { get; }
/// <summary>
/// Gets the token.
/// </summary>
/// <param name="model">The model.</param>
/// <returns></returns>
[HttpPost]
public async Task<IActionResult> GetToken([FromBody] UserAuthModel model)
{
var user = await UserManager.FindByNameAsync(model.Username);
if (user == null)
{
return null;
}
// Verify Password
if ((await UserManager.CheckPasswordAsync(user, model.Password)))
{
// Get the url
var url = Config.Get(ConfigurationTypes.Url);
var port = Config.Get(ConfigurationTypes.Port);
#if !DEBUG
var audience = $"{url}:{port}";
#else
var audience = $"http://localhost:52038/";
#endif
var roles = await UserManager.GetRolesAsync(user);
var claims = new List<Claim>
{
new Claim(JwtRegisteredClaimNames.Sub, user.UserName),
new Claim("name", user.UserName),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())
};
claims.AddRange(roles.Select(role => new Claim("role", role)));
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(TokenAuthenticationOptions.SecretKey));
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
var token = new JwtSecurityToken(
claims: claims,
expires: DateTime.UtcNow.AddHours(5),
signingCredentials: creds,
audience: "Ombi", issuer:"Ombi"
);
return new JsonResult(new
{
access_token = new JwtSecurityTokenHandler().WriteToken(token),
expiration = token.ValidTo
});
}
return null;
}
/// <summary>
/// Refreshes the token.
/// </summary>
/// <param name="model">The model.</param>
/// <returns></returns>
/// <exception cref="NotImplementedException"></exception>
[HttpPost("refresh")]
public async Task<IActionResult> RefreshToken([FromBody] UserAuthModel model)
{
throw new NotImplementedException();
}
}
}

@ -1,59 +0,0 @@
using System.Collections.Generic;
using IdentityModel;
using IdentityServer4.Models;
namespace Ombi
{
public class IdentityConfig
{
// scopes define the resources in your system
public static IEnumerable<IdentityResource> GetIdentityResources()
{
return new List<IdentityResource>
{
new IdentityResources.OpenId(),
new IdentityResources.Profile(),
new IdentityResource {
Name = "role",
UserClaims = new List<string> {"role"}
}
};
}
public static IEnumerable<ApiResource> GetApiResources()
{
return new List<ApiResource>
{
new ApiResource("api", "API")
{
UserClaims = {JwtClaimTypes.Name, JwtClaimTypes.Role, JwtClaimTypes.Email, JwtClaimTypes.Id},
}
};
}
// clients want to access resources (aka scopes)
public static IEnumerable<Client> GetClients()
{
// client credentials client
return new List<Client>
{
new Client
{
ClientId = "frontend",
AllowedGrantTypes = GrantTypes.ResourceOwnerPassword,
ClientSecrets =
{
new Secret("secret".Sha256()) // TODO read up on what this actually is
},
AllowedScopes =
{
"api",
},
AccessTokenType = AccessTokenType.Jwt
}
};
}
}
}

@ -0,0 +1,7 @@
namespace Ombi.Models.Identity
{
public class TokenAuthentication
{
public string SecretKey { get; set; }
}
}

@ -43,9 +43,6 @@
<PackageReference Include="Hangfire.MemoryStorage.Core" Version="1.4.0" />
<PackageReference Include="Hangfire.RecurringJobExtensions" Version="1.1.6" />
<PackageReference Include="Hangfire.SQLite.Core" Version="1.0.2" />
<PackageReference Include="IdentityServer4" Version="1.5.2" />
<PackageReference Include="IdentityServer4.AccessTokenValidation" Version="1.2.1" />
<PackageReference Include="IdentityServer4.AspNetIdentity" Version="1.0.1" />
<PackageReference Include="Microsoft.AspNetCore" Version="1.1.2" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="1.1.2" />
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="1.1.3" />

@ -2,16 +2,13 @@
using System.IO;
using System.Linq;
using System.Security.Principal;
using System.Text;
using AutoMapper;
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.Hosting.Server.Features;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.AspNetCore.SpaServices.Webpack;
@ -25,10 +22,10 @@ using Microsoft.Extensions.Options;
using Microsoft.Extensions.PlatformAbstractions;
using Microsoft.IdentityModel.Tokens;
using Ombi.Config;
using Ombi.Core.IdentityResolver;
using Ombi.DependencyInjection;
using Ombi.Helpers;
using Ombi.Mapping;
using Ombi.Models.Identity;
using Ombi.Schedule;
using Ombi.Store.Context;
using Ombi.Store.Entities;
@ -83,15 +80,16 @@ namespace Ombi
.AddEntityFrameworkStores<OmbiContext>()
.AddDefaultTokenProviders();
services.AddIdentityServer()
.AddTemporarySigningCredential()
.AddInMemoryPersistedGrants()
.AddInMemoryIdentityResources(IdentityConfig.GetIdentityResources())
.AddInMemoryApiResources(IdentityConfig.GetApiResources())
.AddInMemoryClients(IdentityConfig.GetClients())
.AddAspNetIdentity<OmbiUser>()
.Services.AddTransient<IResourceOwnerPasswordValidator, OmbiOwnerPasswordValidator>()
.AddTransient<IProfileService, OmbiProfileService>();
//services.AddIdentityServer()
// .AddTemporarySigningCredential()
// .AddInMemoryPersistedGrants()
// .AddInMemoryIdentityResources(IdentityConfig.GetIdentityResources())
// .AddInMemoryApiResources(IdentityConfig.GetApiResources())
// .AddInMemoryClients(IdentityConfig.GetClients())
// .AddAspNetIdentity<OmbiUser>()
// .Services.AddTransient<IResourceOwnerPasswordValidator, OmbiOwnerPasswordValidator>()
// .AddTransient<IProfileService, OmbiProfileService>();
services.Configure<IdentityOptions>(options =>
{
@ -151,10 +149,9 @@ namespace Ombi
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddScoped<IPrincipal>(sp => sp.GetService<IHttpContextAccessor>().HttpContext.User);
//services.Configure<TokenAuthenticationOptions>(Configuration.GetSection("TokenAuthentication"));
services.Configure<ApplicationSettings>(Configuration.GetSection("ApplicationSettings"));
services.Configure<UserSettings>(Configuration.GetSection("UserSettings"));
services.Configure<TokenAuthentication>(Configuration.GetSection("TokenAuthentication"));
services.Configure<LandingPageBackground>(Configuration.GetSection("LandingPageBackground"));
services.AddHangfire(x =>
@ -179,8 +176,8 @@ namespace Ombi
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory, IMemoryCache cache)
{
var options = (IOptions<UserSettings>) app.ApplicationServices.GetService(
typeof(IOptions<UserSettings>));
var tokenOptions = (IOptions<TokenAuthentication>)app.ApplicationServices.GetService(
typeof(IOptions<TokenAuthentication>));
var ctx = (IOmbiContext)app.ApplicationServices.GetService(typeof(IOmbiContext));
@ -190,26 +187,54 @@ namespace Ombi
Console.WriteLine($"Using Url {url.Value}:{port.Value} for Identity Server");
app.UseIdentity();
app.UseIdentityServer();
app.UseIdentityServerAuthentication(new IdentityServerAuthenticationOptions
{
#if !DEBUG
Authority = $"{url.Value}:{port.Value}",
var audience = $"{url.Value}:{port.Value}";
#else
Authority = $"http://localhost:52038/",
var audience = $"http://localhost:52038/";
#endif
ApiName = "api",
ApiSecret = "secret",
EnableCaching = true,
CacheDuration = TimeSpan.FromMinutes(10), // that's the default
RequireHttpsMetadata = options.Value.UseHttps, // FOR DEV set to false
AutomaticAuthenticate = true,
AutomaticChallenge = true,
var tokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(tokenOptions.Value.SecretKey)),
RequireExpirationTime = true,
ValidateLifetime = true,
ValidAudience = "Ombi",
ValidIssuer = "Ombi",
ClockSkew = TimeSpan.Zero
};
app.UseJwtBearerAuthentication(new JwtBearerOptions()
{
Audience = "Ombi",
AutomaticAuthenticate = true,
TokenValidationParameters = tokenValidationParameters
});
// app.UseIdentityServer();
// app.UseIdentityServerAuthentication(new IdentityServerAuthenticationOptions
// {
//#if !DEBUG
// Authority = $"{url.Value}:{port.Value}",
//#else
// Authority = $"http://localhost:52038/",
//#endif
// ApiName = "api",
// ApiSecret = "secret",
// EnableCaching = true,
// CacheDuration = TimeSpan.FromMinutes(10), // that's the default
// RequireHttpsMetadata = options.Value.UseHttps, // FOR DEV set to false
// AutomaticAuthenticate = true,
// AutomaticChallenge = true,
// });
loggerFactory.AddSerilog();
if (env.IsDevelopment())

@ -16,11 +16,7 @@
"UseHttps": false
},
"TokenAuthentication": {
"SecretKey": "secretkey_secretkey123!",
"Issuer": "OmbiIssuer",
"Audience": "OmbiAudience",
"TokenPath": "/api/v1/token/",
"CookieName": "access_token"
"SecretKey": "secretkey_secretkey123!"
},
"LandingPageBackground": {
"Movies": [

Loading…
Cancel
Save