KeyPressed(e))">
+ @if (PageState.Site.Settings.ContainsKey("OpenIdConnectOptions:Provider") && !string.IsNullOrEmpty(PageState.Site.Settings["OpenIdConnectOptions:Provider"]))
+ {
+
+
+ }
-
+
@@ -146,14 +168,18 @@ else
private string _search;
private string _allowregistration;
- private string _minimumlength = "6";
- private string _uniquecharacters = "1";
- private string _requiredigit = "true";
- private string _requireupper = "true";
- private string _requirelower = "true";
- private string _requirepunctuation = "true";
- private string _maximumfailures = "5";
- private string _lockoutduration = "5";
+ private string _minimumlength;
+ private string _uniquecharacters;
+ private string _requiredigit;
+ private string _requireupper;
+ private string _requirelower;
+ private string _requirepunctuation;
+ private string _maximumfailures;
+ private string _lockoutduration;
+ private string _provider;
+ private string _authority;
+ private string _clientid;
+ private string _clientsecret;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin;
@@ -164,21 +190,19 @@ else
userroles = Search(_search);
_allowregistration = PageState.Site.AllowRegistration.ToString();
- if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
- {
- Dictionary
systeminfo = await SystemService.GetSystemInfoAsync();
- if (systeminfo != null)
- {
- _minimumlength = systeminfo["Password:RequiredLength"].ToString();
- _uniquecharacters = systeminfo["Password:RequiredUniqueChars"].ToString();
- _requiredigit = systeminfo["Password:RequireDigit"].ToString();
- _requireupper = systeminfo["Password:RequireUppercase"].ToString();
- _requirelower = systeminfo["Password:RequireLowercase"].ToString();
- _requirepunctuation = systeminfo["Password:RequireNonAlphanumeric"].ToString();
- _maximumfailures = systeminfo["Lockout:MaxFailedAccessAttempts"].ToString();
- _lockoutduration = TimeSpan.Parse(systeminfo["Lockout:DefaultLockoutTimeSpan"].ToString()).TotalMinutes.ToString();
- }
- }
+ var settings = await SettingService.GetSiteSettingsAsync(PageState.Site.SiteId);
+ _minimumlength = SettingService.GetSetting(settings, "IdentityOptions:Password:RequiredLength", "6");
+ _uniquecharacters = SettingService.GetSetting(settings, "IdentityOptions:Password:RequiredUniqueChars", "1");
+ _requiredigit = SettingService.GetSetting(settings, "IdentityOptions:Password:RequireDigit", "true");
+ _requireupper = SettingService.GetSetting(settings, "IdentityOptions:Password:RequireUppercase", "true");
+ _requirelower = SettingService.GetSetting(settings, "IdentityOptions:Password:RequireLowercase", "true");
+ _requirepunctuation = SettingService.GetSetting(settings, "IdentityOptions:Password:RequireNonAlphanumeric", "true");
+ _maximumfailures = SettingService.GetSetting(settings, "IdentityOptions:Lockout:MaxFailedAccessAttempts", "5");
+ _lockoutduration = TimeSpan.Parse(SettingService.GetSetting(settings, "IdentityOptions:Lockout:DefaultLockoutTimeSpan", "00:05:00")).TotalMinutes.ToString();
+ _provider = SettingService.GetSetting(settings, "OpenIdConnectOptions:Provider", "");
+ _authority = SettingService.GetSetting(settings, "OpenIdConnectOptions:Authority", "");
+ _clientid = SettingService.GetSetting(settings, "OpenIdConnectOptions:ClientId", "");
+ _clientsecret = SettingService.GetSetting(settings, "OpenIdConnectOptions:ClientSecret", "");
}
private List Search(string search)
@@ -248,24 +272,22 @@ else
site.AllowRegistration = bool.Parse(_allowregistration);
await SiteService.UpdateSiteAsync(site);
- if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
- {
- var settings = new Dictionary();
- settings.Add("Password:RequiredLength", _minimumlength);
- settings.Add("Password:RequiredUniqueChars", _uniquecharacters);
- settings.Add("Password:RequireDigit", _requiredigit);
- settings.Add("Password:RequireUppercase", _requireupper);
- settings.Add("Password:RequireLowercase", _requirelower);
- settings.Add("Password:RequireNonAlphanumeric", _requirepunctuation);
- settings.Add("Lockout:MaxFailedAccessAttempts", _maximumfailures);
- settings.Add("Lockout:DefaultLockoutTimeSpan", TimeSpan.FromMinutes(Convert.ToInt64(_lockoutduration)).ToString());
- await SystemService.UpdateSystemInfoAsync(settings);
- AddModuleMessage(Localizer["Success.UpdateConfig.Restart"], MessageType.Success);
- }
- else
- {
- AddModuleMessage(Localizer["Success.SaveSiteSettings"], MessageType.Success);
- }
+ var settings = await SettingService.GetSiteSettingsAsync(site.SiteId);
+ settings = SettingService.SetSetting(settings, "IdentityOptions:Password:RequiredLength", _minimumlength, true);
+ settings = SettingService.SetSetting(settings, "IdentityOptions:Password:RequiredUniqueChars", _uniquecharacters, true);
+ settings = SettingService.SetSetting(settings, "IdentityOptions:Password:RequireDigit", _requiredigit, true);
+ settings = SettingService.SetSetting(settings, "IdentityOptions:Password:RequireUppercase", _requireupper, true);
+ settings = SettingService.SetSetting(settings, "IdentityOptions:Password:RequireLowercase", _requirelower, true);
+ settings = SettingService.SetSetting(settings, "IdentityOptions:Password:RequireNonAlphanumeric", _requirepunctuation, true);
+ settings = SettingService.SetSetting(settings, "IdentityOptions:Lockout:MaxFailedAccessAttempts", _maximumfailures, true);
+ settings = SettingService.SetSetting(settings, "IdentityOptions:Lockout:DefaultLockoutTimeSpan", TimeSpan.FromMinutes(Convert.ToInt64(_lockoutduration)).ToString(), true);
+ settings = SettingService.SetSetting(settings, "OpenIdConnectOptions:Provider", _provider, false);
+ settings = SettingService.SetSetting(settings, "OpenIdConnectOptions:Authority", _authority, true);
+ settings = SettingService.SetSetting(settings, "OpenIdConnectOptions:ClientId", _clientid, true);
+ settings = SettingService.SetSetting(settings, "OpenIdConnectOptions:ClientSecret", _clientsecret, true);
+ await SettingService.UpdateSiteSettingsAsync(settings, site.SiteId);
+
+ AddModuleMessage(Localizer["Success.SaveSiteSettings"], MessageType.Success);
}
catch (Exception ex)
{
diff --git a/Oqtane.Client/Themes/Controls/Theme/LoginBase.cs b/Oqtane.Client/Themes/Controls/Theme/LoginBase.cs
index d8afa02e..5a1b3452 100644
--- a/Oqtane.Client/Themes/Controls/Theme/LoginBase.cs
+++ b/Oqtane.Client/Themes/Controls/Theme/LoginBase.cs
@@ -38,7 +38,7 @@ namespace Oqtane.Themes.Controls
PageState.User = null;
bool authorizedtoviewpage = UserSecurity.IsAuthorized(PageState.User, PermissionNames.View, PageState.Page.Permissions);
- if (PageState.Runtime == Oqtane.Shared.Runtime.Server)
+ if (PageState.Runtime == Shared.Runtime.Server)
{
// server-side Blazor needs to post to the Logout page
var fields = new { __RequestVerificationToken = SiteState.AntiForgeryToken, returnurl = !authorizedtoviewpage ? PageState.Alias.Path : PageState.Alias.Path + "/" + PageState.Page.Path };
diff --git a/Oqtane.Server/Controllers/SystemController.cs b/Oqtane.Server/Controllers/SystemController.cs
index 31035d37..c0bdd44e 100644
--- a/Oqtane.Server/Controllers/SystemController.cs
+++ b/Oqtane.Server/Controllers/SystemController.cs
@@ -51,14 +51,6 @@ namespace Oqtane.Controllers
systeminfo.Add("Logging:LogLevel:Notify", _configManager.GetSetting("Logging:LogLevel:Notify", "Error"));
systeminfo.Add("UseSwagger", _configManager.GetSetting("UseSwagger", "true"));
systeminfo.Add("PackageService", _configManager.GetSetting("PackageService", "true"));
- systeminfo.Add("Password:RequiredLength", _configManager.GetSetting("Password:RequiredLength", "6"));
- systeminfo.Add("Password:RequiredUniqueChars", _configManager.GetSetting("Password:RequiredUniqueChars", "1"));
- systeminfo.Add("Password:RequireDigit", _configManager.GetSetting("Password:RequireDigit", "true"));
- systeminfo.Add("Password:RequireUppercase", _configManager.GetSetting("Password:RequireUppercase", "true"));
- systeminfo.Add("Password:RequireLowercase", _configManager.GetSetting("Password:RequireLowercase", "true"));
- systeminfo.Add("Password:RequireNonAlphanumeric", _configManager.GetSetting("Password:RequireNonAlphanumeric", "true"));
- systeminfo.Add("Lockout:MaxFailedAccessAttempts", _configManager.GetSetting("Lockout:MaxFailedAccessAttempts", "5"));
- systeminfo.Add("Lockout:DefaultLockoutTimeSpan", _configManager.GetSetting("Lockout:DefaultLockoutTimeSpan", "00:05:00"));
break;
}
diff --git a/Oqtane.Server/Extensions/HttpContextExtensions.cs b/Oqtane.Server/Extensions/HttpContextExtensions.cs
new file mode 100644
index 00000000..c601297c
--- /dev/null
+++ b/Oqtane.Server/Extensions/HttpContextExtensions.cs
@@ -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;
+ }
+ }
+}
diff --git a/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs b/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs
index 15f70c2e..22b144e7 100644
--- a/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs
+++ b/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs
@@ -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 AddOqtaneSiteOptions(this IServiceCollection services)
+ where T : class, IAlias, new()
+ {
+ return new OqtaneSiteOptionsBuilder(services);
+ }
+
internal static IServiceCollection AddOqtaneSingletonServices(this IServiceCollection services)
{
services.AddSingleton();
@@ -71,6 +78,8 @@ namespace Microsoft.Extensions.DependencyInjection
internal static IServiceCollection AddOqtaneTransientServices(this IServiceCollection services)
{
services.AddTransient();
+ services.AddTransient();
+
services.AddTransient();
services.AddTransient();
services.AddTransient();
@@ -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
{
diff --git a/Oqtane.Server/Extensions/OqtaneSiteAuthenticationBuilderExtensions.cs b/Oqtane.Server/Extensions/OqtaneSiteAuthenticationBuilderExtensions.cs
new file mode 100644
index 00000000..dc60fb74
--- /dev/null
+++ b/Oqtane.Server/Extensions/OqtaneSiteAuthenticationBuilderExtensions.cs
@@ -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 WithSiteAuthentication(
+ this OqtaneSiteOptionsBuilder builder)
+ where TAlias : class, IAlias, new()
+ {
+ builder.WithSiteAuthenticationCore();
+ builder.WithSiteAuthenticationOptions();
+
+ return builder;
+ }
+
+ public static OqtaneSiteOptionsBuilder WithSiteAuthenticationCore(
+ this OqtaneSiteOptionsBuilder builder)
+ where TAlias : class, IAlias, new()
+ {
+ builder.Services.DecorateService>();
+ builder.Services.Replace(ServiceDescriptor.Singleton());
+
+ return builder;
+ }
+
+ public static OqtaneSiteOptionsBuilder WithSiteAuthenticationOptions(
+ this OqtaneSiteOptionsBuilder builder)
+ where TAlias : class, IAlias, new()
+ {
+ // site OpenIdConnect options
+ builder.AddSiteOptions((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((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>();
+ var _users = context.HttpContext.RequestServices.GetRequiredService();
+ var _userRoles = context.HttpContext.RequestServices.GetRequiredService();
+ 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();
+ 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
+ {
+ 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();
+ List 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 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();
+ _logger.Log(LogLevel.Information, "OqtaneSiteAuthenticationBuilderExtensions", Enums.LogFunction.Security, "OpenId Connect Server Did Not Return An Email For User");
+ }
+ }
+
+ public static bool DecorateService(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(sp, parameters2)!;
+ },
+ existingService.Lifetime);
+
+ if (existingService.ImplementationInstance != null)
+ {
+ newService = new ServiceDescriptor(existingService.ServiceType,
+ sp =>
+ {
+ TService inner = (TService)existingService.ImplementationInstance;
+ return ActivatorUtilities.CreateInstance(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(sp, inner, parameters)!;
+ },
+ existingService.Lifetime);
+ }
+
+ services.Remove(existingService);
+ services.Add(newService);
+
+ return true;
+ }
+ }
+}
diff --git a/Oqtane.Server/Extensions/OqtaneSiteIdentityBuilderExtensions.cs b/Oqtane.Server/Extensions/OqtaneSiteIdentityBuilderExtensions.cs
new file mode 100644
index 00000000..14a766e3
--- /dev/null
+++ b/Oqtane.Server/Extensions/OqtaneSiteIdentityBuilderExtensions.cs
@@ -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 WithSiteIdentity(
+ this OqtaneSiteOptionsBuilder builder)
+ where TAlias : class, IAlias, new()
+ {
+ // site identity options
+ builder.AddSiteOptions((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;
+ }
+ }
+}
diff --git a/Oqtane.Server/Extensions/OqtaneSiteOptionsBuilder.cs b/Oqtane.Server/Extensions/OqtaneSiteOptionsBuilder.cs
new file mode 100644
index 00000000..52f751b4
--- /dev/null
+++ b/Oqtane.Server/Extensions/OqtaneSiteOptionsBuilder.cs
@@ -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 where TSiteOptions : class, IAlias, new()
+ {
+ public IServiceCollection Services { get; set; }
+
+ public OqtaneSiteOptionsBuilder(IServiceCollection services)
+ {
+ Services = services;
+ }
+
+ public OqtaneSiteOptionsBuilder AddSiteOptions(
+ Action siteOptions) where TOptions : class, new()
+ {
+ Services.TryAddSingleton, SiteOptionsCache>();
+ Services.AddSingleton, SiteOptions>
+ (sp => new SiteOptions(siteOptions));
+ Services.TryAddTransient, SiteOptionsFactory>();
+ Services.TryAddScoped>(sp => BuildOptionsManager(sp));
+ Services.TryAddSingleton>(sp => BuildOptionsManager(sp));
+
+ return this;
+ }
+
+ private static SiteOptionsManager BuildOptionsManager(IServiceProvider sp)
+ where TOptions : class, new()
+ {
+ var cache = ActivatorUtilities.CreateInstance(sp, typeof(SiteOptionsCache));
+ return (SiteOptionsManager)ActivatorUtilities.CreateInstance(sp, typeof(SiteOptionsManager), new[] { cache });
+ }
+
+ }
+}
diff --git a/Oqtane.Server/Extensions/StringExtensions.cs b/Oqtane.Server/Extensions/StringExtensions.cs
index ede3bce9..f3640368 100644
--- a/Oqtane.Server/Extensions/StringExtensions.cs
+++ b/Oqtane.Server/Extensions/StringExtensions.cs
@@ -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
{
diff --git a/Oqtane.Server/Infrastructure/AliasAccessor.cs b/Oqtane.Server/Infrastructure/AliasAccessor.cs
new file mode 100644
index 00000000..87cbecd3
--- /dev/null
+++ b/Oqtane.Server/Infrastructure/AliasAccessor.cs
@@ -0,0 +1,18 @@
+using Microsoft.AspNetCore.Http;
+using Oqtane.Extensions;
+using Oqtane.Models;
+
+namespace Oqtane.Infrastructure
+{
+ public class AliasAccessor : IAliasAccessor
+ {
+ private readonly IHttpContextAccessor _httpContextAccessor;
+
+ public AliasAccessor(IHttpContextAccessor httpContextAccessor)
+ {
+ _httpContextAccessor = httpContextAccessor;
+ }
+
+ public Alias Alias => _httpContextAccessor.HttpContext.GetAlias();
+ }
+}
diff --git a/Oqtane.Server/Infrastructure/Interfaces/IAliasAccessor.cs b/Oqtane.Server/Infrastructure/Interfaces/IAliasAccessor.cs
new file mode 100644
index 00000000..fc54d683
--- /dev/null
+++ b/Oqtane.Server/Infrastructure/Interfaces/IAliasAccessor.cs
@@ -0,0 +1,9 @@
+using Oqtane.Models;
+
+namespace Oqtane.Infrastructure
+{
+ public interface IAliasAccessor
+ {
+ Alias Alias { get; }
+ }
+}
diff --git a/Oqtane.Server/Infrastructure/Internal/SiteAuthenticationSchemeProvider.cs b/Oqtane.Server/Infrastructure/Internal/SiteAuthenticationSchemeProvider.cs
new file mode 100644
index 00000000..359e8183
--- /dev/null
+++ b/Oqtane.Server/Infrastructure/Internal/SiteAuthenticationSchemeProvider.cs
@@ -0,0 +1,112 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Authentication;
+using Microsoft.Extensions.Options;
+
+namespace Oqtane.Infrastructure
+{
+ internal class SiteAuthenticationSchemeProvider : IAuthenticationSchemeProvider
+ {
+ public SiteAuthenticationSchemeProvider(IOptions options)
+ : this(options, new Dictionary(StringComparer.Ordinal))
+ {
+ }
+
+ public SiteAuthenticationSchemeProvider(IOptions options, IDictionary schemes)
+ {
+ _optionsProvider = options;
+
+ _schemes = schemes ?? throw new ArgumentNullException(nameof(schemes));
+ _requestHandlers = new List();
+
+ foreach (var builder in _optionsProvider.Value.Schemes)
+ {
+ var scheme = builder.Build();
+ AddScheme(scheme);
+ }
+ }
+
+ private readonly IOptions _optionsProvider;
+ private readonly object _lock = new object();
+
+ private readonly IDictionary _schemes;
+ private readonly List _requestHandlers;
+
+ private Task GetDefaultSchemeAsync()
+ => _optionsProvider.Value.DefaultScheme != null
+ ? GetSchemeAsync(_optionsProvider.Value.DefaultScheme)
+ : Task.FromResult(null);
+
+ public virtual Task GetDefaultAuthenticateSchemeAsync()
+ => _optionsProvider.Value.DefaultAuthenticateScheme != null
+ ? GetSchemeAsync(_optionsProvider.Value.DefaultAuthenticateScheme)
+ : GetDefaultSchemeAsync();
+
+ public virtual Task GetDefaultChallengeSchemeAsync()
+ => _optionsProvider.Value.DefaultChallengeScheme != null
+ ? GetSchemeAsync(_optionsProvider.Value.DefaultChallengeScheme)
+ : GetDefaultSchemeAsync();
+
+ public virtual Task GetDefaultForbidSchemeAsync()
+ => _optionsProvider.Value.DefaultForbidScheme != null
+ ? GetSchemeAsync(_optionsProvider.Value.DefaultForbidScheme)
+ : GetDefaultChallengeSchemeAsync();
+
+ public virtual Task GetDefaultSignInSchemeAsync()
+ => _optionsProvider.Value.DefaultSignInScheme != null
+ ? GetSchemeAsync(_optionsProvider.Value.DefaultSignInScheme)
+ : GetDefaultSchemeAsync();
+
+ public virtual Task GetDefaultSignOutSchemeAsync()
+ => _optionsProvider.Value.DefaultSignOutScheme != null
+ ? GetSchemeAsync(_optionsProvider.Value.DefaultSignOutScheme)
+ : GetDefaultSignInSchemeAsync();
+
+ public virtual Task GetSchemeAsync(string name)
+ => Task.FromResult(_schemes.ContainsKey(name) ? _schemes[name] : null);
+
+ public virtual Task> GetRequestHandlerSchemesAsync()
+ => Task.FromResult>(_requestHandlers);
+
+ public virtual void AddScheme(AuthenticationScheme scheme)
+ {
+ if (_schemes.ContainsKey(scheme.Name))
+ {
+ throw new InvalidOperationException("Scheme already exists: " + scheme.Name);
+ }
+ lock (_lock)
+ {
+ if (_schemes.ContainsKey(scheme.Name))
+ {
+ throw new InvalidOperationException("Scheme already exists: " + scheme.Name);
+ }
+ if (typeof(IAuthenticationRequestHandler).IsAssignableFrom(scheme.HandlerType))
+ {
+ _requestHandlers.Add(scheme);
+ }
+ _schemes[scheme.Name] = scheme;
+ }
+ }
+
+ public virtual void RemoveScheme(string name)
+ {
+ if (!_schemes.ContainsKey(name))
+ {
+ return;
+ }
+ lock (_lock)
+ {
+ if (_schemes.ContainsKey(name))
+ {
+ var scheme = _schemes[name];
+ _requestHandlers.Remove(scheme);
+ _schemes.Remove(name);
+ }
+ }
+ }
+
+ public virtual Task> GetAllSchemesAsync()
+ => Task.FromResult>(_schemes.Values);
+ }
+}
diff --git a/Oqtane.Server/Infrastructure/Internal/SiteAuthenticationService.cs b/Oqtane.Server/Infrastructure/Internal/SiteAuthenticationService.cs
new file mode 100644
index 00000000..40ccb519
--- /dev/null
+++ b/Oqtane.Server/Infrastructure/Internal/SiteAuthenticationService.cs
@@ -0,0 +1,62 @@
+using System.Security.Claims;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Authentication;
+using Microsoft.AspNetCore.Http;
+using Oqtane.Extensions;
+using Oqtane.Models;
+using Oqtane.Shared;
+
+namespace Oqtane.Infrastructure
+{
+ internal class SiteAuthenticationService : IAuthenticationService
+ where TAlias : class, IAlias, new()
+ {
+ private readonly IAuthenticationService _inner;
+
+ public SiteAuthenticationService(IAuthenticationService inner)
+ {
+ _inner = inner ?? throw new System.ArgumentNullException(nameof(inner));
+ }
+
+ private static void AddTenantIdentifierToProperties(HttpContext context, ref AuthenticationProperties properties)
+ {
+ // add site identifier to the authentication properties so on the callback we can use it to set context
+ var alias = context.GetAlias();
+ if (alias != null)
+ {
+ properties ??= new AuthenticationProperties();
+ if (!properties.Items.Keys.Contains(Constants.SiteToken))
+ {
+ properties.Items.Add(Constants.SiteToken, alias.SiteKey);
+ }
+ }
+ }
+
+ public Task AuthenticateAsync(HttpContext context, string scheme)
+ => _inner.AuthenticateAsync(context, scheme);
+
+ public async Task ChallengeAsync(HttpContext context, string scheme, AuthenticationProperties properties)
+ {
+ AddTenantIdentifierToProperties(context, ref properties);
+ await _inner.ChallengeAsync(context, scheme, properties);
+ }
+
+ public async Task ForbidAsync(HttpContext context, string scheme, AuthenticationProperties properties)
+ {
+ AddTenantIdentifierToProperties(context, ref properties);
+ await _inner.ForbidAsync(context, scheme, properties);
+ }
+
+ public async Task SignInAsync(HttpContext context, string scheme, ClaimsPrincipal principal, AuthenticationProperties properties)
+ {
+ AddTenantIdentifierToProperties(context, ref properties);
+ await _inner.SignInAsync(context, scheme, principal, properties);
+ }
+
+ public async Task SignOutAsync(HttpContext context, string scheme, AuthenticationProperties properties)
+ {
+ AddTenantIdentifierToProperties(context, ref properties);
+ await _inner.SignOutAsync(context, scheme, properties);
+ }
+ }
+}
diff --git a/Oqtane.Server/Infrastructure/Middleware/TenantMiddleware.cs b/Oqtane.Server/Infrastructure/Middleware/TenantMiddleware.cs
index 9a748d96..e4238a62 100644
--- a/Oqtane.Server/Infrastructure/Middleware/TenantMiddleware.cs
+++ b/Oqtane.Server/Infrastructure/Middleware/TenantMiddleware.cs
@@ -1,5 +1,8 @@
+using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
+using Oqtane.Repository;
+using Oqtane.Shared;
namespace Oqtane.Infrastructure
{
@@ -18,19 +21,30 @@ namespace Oqtane.Infrastructure
var config = context.RequestServices.GetService(typeof(IConfigManager)) as IConfigManager;
if (config.IsInstalled())
{
- // get alias
+ // get alias (note that this also sets SiteState.Alias)
var tenantManager = context.RequestServices.GetService(typeof(ITenantManager)) as ITenantManager;
var alias = tenantManager.GetAlias();
- // rewrite path by removing alias path prefix from api and pages requests
- if (alias != null && !string.IsNullOrEmpty(alias.Path))
+ if (alias != null)
{
- string path = context.Request.Path.ToString();
- if (path.StartsWith("/" + alias.Path) && (path.Contains("/api/") || path.Contains("/pages/")))
+ // get site settings and store alias in HttpContext
+ var settingRepository = context.RequestServices.GetService(typeof(ISettingRepository)) as ISettingRepository;
+ alias.SiteSettings = settingRepository.GetSettings(EntityNames.Site)
+ .ToDictionary(setting => setting.SettingName, setting => setting.SettingValue);
+ context.Items.Add(Constants.HttpContextAliasKey, alias);
+
+ // rewrite path by removing alias path prefix from api and pages requests (for consistent routing)
+ if (!string.IsNullOrEmpty(alias.Path))
{
- context.Request.Path = path.Replace("/" + alias.Path, "");
+ string path = context.Request.Path.ToString();
+ if (path.StartsWith("/" + alias.Path) && (path.Contains("/api/") || path.Contains("/pages/")))
+ {
+ context.Request.Path = path.Replace("/" + alias.Path, "");
+ }
}
+
}
+
}
// continue processing
diff --git a/Oqtane.Server/Infrastructure/Options/ISiteOptions.cs b/Oqtane.Server/Infrastructure/Options/ISiteOptions.cs
new file mode 100644
index 00000000..0cf2d599
--- /dev/null
+++ b/Oqtane.Server/Infrastructure/Options/ISiteOptions.cs
@@ -0,0 +1,12 @@
+
+using Oqtane.Models;
+
+namespace Oqtane.Infrastructure
+{
+ public interface ISiteOptions
+ where TOptions : class, new()
+ where TAlias : class, IAlias, new()
+ {
+ void Configure(TOptions options, TAlias siteOptions);
+ }
+}
diff --git a/Oqtane.Server/Infrastructure/Options/SiteOptions.cs b/Oqtane.Server/Infrastructure/Options/SiteOptions.cs
new file mode 100644
index 00000000..63db36d3
--- /dev/null
+++ b/Oqtane.Server/Infrastructure/Options/SiteOptions.cs
@@ -0,0 +1,22 @@
+using System;
+using Oqtane.Models;
+
+namespace Oqtane.Infrastructure
+{
+ public class SiteOptions : ISiteOptions
+ where TOptions : class, new()
+ where TAlias : class, IAlias, new()
+ {
+ private readonly Action configureOptions;
+
+ public SiteOptions(Action configureOptions)
+ {
+ this.configureOptions = configureOptions;
+ }
+
+ public void Configure(TOptions options, TAlias siteOptions)
+ {
+ configureOptions(options, siteOptions);
+ }
+ }
+}
diff --git a/Oqtane.Server/Infrastructure/Options/SiteOptionsCache.cs b/Oqtane.Server/Infrastructure/Options/SiteOptionsCache.cs
new file mode 100644
index 00000000..0941fa38
--- /dev/null
+++ b/Oqtane.Server/Infrastructure/Options/SiteOptionsCache.cs
@@ -0,0 +1,70 @@
+using System;
+using System.Collections.Concurrent;
+using Microsoft.Extensions.Options;
+using Oqtane.Models;
+
+namespace Oqtane.Infrastructure
+{
+ public class SiteOptionsCache : IOptionsMonitorCache
+ where TOptions : class
+ where TAlias : class, IAlias, new()
+ {
+ private readonly IAliasAccessor _aliasAccessor;
+ private readonly ConcurrentDictionary> map = new ConcurrentDictionary>();
+
+ public SiteOptionsCache(IAliasAccessor aliasAccessor)
+ {
+ _aliasAccessor = aliasAccessor;
+ }
+
+ public void Clear()
+ {
+ var cache = map.GetOrAdd(GetKey(), new OptionsCache());
+ cache.Clear();
+ }
+
+ public void Clear(Alias alias)
+ {
+ var cache = map.GetOrAdd(alias.SiteKey, new OptionsCache());
+
+ cache.Clear();
+ }
+
+ public void ClearAll()
+ {
+ foreach (var cache in map.Values)
+ {
+ cache.Clear();
+ }
+ }
+
+ public TOptions GetOrAdd(string name, Func createOptions)
+ {
+ name = name ?? Options.DefaultName;
+ var cache = map.GetOrAdd(GetKey(), new OptionsCache());
+
+ return cache.GetOrAdd(name, createOptions);
+ }
+
+ public bool TryAdd(string name, TOptions options)
+ {
+ name = name ?? Options.DefaultName;
+ var cache = map.GetOrAdd(GetKey(), new OptionsCache());
+
+ return cache.TryAdd(name, options);
+ }
+
+ public bool TryRemove(string name)
+ {
+ name = name ?? Options.DefaultName;
+ var cache = map.GetOrAdd(GetKey(), new OptionsCache());
+
+ return cache.TryRemove(name);
+ }
+
+ private string GetKey()
+ {
+ return _aliasAccessor?.Alias?.SiteKey ?? "";
+ }
+ }
+}
diff --git a/Oqtane.Server/Infrastructure/Options/SiteOptionsFactory.cs b/Oqtane.Server/Infrastructure/Options/SiteOptionsFactory.cs
new file mode 100644
index 00000000..1c719c7c
--- /dev/null
+++ b/Oqtane.Server/Infrastructure/Options/SiteOptionsFactory.cs
@@ -0,0 +1,77 @@
+using System.Collections.Generic;
+using Microsoft.Extensions.Options;
+using Oqtane.Models;
+
+namespace Oqtane.Infrastructure
+{
+ public class SiteOptionsFactory : IOptionsFactory
+ where TOptions : class, new()
+ where TAlias : class, IAlias, new()
+ {
+ private readonly IConfigureOptions[] _configureOptions;
+ private readonly IPostConfigureOptions[] _postConfigureOptions;
+ private readonly IValidateOptions[] _validations;
+ private readonly ISiteOptions[] _siteOptions;
+ private readonly IAliasAccessor _aliasAccessor;
+
+ public SiteOptionsFactory(IEnumerable> configureOptions, IEnumerable> postConfigureOptions, IEnumerable> validations, IEnumerable> siteOptions, IAliasAccessor aliasAccessor)
+ {
+ _configureOptions = configureOptions as IConfigureOptions[] ?? new List>(configureOptions).ToArray();
+ _postConfigureOptions = postConfigureOptions as IPostConfigureOptions[] ?? new List>(postConfigureOptions).ToArray();
+ _validations = validations as IValidateOptions[] ?? new List>(validations).ToArray();
+ _siteOptions = siteOptions as ISiteOptions[] ?? new List>(siteOptions).ToArray();
+ _aliasAccessor = aliasAccessor;
+ }
+
+ public TOptions Create(string name)
+ {
+ // default options
+ var options = new TOptions();
+ foreach (var setup in _configureOptions)
+ {
+ if (setup is IConfigureNamedOptions namedSetup)
+ {
+ namedSetup.Configure(name, options);
+ }
+ else if (name == Options.DefaultName)
+ {
+ setup.Configure(options);
+ }
+ }
+
+ // override with site specific options
+ if (_aliasAccessor?.Alias != null)
+ {
+ foreach (var siteOption in _siteOptions)
+ {
+ siteOption.Configure(options, _aliasAccessor.Alias as TAlias);
+ }
+ }
+
+ // post configuration
+ foreach (var post in _postConfigureOptions)
+ {
+ post.PostConfigure(name, options);
+ }
+
+ //if (_validations.Length > 0)
+ //{
+ // var failures = new List();
+ // foreach (IValidateOptions validate in _validations)
+ // {
+ // ValidateOptionsResult result = validate.Validate(name, options);
+ // if (result != null && result.Failed)
+ // {
+ // failures.AddRange(result.Failures);
+ // }
+ // }
+ // if (failures.Count > 0)
+ // {
+ // throw new OptionsValidationException(name, typeof(TOptions), failures);
+ // }
+ //}
+
+ return options;
+ }
+ }
+}
diff --git a/Oqtane.Server/Infrastructure/Options/SiteOptionsManager.cs b/Oqtane.Server/Infrastructure/Options/SiteOptionsManager.cs
new file mode 100644
index 00000000..f06e09ab
--- /dev/null
+++ b/Oqtane.Server/Infrastructure/Options/SiteOptionsManager.cs
@@ -0,0 +1,35 @@
+using Microsoft.Extensions.Options;
+
+namespace Oqtane.Infrastructure
+{
+ public class SiteOptionsManager : IOptions, IOptionsSnapshot where TOptions : class, new()
+ {
+ private readonly IOptionsFactory _factory;
+ private readonly IOptionsMonitorCache _cache; // private cache
+
+ public SiteOptionsManager(IOptionsFactory factory, IOptionsMonitorCache cache)
+ {
+ _factory = factory;
+ _cache = cache;
+ }
+
+ public TOptions Value
+ {
+ get
+ {
+ return Get(Options.DefaultName);
+ }
+ }
+
+ public virtual TOptions Get(string name)
+ {
+ name = name ?? Options.DefaultName;
+ return _cache.GetOrAdd(name, () => _factory.Create(name));
+ }
+
+ public void Reset()
+ {
+ _cache.Clear();
+ }
+ }
+}
diff --git a/Oqtane.Server/Infrastructure/TenantManager.cs b/Oqtane.Server/Infrastructure/TenantManager.cs
index 72798454..4a1de172 100644
--- a/Oqtane.Server/Infrastructure/TenantManager.cs
+++ b/Oqtane.Server/Infrastructure/TenantManager.cs
@@ -26,7 +26,7 @@ namespace Oqtane.Infrastructure
{
Alias alias = null;
- if (_siteState != null && _siteState.Alias != null)
+ if (_siteState != null && _siteState.Alias != null && _siteState.Alias.AliasId != -1)
{
alias = _siteState.Alias;
}
diff --git a/Oqtane.Server/Oqtane.Server.csproj b/Oqtane.Server/Oqtane.Server.csproj
index da00afff..b3e194ee 100644
--- a/Oqtane.Server/Oqtane.Server.csproj
+++ b/Oqtane.Server/Oqtane.Server.csproj
@@ -32,6 +32,7 @@
+
diff --git a/Oqtane.Server/Pages/OIDC.cshtml b/Oqtane.Server/Pages/OIDC.cshtml
new file mode 100644
index 00000000..47fe734a
--- /dev/null
+++ b/Oqtane.Server/Pages/OIDC.cshtml
@@ -0,0 +1,3 @@
+@page "/pages/oidc"
+@namespace Oqtane.Pages
+@model Oqtane.Pages.OIDCModel
diff --git a/Oqtane.Server/Pages/OIDC.cshtml.cs b/Oqtane.Server/Pages/OIDC.cshtml.cs
new file mode 100644
index 00000000..433d1bea
--- /dev/null
+++ b/Oqtane.Server/Pages/OIDC.cshtml.cs
@@ -0,0 +1,15 @@
+using Microsoft.AspNetCore.Authentication;
+using Microsoft.AspNetCore.Authentication.OpenIdConnect;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.RazorPages;
+
+namespace Oqtane.Pages
+{
+ public class OIDCModel : PageModel
+ {
+ public IActionResult OnGetAsync(string returnurl)
+ {
+ return new ChallengeResult(OpenIdConnectDefaults.AuthenticationScheme, new AuthenticationProperties { RedirectUri = !string.IsNullOrEmpty(returnurl) ? returnurl : "/" });
+ }
+ }
+}
diff --git a/Oqtane.Server/Security/PrincipalValidator.cs b/Oqtane.Server/Security/PrincipalValidator.cs
index 46f9f3c7..5a45c820 100644
--- a/Oqtane.Server/Security/PrincipalValidator.cs
+++ b/Oqtane.Server/Security/PrincipalValidator.cs
@@ -37,17 +37,20 @@ namespace Oqtane.Security
var userRepository = context.HttpContext.RequestServices.GetService(typeof(IUserRepository)) as IUserRepository;
var userRoleRepository = context.HttpContext.RequestServices.GetService(typeof(IUserRoleRepository)) as IUserRoleRepository;
- User user = userRepository.GetUser(context.Principal.Identity.Name);
- if (user != null)
+ if (context.Principal.Identity.Name != null)
{
- List userroles = userRoleRepository.GetUserRoles(user.UserId, alias.SiteId).ToList();
- var identity = UserSecurity.CreateClaimsIdentity(alias, user, userroles);
- context.ReplacePrincipal(new ClaimsPrincipal(identity));
- context.ShouldRenew = true;
- }
- else
- {
- context.RejectPrincipal();
+ User user = userRepository.GetUser(context.Principal.Identity.Name);
+ if (user != null)
+ {
+ List userroles = userRoleRepository.GetUserRoles(user.UserId, alias.SiteId).ToList();
+ var identity = UserSecurity.CreateClaimsIdentity(alias, user, userroles);
+ context.ReplacePrincipal(new ClaimsPrincipal(identity));
+ context.ShouldRenew = true;
+ }
+ else
+ {
+ context.RejectPrincipal();
+ }
}
}
}
diff --git a/Oqtane.Server/Startup.cs b/Oqtane.Server/Startup.cs
index c3ec9217..44a8bf2b 100644
--- a/Oqtane.Server/Startup.cs
+++ b/Oqtane.Server/Startup.cs
@@ -16,6 +16,9 @@ using Oqtane.Repository;
using Oqtane.Security;
using Oqtane.Shared;
using Microsoft.AspNetCore.HttpOverrides;
+using Microsoft.IdentityModel.Protocols.OpenIdConnect;
+using Microsoft.AspNetCore.Authentication.OpenIdConnect;
+using System.Threading.Tasks;
namespace Oqtane
{
@@ -72,37 +75,12 @@ namespace Oqtane
// setup HttpClient for server side in a client side compatible fashion ( with auth cookie )
services.TryAddHttpClientWithAuthenticationCookie();
- // register custom authorization policies
- services.AddOqtaneAuthorizationPolicies();
-
// register scoped core services
services.AddScoped()
.AddOqtaneScopedServices();
services.AddSingleton();
- services.AddIdentityCore(options => { })
- .AddEntityFrameworkStores()
- .AddSignInManager()
- .AddDefaultTokenProviders()
- .AddClaimsPrincipalFactory>(); // role claims
-
- services.ConfigureOqtaneIdentityOptions(Configuration);
-
- services.AddAuthentication(Constants.AuthenticationScheme)
- .AddCookie(Constants.AuthenticationScheme);
-
- services.ConfigureOqtaneCookieOptions();
-
- services.AddAntiforgery(options =>
- {
- options.HeaderName = Constants.AntiForgeryTokenHeaderName;
- options.Cookie.HttpOnly = false;
- options.Cookie.Name = Constants.AntiForgeryTokenCookieName;
- options.Cookie.SameSite = SameSiteMode.Strict;
- options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest;
- });
-
// register singleton scoped core services
services.AddSingleton(Configuration)
.AddOqtaneSingletonServices();
@@ -117,10 +95,43 @@ namespace Oqtane
services.AddOqtane(_supportedCultures);
services.AddOqtaneDbContext();
+ services.AddAntiforgery(options =>
+ {
+ options.HeaderName = Constants.AntiForgeryTokenHeaderName;
+ options.Cookie.Name = Constants.AntiForgeryTokenCookieName;
+ options.Cookie.SameSite = SameSiteMode.Strict;
+ options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest;
+ //options.Cookie.HttpOnly = false;
+ });
+
+ services.AddIdentityCore(options => { })
+ .AddEntityFrameworkStores()
+ .AddSignInManager()
+ .AddDefaultTokenProviders()
+ .AddClaimsPrincipalFactory>(); // role claims
+
+ services.ConfigureOqtaneIdentityOptions(Configuration);
+
+ services.AddAuthentication(options =>
+ {
+ options.DefaultAuthenticateScheme = Constants.AuthenticationScheme;
+ options.DefaultChallengeScheme = Constants.AuthenticationScheme;
+ })
+ .AddCookie(Constants.AuthenticationScheme)
+ .AddOpenIdConnect();
+
+ services.ConfigureOqtaneCookieOptions();
+
+ services.AddOqtaneSiteOptions()
+ .WithSiteIdentity()
+ .WithSiteAuthentication();
+
+ services.AddOqtaneAuthorizationPolicies();
+
services.AddMvc()
.AddNewtonsoftJson()
.AddOqtaneApplicationParts() // register any Controllers from custom modules
- .ConfigureOqtaneMvc(); // any additional configuration from IStart classes.
+ .ConfigureOqtaneMvc(); // any additional configuration from IStartup classes
services.AddSwaggerGen(options =>
{
diff --git a/Oqtane.Server/wwwroot/Modules/Oqtane.Modules.Admin.Login/Module.css b/Oqtane.Server/wwwroot/Modules/Oqtane.Modules.Admin.Login/Module.css
index a3d63e55..086b246b 100644
--- a/Oqtane.Server/wwwroot/Modules/Oqtane.Modules.Admin.Login/Module.css
+++ b/Oqtane.Server/wwwroot/Modules/Oqtane.Modules.Admin.Login/Module.css
@@ -1,9 +1,5 @@
/* Login Module Custom Styles */
-.Oqtane-Modules-Admin-Login .input {
+.Oqtane-Modules-Admin-Login {
width: 200px;
}
-
-.Oqtane-Modules-Admin-Login .password {
- width: 270px;
-}
\ No newline at end of file
diff --git a/Oqtane.Shared/Interfaces/IAlias.cs b/Oqtane.Shared/Interfaces/IAlias.cs
new file mode 100644
index 00000000..43ae54ef
--- /dev/null
+++ b/Oqtane.Shared/Interfaces/IAlias.cs
@@ -0,0 +1,21 @@
+using System;
+using System.Collections.Generic;
+
+namespace Oqtane.Models
+{
+ public interface IAlias
+ {
+ int AliasId { get; set; }
+ string Name { get; set; }
+ int TenantId { get; set; }
+ int SiteId { get; set; }
+ bool IsDefault { get; set; }
+ string CreatedBy { get; set; }
+ DateTime CreatedOn { get; set; }
+ string ModifiedBy { get; set; }
+ DateTime ModifiedOn { get; set; }
+ string Path { get; }
+ string SiteKey { get; }
+ Dictionary SiteSettings { get; set; }
+ }
+}
diff --git a/Oqtane.Shared/Models/Alias.cs b/Oqtane.Shared/Models/Alias.cs
index 229fde44..f6c2c8a0 100644
--- a/Oqtane.Shared/Models/Alias.cs
+++ b/Oqtane.Shared/Models/Alias.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
namespace Oqtane.Models
@@ -6,7 +7,7 @@ namespace Oqtane.Models
///
/// An Alias maps a url like `oqtane.my` or `oqtane.my/products` to a and
///
- public class Alias : IAuditable
+ public class Alias : IAlias, IAuditable
{
///
/// The primary ID for internal use. It's also used in API calls to identify the site.
@@ -68,5 +69,22 @@ namespace Oqtane.Models
}
}
+ ///
+ /// Unique key used for identifying a site within a runtime process (ie. cache, etc...)
+ ///
+ [NotMapped]
+ public string SiteKey
+ {
+ get
+ {
+ return TenantId.ToString() + ":" + SiteId.ToString();
+ }
+ }
+
+ ///
+ /// Site-specific settings
+ ///
+ [NotMapped]
+ public Dictionary SiteSettings { get; set; }
}
}
diff --git a/Oqtane.Shared/Shared/Constants.cs b/Oqtane.Shared/Shared/Constants.cs
index 50848d85..b81c2375 100644
--- a/Oqtane.Shared/Shared/Constants.cs
+++ b/Oqtane.Shared/Shared/Constants.cs
@@ -85,5 +85,8 @@ namespace Oqtane.Shared {
public static readonly string AntiForgeryTokenCookieName = "X-XSRF-TOKEN-COOKIE";
public static readonly string DefaultVisitorFilter = "bot,crawler,slurp,spider,(none),??";
+
+ public static readonly string HttpContextAliasKey = "SiteState.Alias";
+ public static readonly string SiteToken = "{SiteToken}";
}
}
diff --git a/Oqtane.Shared/Shared/SiteState.cs b/Oqtane.Shared/Shared/SiteState.cs
index 67d5cbd8..9a7320e3 100644
--- a/Oqtane.Shared/Shared/SiteState.cs
+++ b/Oqtane.Shared/Shared/SiteState.cs
@@ -2,7 +2,7 @@ using Oqtane.Models;
namespace Oqtane.Shared
{
- // this class is used for passing state between components and services, or controllers and repositories
+ // this class is used for passing state between components and services as well as controllers and repositories
public class SiteState
{
public Alias Alias { get; set; }