Added support for per site options and OpenID Connect

This commit is contained in:
Shaun Walker
2022-03-13 22:55:52 -04:00
parent a47ecbdea9
commit 9bbbff31f8
31 changed files with 1064 additions and 180 deletions

View File

@ -0,0 +1,18 @@
using Microsoft.AspNetCore.Http;
using Oqtane.Models;
using Oqtane.Shared;
namespace Oqtane.Extensions
{
public static class HttpContextExtensions
{
public static Alias GetAlias(this HttpContext context)
{
if (context != null && context.Items.ContainsKey(Constants.HttpContextAliasKey))
{
return context.Items[Constants.HttpContextAliasKey] as Alias;
}
return null;
}
}
}

View File

@ -15,6 +15,7 @@ using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.OpenApi.Models;
using Oqtane.Infrastructure;
using Oqtane.Models;
using Oqtane.Modules;
using Oqtane.Repository;
using Oqtane.Security;
@ -58,6 +59,12 @@ namespace Microsoft.Extensions.DependencyInjection
return services;
}
public static OqtaneSiteOptionsBuilder<T> AddOqtaneSiteOptions<T>(this IServiceCollection services)
where T : class, IAlias, new()
{
return new OqtaneSiteOptionsBuilder<T>(services);
}
internal static IServiceCollection AddOqtaneSingletonServices(this IServiceCollection services)
{
services.AddSingleton<IInstallationManager, InstallationManager>();
@ -71,6 +78,8 @@ namespace Microsoft.Extensions.DependencyInjection
internal static IServiceCollection AddOqtaneTransientServices(this IServiceCollection services)
{
services.AddTransient<ITenantManager, TenantManager>();
services.AddTransient<IAliasAccessor, AliasAccessor>();
services.AddTransient<IModuleDefinitionRepository, ModuleDefinitionRepository>();
services.AddTransient<IThemeRepository, ThemeRepository>();
services.AddTransient<IUserPermissions, UserPermissions>();
@ -124,6 +133,11 @@ namespace Microsoft.Extensions.DependencyInjection
context.Response.StatusCode = (int)HttpStatusCode.Forbidden;
return Task.CompletedTask;
};
options.Events.OnRedirectToLogout = context =>
{
context.Response.StatusCode = (int)HttpStatusCode.Forbidden;
return Task.CompletedTask;
};
options.Events.OnValidatePrincipal = PrincipalValidator.ValidateAsync;
});
@ -314,7 +328,7 @@ namespace Microsoft.Extensions.DependencyInjection
try
{
Assembly assembly = AssemblyLoadContext.Default.LoadFromStream(new MemoryStream(File.ReadAllBytes(assemblyFile.FullName)));
Assembly assembly = AssemblyLoadContext.Default.LoadFromStream(new MemoryStream(System.IO.File.ReadAllBytes(assemblyFile.FullName)));
Debug.WriteLine($"Oqtane Info: Loaded Assembly {assemblyName}");
}
catch (Exception ex)
@ -333,9 +347,9 @@ namespace Microsoft.Extensions.DependencyInjection
private static Assembly ResolveDependencies(AssemblyLoadContext context, AssemblyName name)
{
var assemblyPath = Path.GetDirectoryName(Assembly.GetEntryAssembly()?.Location) + Path.DirectorySeparatorChar + name.Name + ".dll";
if (File.Exists(assemblyPath))
if (System.IO.File.Exists(assemblyPath))
{
return context.LoadFromStream(new MemoryStream(File.ReadAllBytes(assemblyPath)));
return context.LoadFromStream(new MemoryStream(System.IO.File.ReadAllBytes(assemblyPath)));
}
else
{

View File

@ -0,0 +1,233 @@
using System;
using System.Linq;
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Oqtane.Infrastructure;
using Oqtane.Models;
using Oqtane.Shared;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using Oqtane.Repository;
using System.IO;
using System.Collections.Generic;
using Oqtane.Security;
namespace Oqtane.Extensions
{
public static class OqtaneSiteAuthenticationBuilderExtensions
{
public static OqtaneSiteOptionsBuilder<TAlias> WithSiteAuthentication<TAlias>(
this OqtaneSiteOptionsBuilder<TAlias> builder)
where TAlias : class, IAlias, new()
{
builder.WithSiteAuthenticationCore();
builder.WithSiteAuthenticationOptions();
return builder;
}
public static OqtaneSiteOptionsBuilder<TAlias> WithSiteAuthenticationCore<TAlias>(
this OqtaneSiteOptionsBuilder<TAlias> builder)
where TAlias : class, IAlias, new()
{
builder.Services.DecorateService<IAuthenticationService, SiteAuthenticationService<TAlias>>();
builder.Services.Replace(ServiceDescriptor.Singleton<IAuthenticationSchemeProvider, SiteAuthenticationSchemeProvider>());
return builder;
}
public static OqtaneSiteOptionsBuilder<TAlias> WithSiteAuthenticationOptions<TAlias>(
this OqtaneSiteOptionsBuilder<TAlias> builder)
where TAlias : class, IAlias, new()
{
// site OpenIdConnect options
builder.AddSiteOptions<OpenIdConnectOptions>((options, alias) =>
{
if (alias.SiteSettings.ContainsKey("OpenIdConnectOptions:Authority"))
{
options.Authority = alias.SiteSettings["OpenIdConnectOptions:Authority"];
}
if (alias.SiteSettings.ContainsKey("OpenIdConnectOptions:ClientId"))
{
options.ClientId = alias.SiteSettings["OpenIdConnectOptions:ClientId"];
}
if (alias.SiteSettings.ContainsKey("OpenIdConnectOptions:ClientSecret"))
{
options.ClientSecret = alias.SiteSettings["OpenIdConnectOptions:ClientSecret"];
}
// default options
options.SignInScheme = Constants.AuthenticationScheme; // identity cookie
options.RequireHttpsMetadata = true;
options.UsePkce = true;
options.Scope.Add("openid"); // core claims
options.Scope.Add("profile"); // name claims
options.Scope.Add("email"); // email claim
//options.Scope.Add("offline_access"); // get refresh token
options.SaveTokens = true;
options.GetClaimsFromUserInfoEndpoint = true;
options.CallbackPath = string.IsNullOrEmpty(alias.Path) ? "/signin-oidc" : "/" + alias.Path + "/signin-oidc";
options.ResponseType = OpenIdConnectResponseType.Code;
options.Events.OnTokenValidated = OnTokenValidated;
});
// site ChallengeScheme options
builder.AddSiteOptions<AuthenticationOptions>((options, alias) =>
{
if (alias.SiteSettings.ContainsKey("OpenIdConnectOptions:Authority") && !string.IsNullOrEmpty(alias.SiteSettings["OpenIdConnectOptions:Authority"]))
{
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
}
});
return builder;
}
private static async Task OnTokenValidated(TokenValidatedContext context)
{
var email = context.Principal.Identity.Name;
if (email != null)
{
var _identityUserManager = context.HttpContext.RequestServices.GetRequiredService<UserManager<IdentityUser>>();
var _users = context.HttpContext.RequestServices.GetRequiredService<IUserRepository>();
var _userRoles = context.HttpContext.RequestServices.GetRequiredService<IUserRoleRepository>();
User user = null;
var identityuser = await _identityUserManager.FindByEmailAsync(email);
if (identityuser == null)
{
identityuser = new IdentityUser();
identityuser.UserName = email;
identityuser.Email = email;
identityuser.EmailConfirmed = true;
var result = await _identityUserManager.CreateAsync(identityuser, Guid.NewGuid().ToString("N") + "-Xx!");
if (result.Succeeded)
{
user = new User();
user.SiteId = context.HttpContext.GetAlias().SiteId;
user.Username = email;
user.DisplayName = email;
user.Email = email;
user.LastLoginOn = null;
user.LastIPAddress = "";
user = _users.AddUser(user);
// add folder for user
var _folders = context.HttpContext.RequestServices.GetRequiredService<IFolderRepository>();
Folder folder = _folders.GetFolder(user.SiteId, Utilities.PathCombine("Users", Path.DirectorySeparatorChar.ToString()));
if (folder != null)
{
_folders.AddFolder(new Folder
{
SiteId = folder.SiteId,
ParentId = folder.FolderId,
Name = "My Folder",
Type = FolderTypes.Private,
Path = Utilities.PathCombine(folder.Path, user.UserId.ToString(), Path.DirectorySeparatorChar.ToString()),
Order = 1,
ImageSizes = "",
Capacity = Constants.UserFolderCapacity,
IsSystem = true,
Permissions = new List<Permission>
{
new Permission(PermissionNames.Browse, user.UserId, true),
new Permission(PermissionNames.View, RoleNames.Everyone, true),
new Permission(PermissionNames.Edit, user.UserId, true)
}.EncodePermissions()
});
}
// add auto assigned roles to user for site
var _roles = context.HttpContext.RequestServices.GetRequiredService<IRoleRepository>();
List<Role> roles = _roles.GetRoles(user.SiteId).Where(item => item.IsAutoAssigned).ToList();
foreach (Role role in roles)
{
UserRole userrole = new UserRole();
userrole.UserId = user.UserId;
userrole.RoleId = role.RoleId;
userrole.EffectiveDate = null;
userrole.ExpiryDate = null;
_userRoles.AddUserRole(userrole);
}
}
}
else
{
email = identityuser.UserName;
}
// add claims to principal
user = _users.GetUser(email);
if (user != null)
{
List<UserRole> userroles = _userRoles.GetUserRoles(user.UserId, context.HttpContext.GetAlias().SiteId).ToList();
var identity = UserSecurity.CreateClaimsIdentity(context.HttpContext.GetAlias(), user, userroles);
var principalIdentity = (ClaimsIdentity)context.Principal.Identity;
foreach (var claim in identity.Claims)
{
if (!principalIdentity.Claims.Contains(claim))
{
principalIdentity.AddClaim(claim);
}
}
}
}
else
{
var _logger = context.HttpContext.RequestServices.GetRequiredService<ILogManager>();
_logger.Log(LogLevel.Information, "OqtaneSiteAuthenticationBuilderExtensions", Enums.LogFunction.Security, "OpenId Connect Server Did Not Return An Email For User");
}
}
public static bool DecorateService<TService, TImpl>(this IServiceCollection services, params object[] parameters)
{
var existingService = services.SingleOrDefault(s => s.ServiceType == typeof(TService));
if (existingService == null)
return false;
var newService = new ServiceDescriptor(existingService.ServiceType,
sp =>
{
TService inner = (TService)ActivatorUtilities.CreateInstance(sp, existingService.ImplementationType!);
var parameters2 = new object[parameters.Length + 1];
Array.Copy(parameters, 0, parameters2, 1, parameters.Length);
parameters2[0] = inner;
return ActivatorUtilities.CreateInstance<TImpl>(sp, parameters2)!;
},
existingService.Lifetime);
if (existingService.ImplementationInstance != null)
{
newService = new ServiceDescriptor(existingService.ServiceType,
sp =>
{
TService inner = (TService)existingService.ImplementationInstance;
return ActivatorUtilities.CreateInstance<TImpl>(sp, inner, parameters)!;
},
existingService.Lifetime);
}
else if (existingService.ImplementationFactory != null)
{
newService = new ServiceDescriptor(existingService.ServiceType,
sp =>
{
TService inner = (TService)existingService.ImplementationFactory(sp);
return ActivatorUtilities.CreateInstance<TImpl>(sp, inner, parameters)!;
},
existingService.Lifetime);
}
services.Remove(existingService);
services.Add(newService);
return true;
}
}
}

View File

@ -0,0 +1,57 @@
using Microsoft.Extensions.DependencyInjection;
using Oqtane.Models;
using Microsoft.AspNetCore.Identity;
using System;
namespace Oqtane.Extensions
{
public static class OqtaneSiteIdentityBuilderExtensions
{
public static OqtaneSiteOptionsBuilder<TAlias> WithSiteIdentity<TAlias>(
this OqtaneSiteOptionsBuilder<TAlias> builder)
where TAlias : class, IAlias, new()
{
// site identity options
builder.AddSiteOptions<IdentityOptions>((options, alias) =>
{
// password options
if (alias.SiteSettings.ContainsKey("IdentityOptions:Password:RequiredLength"))
{
options.Password.RequiredLength = int.Parse(alias.SiteSettings["IdentityOptions:Password:RequiredLength"]);
}
if (alias.SiteSettings.ContainsKey("IdentityOptions:Password:RequiredUniqueChars"))
{
options.Password.RequiredUniqueChars = int.Parse(alias.SiteSettings["IdentityOptions:Password:RequiredUniqueChars"]);
}
if (alias.SiteSettings.ContainsKey("IdentityOptions:Password:RequireDigit"))
{
options.Password.RequireDigit = bool.Parse(alias.SiteSettings["IdentityOptions:Password:RequireDigit"]);
}
if (alias.SiteSettings.ContainsKey("IdentityOptions:Password:RequireUppercase"))
{
options.Password.RequireUppercase = bool.Parse(alias.SiteSettings["IdentityOptions:Password:RequireUppercase"]);
}
if (alias.SiteSettings.ContainsKey("IdentityOptions:Password:RequireLowercase"))
{
options.Password.RequireLowercase = bool.Parse(alias.SiteSettings["IdentityOptions:Password:RequireLowercase"]);
}
if (alias.SiteSettings.ContainsKey("IdentityOptions:Password:RequireNonAlphanumeric"))
{
options.Password.RequireNonAlphanumeric = bool.Parse(alias.SiteSettings["IdentityOptions:Password:RequireNonAlphanumeric"]);
}
// lockout options
if (alias.SiteSettings.ContainsKey("IdentityOptions:Password:MaxFailedAccessAttempts"))
{
options.Lockout.MaxFailedAccessAttempts = int.Parse(alias.SiteSettings["IdentityOptions:Password:MaxFailedAccessAttempts"]);
}
if (alias.SiteSettings.ContainsKey("IdentityOptions:Password:DefaultLockoutTimeSpan"))
{
options.Lockout.DefaultLockoutTimeSpan = TimeSpan.Parse(alias.SiteSettings["IdentityOptions:Password:DefaultLockoutTimeSpan"]);
}
});
return builder;
}
}
}

View File

@ -0,0 +1,39 @@
using System;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Options;
using Oqtane.Infrastructure;
using Oqtane.Models;
namespace Microsoft.Extensions.DependencyInjection
{
public partial class OqtaneSiteOptionsBuilder<TSiteOptions> where TSiteOptions : class, IAlias, new()
{
public IServiceCollection Services { get; set; }
public OqtaneSiteOptionsBuilder(IServiceCollection services)
{
Services = services;
}
public OqtaneSiteOptionsBuilder<TSiteOptions> AddSiteOptions<TOptions>(
Action<TOptions, TSiteOptions> siteOptions) where TOptions : class, new()
{
Services.TryAddSingleton<IOptionsMonitorCache<TOptions>, SiteOptionsCache<TOptions, TSiteOptions>>();
Services.AddSingleton<ISiteOptions<TOptions, TSiteOptions>, SiteOptions<TOptions, TSiteOptions>>
(sp => new SiteOptions<TOptions, TSiteOptions>(siteOptions));
Services.TryAddTransient<IOptionsFactory<TOptions>, SiteOptionsFactory<TOptions, TSiteOptions>>();
Services.TryAddScoped<IOptionsSnapshot<TOptions>>(sp => BuildOptionsManager<TOptions>(sp));
Services.TryAddSingleton<IOptions<TOptions>>(sp => BuildOptionsManager<TOptions>(sp));
return this;
}
private static SiteOptionsManager<TOptions> BuildOptionsManager<TOptions>(IServiceProvider sp)
where TOptions : class, new()
{
var cache = ActivatorUtilities.CreateInstance(sp, typeof(SiteOptionsCache<TOptions, TSiteOptions>));
return (SiteOptionsManager<TOptions>)ActivatorUtilities.CreateInstance(sp, typeof(SiteOptionsManager<TOptions>), new[] { cache });
}
}
}

View File

@ -1,7 +1,5 @@
using System.Collections.Generic;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.StaticFiles;
using Oqtane.Models;
namespace Oqtane.Extensions
{