Added support for per site options and OpenID Connect
This commit is contained in:
parent
a47ecbdea9
commit
9bbbff31f8
@ -18,14 +18,19 @@
|
||||
@if (!twofactor)
|
||||
{
|
||||
<form @ref="login" class="@(validated ? "was-validated" : "needs-validation")" novalidate>
|
||||
<div class="container Oqtane-Modules-Admin-Login" @onkeypress="@(e => KeyPressed(e))">
|
||||
<div class="Oqtane-Modules-Admin-Login" @onkeypress="@(e => KeyPressed(e))">
|
||||
@if (PageState.Site.Settings.ContainsKey("OpenIdConnectOptions:Provider") && !string.IsNullOrEmpty(PageState.Site.Settings["OpenIdConnectOptions:Provider"]))
|
||||
{
|
||||
<button type="button" class="btn btn-primary" @onclick="ExternalLogin">Use @PageState.Site.Settings["OpenIdConnectOptions:Provider"]</button>
|
||||
<hr />
|
||||
}
|
||||
<div class="form-group">
|
||||
<Label Class="control-label" For="username" HelpText="Please enter your Username" ResourceKey="Username">Username:</Label>
|
||||
<input id="username" type="text" @ref="username" class="form-control input" placeholder="@Localizer["Username.Placeholder"]" @bind="@_username" required />
|
||||
<input id="username" type="text" @ref="username" class="form-control" placeholder="@Localizer["Username.Placeholder"]" @bind="@_username" required />
|
||||
</div>
|
||||
<div class="form-group mt-2">
|
||||
<Label Class="control-label" For="password" HelpText="Please enter your Password" ResourceKey="Password">Password:</Label>
|
||||
<div class="input-group password">
|
||||
<div class="input-group">
|
||||
<input id="password" type="@_passwordtype" name="Password" class="form-control" placeholder="@Localizer["Password.Placeholder"]" @bind="@_password" required />
|
||||
<button type="button" class="btn btn-secondary" @onclick="@TogglePassword">@_togglepassword</button>
|
||||
</div>
|
||||
@ -49,7 +54,7 @@
|
||||
<div class="container Oqtane-Modules-Admin-Login">
|
||||
<div class="form-group">
|
||||
<Label Class="control-label" For="code" HelpText="Please enter the secure verification code which was sent to you by email" ResourceKey="Code">Verification Code:</Label>
|
||||
<input id="code" class="form-control input" @bind="@_code" placeholder="@Localizer["Code.Placeholder"]" maxlength="6" required />
|
||||
<input id="code" class="form-control" @bind="@_code" placeholder="@Localizer["Code.Placeholder"]" maxlength="6" required />
|
||||
</div>
|
||||
<br />
|
||||
<button type="button" class="btn btn-primary" @onclick="Login">@SharedLocalizer["Login"]</button>
|
||||
@ -245,7 +250,11 @@
|
||||
_passwordtype = "password";
|
||||
_togglepassword = Localizer["ShowPassword"];
|
||||
}
|
||||
//StateHasChanged();
|
||||
}
|
||||
|
||||
private void ExternalLogin()
|
||||
{
|
||||
NavigationManager.NavigateTo(Utilities.TenantUrl(PageState.Alias, "/pages/oidc?returnurl=" + _returnUrl), true);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -4,7 +4,6 @@
|
||||
@inject IUserService UserService
|
||||
@inject ISettingService SettingService
|
||||
@inject ISiteService SiteService
|
||||
@inject ISystemService SystemService
|
||||
@inject IStringLocalizer<Index> Localizer
|
||||
@inject IStringLocalizer<SharedResources> SharedLocalizer
|
||||
|
||||
@ -65,74 +64,97 @@ else
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
@if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
|
||||
{
|
||||
<br />
|
||||
<Section Name="Password" Heading="Password Settings" ResourceKey="PasswordSettings">
|
||||
<div class="row mb-1 align-items-center">
|
||||
<Label Class="col-sm-3" For="minimumlength" HelpText="The Minimum Length For A Password" ResourceKey="RequiredLength">Minimum Length:</Label>
|
||||
<div class="col-sm-9">
|
||||
<input id="minimumlength" class="form-control" @bind="@_minimumlength" required />
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-1 align-items-center">
|
||||
<Label Class="col-sm-3" For="uniquecharacters" HelpText="The Minimum Number Of Unique Characters Which A Password Must Contain" ResourceKey="UniqueCharacters">Unique Characters:</Label>
|
||||
<div class="col-sm-9">
|
||||
<input id="uniquecharacters" class="form-control" @bind="@_uniquecharacters" required />
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-1 align-items-center">
|
||||
<Label Class="col-sm-3" For="requiredigit" HelpText="Indicate If Passwords Must Contain A Digit" ResourceKey="RequireDigit">Require Digit?</Label>
|
||||
<div class="col-sm-9">
|
||||
<select id="requiredigit" class="form-select" @bind="@_requiredigit" required>
|
||||
<option value="true">@SharedLocalizer["Yes"]</option>
|
||||
<option value="false">@SharedLocalizer["No"]</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-1 align-items-center">
|
||||
<Label Class="col-sm-3" For="requireupper" HelpText="Indicate If Passwords Must Contain An Upper Case Character" ResourceKey="RequireUpper">Require Uppercase?</Label>
|
||||
<div class="col-sm-9">
|
||||
<select id="requireupper" class="form-select" @bind="@_requireupper" required>
|
||||
<option value="true">@SharedLocalizer["Yes"]</option>
|
||||
<option value="false">@SharedLocalizer["No"]</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-1 align-items-center">
|
||||
<Label Class="col-sm-3" For="requirelower" HelpText="Indicate If Passwords Must Contain A Lower Case Character" ResourceKey="RequireLower">Require Lowercase?</Label>
|
||||
<div class="col-sm-9">
|
||||
<select id="requirelower" class="form-select" @bind="@_requirelower" required>
|
||||
<option value="true">@SharedLocalizer["Yes"]</option>
|
||||
<option value="false">@SharedLocalizer["No"]</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-1 align-items-center">
|
||||
<Label Class="col-sm-3" For="requirepunctuation" HelpText="Indicate if Passwords Must Contain A Non-alphanumeric Character (ie. Punctuation)" ResourceKey="RequirePunctuation">Require Punctuation?</Label>
|
||||
<div class="col-sm-9">
|
||||
<select id="requirepunctuation" class="form-select" @bind="@_requirepunctuation" required>
|
||||
<option value="true">@SharedLocalizer["Yes"]</option>
|
||||
<option value="false">@SharedLocalizer["No"]</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
<Section Name="Lockout" Heading="Lockout Settings" ResourceKey="LockoutSettings">
|
||||
<div class="row mb-1 align-items-center">
|
||||
<Label Class="col-sm-3" For="maximum" HelpText="The Maximum Number Of Sign In Attempts Before A User Is Locked Out" ResourceKey="MaximumFailures">Maximum Failures:</Label>
|
||||
<div class="col-sm-9">
|
||||
<input id="maximum" class="form-control" @bind="@_maximumfailures" required />
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-1 align-items-center">
|
||||
<Label Class="col-sm-3" For="lockoutduration" HelpText="The Number Of Minutes A User Should Be Locked Out" ResourceKey="LockoutDuration">Lockout Duration:</Label>
|
||||
<div class="col-sm-9">
|
||||
<input id="lockoutduration" class="form-control" @bind="@_lockoutduration" required />
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
}
|
||||
<br />
|
||||
<Section Name="Password" Heading="Password Settings" ResourceKey="PasswordSettings">
|
||||
<div class="row mb-1 align-items-center">
|
||||
<Label Class="col-sm-3" For="minimumlength" HelpText="The Minimum Length For A Password" ResourceKey="RequiredLength">Minimum Length:</Label>
|
||||
<div class="col-sm-9">
|
||||
<input id="minimumlength" class="form-control" @bind="@_minimumlength" required />
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-1 align-items-center">
|
||||
<Label Class="col-sm-3" For="uniquecharacters" HelpText="The Minimum Number Of Unique Characters Which A Password Must Contain" ResourceKey="UniqueCharacters">Unique Characters:</Label>
|
||||
<div class="col-sm-9">
|
||||
<input id="uniquecharacters" class="form-control" @bind="@_uniquecharacters" required />
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-1 align-items-center">
|
||||
<Label Class="col-sm-3" For="requiredigit" HelpText="Indicate If Passwords Must Contain A Digit" ResourceKey="RequireDigit">Require Digit?</Label>
|
||||
<div class="col-sm-9">
|
||||
<select id="requiredigit" class="form-select" @bind="@_requiredigit" required>
|
||||
<option value="true">@SharedLocalizer["Yes"]</option>
|
||||
<option value="false">@SharedLocalizer["No"]</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-1 align-items-center">
|
||||
<Label Class="col-sm-3" For="requireupper" HelpText="Indicate If Passwords Must Contain An Upper Case Character" ResourceKey="RequireUpper">Require Uppercase?</Label>
|
||||
<div class="col-sm-9">
|
||||
<select id="requireupper" class="form-select" @bind="@_requireupper" required>
|
||||
<option value="true">@SharedLocalizer["Yes"]</option>
|
||||
<option value="false">@SharedLocalizer["No"]</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-1 align-items-center">
|
||||
<Label Class="col-sm-3" For="requirelower" HelpText="Indicate If Passwords Must Contain A Lower Case Character" ResourceKey="RequireLower">Require Lowercase?</Label>
|
||||
<div class="col-sm-9">
|
||||
<select id="requirelower" class="form-select" @bind="@_requirelower" required>
|
||||
<option value="true">@SharedLocalizer["Yes"]</option>
|
||||
<option value="false">@SharedLocalizer["No"]</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-1 align-items-center">
|
||||
<Label Class="col-sm-3" For="requirepunctuation" HelpText="Indicate if Passwords Must Contain A Non-alphanumeric Character (ie. Punctuation)" ResourceKey="RequirePunctuation">Require Punctuation?</Label>
|
||||
<div class="col-sm-9">
|
||||
<select id="requirepunctuation" class="form-select" @bind="@_requirepunctuation" required>
|
||||
<option value="true">@SharedLocalizer["Yes"]</option>
|
||||
<option value="false">@SharedLocalizer["No"]</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
<Section Name="Lockout" Heading="Lockout Settings" ResourceKey="LockoutSettings">
|
||||
<div class="row mb-1 align-items-center">
|
||||
<Label Class="col-sm-3" For="maximum" HelpText="The Maximum Number Of Sign In Attempts Before A User Is Locked Out" ResourceKey="MaximumFailures">Maximum Failures:</Label>
|
||||
<div class="col-sm-9">
|
||||
<input id="maximum" class="form-control" @bind="@_maximumfailures" required />
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-1 align-items-center">
|
||||
<Label Class="col-sm-3" For="lockoutduration" HelpText="The Number Of Minutes A User Should Be Locked Out" ResourceKey="LockoutDuration">Lockout Duration:</Label>
|
||||
<div class="col-sm-9">
|
||||
<input id="lockoutduration" class="form-control" @bind="@_lockoutduration" required />
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
<Section Name="OpenIDConnect" Heading="OpenID Connect Settings" ResourceKey="OpenIDConnectSettings">
|
||||
<div class="row mb-1 align-items-center">
|
||||
<Label Class="col-sm-3" For="provider" HelpText="The OpenID Connect Provider Name" ResourceKey="Provider">Provider:</Label>
|
||||
<div class="col-sm-9">
|
||||
<input id="provider" class="form-control" @bind="@_provider" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-1 align-items-center">
|
||||
<Label Class="col-sm-3" For="authority" HelpText="The OpenID Connect Authority" ResourceKey="Authority">Authority:</Label>
|
||||
<div class="col-sm-9">
|
||||
<input id="authority" class="form-control" @bind="@_authority" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-1 align-items-center">
|
||||
<Label Class="col-sm-3" For="clientid" HelpText="The OpenID Connect Client ID" ResourceKey="ClientID">Client ID:</Label>
|
||||
<div class="col-sm-9">
|
||||
<input id="clientid" class="form-control" @bind="@_clientid" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-1 align-items-center">
|
||||
<Label Class="col-sm-3" For="clientsecret" HelpText="The OpenID Connect Client Secret" ResourceKey="ClientSecret">Client Secret:</Label>
|
||||
<div class="col-sm-9">
|
||||
<input id="clientsecret" class="form-control" @bind="@_clientsecret" />
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
</div>
|
||||
<br />
|
||||
<button type="button" class="btn btn-success" @onclick="SaveSiteSettings">@SharedLocalizer["Save"]</button>
|
||||
@ -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<string, object> 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<UserRole> 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<string, object>();
|
||||
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)
|
||||
{
|
||||
|
@ -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 };
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
18
Oqtane.Server/Extensions/HttpContextExtensions.cs
Normal file
18
Oqtane.Server/Extensions/HttpContextExtensions.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
{
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
39
Oqtane.Server/Extensions/OqtaneSiteOptionsBuilder.cs
Normal file
39
Oqtane.Server/Extensions/OqtaneSiteOptionsBuilder.cs
Normal 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 });
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -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
|
||||
{
|
||||
|
18
Oqtane.Server/Infrastructure/AliasAccessor.cs
Normal file
18
Oqtane.Server/Infrastructure/AliasAccessor.cs
Normal file
@ -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();
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
using Oqtane.Models;
|
||||
|
||||
namespace Oqtane.Infrastructure
|
||||
{
|
||||
public interface IAliasAccessor
|
||||
{
|
||||
Alias Alias { get; }
|
||||
}
|
||||
}
|
@ -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<AuthenticationOptions> options)
|
||||
: this(options, new Dictionary<string, AuthenticationScheme>(StringComparer.Ordinal))
|
||||
{
|
||||
}
|
||||
|
||||
public SiteAuthenticationSchemeProvider(IOptions<AuthenticationOptions> options, IDictionary<string, AuthenticationScheme> schemes)
|
||||
{
|
||||
_optionsProvider = options;
|
||||
|
||||
_schemes = schemes ?? throw new ArgumentNullException(nameof(schemes));
|
||||
_requestHandlers = new List<AuthenticationScheme>();
|
||||
|
||||
foreach (var builder in _optionsProvider.Value.Schemes)
|
||||
{
|
||||
var scheme = builder.Build();
|
||||
AddScheme(scheme);
|
||||
}
|
||||
}
|
||||
|
||||
private readonly IOptions<AuthenticationOptions> _optionsProvider;
|
||||
private readonly object _lock = new object();
|
||||
|
||||
private readonly IDictionary<string, AuthenticationScheme> _schemes;
|
||||
private readonly List<AuthenticationScheme> _requestHandlers;
|
||||
|
||||
private Task<AuthenticationScheme> GetDefaultSchemeAsync()
|
||||
=> _optionsProvider.Value.DefaultScheme != null
|
||||
? GetSchemeAsync(_optionsProvider.Value.DefaultScheme)
|
||||
: Task.FromResult<AuthenticationScheme>(null);
|
||||
|
||||
public virtual Task<AuthenticationScheme> GetDefaultAuthenticateSchemeAsync()
|
||||
=> _optionsProvider.Value.DefaultAuthenticateScheme != null
|
||||
? GetSchemeAsync(_optionsProvider.Value.DefaultAuthenticateScheme)
|
||||
: GetDefaultSchemeAsync();
|
||||
|
||||
public virtual Task<AuthenticationScheme> GetDefaultChallengeSchemeAsync()
|
||||
=> _optionsProvider.Value.DefaultChallengeScheme != null
|
||||
? GetSchemeAsync(_optionsProvider.Value.DefaultChallengeScheme)
|
||||
: GetDefaultSchemeAsync();
|
||||
|
||||
public virtual Task<AuthenticationScheme> GetDefaultForbidSchemeAsync()
|
||||
=> _optionsProvider.Value.DefaultForbidScheme != null
|
||||
? GetSchemeAsync(_optionsProvider.Value.DefaultForbidScheme)
|
||||
: GetDefaultChallengeSchemeAsync();
|
||||
|
||||
public virtual Task<AuthenticationScheme> GetDefaultSignInSchemeAsync()
|
||||
=> _optionsProvider.Value.DefaultSignInScheme != null
|
||||
? GetSchemeAsync(_optionsProvider.Value.DefaultSignInScheme)
|
||||
: GetDefaultSchemeAsync();
|
||||
|
||||
public virtual Task<AuthenticationScheme> GetDefaultSignOutSchemeAsync()
|
||||
=> _optionsProvider.Value.DefaultSignOutScheme != null
|
||||
? GetSchemeAsync(_optionsProvider.Value.DefaultSignOutScheme)
|
||||
: GetDefaultSignInSchemeAsync();
|
||||
|
||||
public virtual Task<AuthenticationScheme> GetSchemeAsync(string name)
|
||||
=> Task.FromResult(_schemes.ContainsKey(name) ? _schemes[name] : null);
|
||||
|
||||
public virtual Task<IEnumerable<AuthenticationScheme>> GetRequestHandlerSchemesAsync()
|
||||
=> Task.FromResult<IEnumerable<AuthenticationScheme>>(_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<IEnumerable<AuthenticationScheme>> GetAllSchemesAsync()
|
||||
=> Task.FromResult<IEnumerable<AuthenticationScheme>>(_schemes.Values);
|
||||
}
|
||||
}
|
@ -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<TAlias> : 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<AuthenticateResult> 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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
|
12
Oqtane.Server/Infrastructure/Options/ISiteOptions.cs
Normal file
12
Oqtane.Server/Infrastructure/Options/ISiteOptions.cs
Normal file
@ -0,0 +1,12 @@
|
||||
|
||||
using Oqtane.Models;
|
||||
|
||||
namespace Oqtane.Infrastructure
|
||||
{
|
||||
public interface ISiteOptions<TOptions, TAlias>
|
||||
where TOptions : class, new()
|
||||
where TAlias : class, IAlias, new()
|
||||
{
|
||||
void Configure(TOptions options, TAlias siteOptions);
|
||||
}
|
||||
}
|
22
Oqtane.Server/Infrastructure/Options/SiteOptions.cs
Normal file
22
Oqtane.Server/Infrastructure/Options/SiteOptions.cs
Normal file
@ -0,0 +1,22 @@
|
||||
using System;
|
||||
using Oqtane.Models;
|
||||
|
||||
namespace Oqtane.Infrastructure
|
||||
{
|
||||
public class SiteOptions<TOptions, TAlias> : ISiteOptions<TOptions, TAlias>
|
||||
where TOptions : class, new()
|
||||
where TAlias : class, IAlias, new()
|
||||
{
|
||||
private readonly Action<TOptions, TAlias> configureOptions;
|
||||
|
||||
public SiteOptions(Action<TOptions, TAlias> configureOptions)
|
||||
{
|
||||
this.configureOptions = configureOptions;
|
||||
}
|
||||
|
||||
public void Configure(TOptions options, TAlias siteOptions)
|
||||
{
|
||||
configureOptions(options, siteOptions);
|
||||
}
|
||||
}
|
||||
}
|
70
Oqtane.Server/Infrastructure/Options/SiteOptionsCache.cs
Normal file
70
Oqtane.Server/Infrastructure/Options/SiteOptionsCache.cs
Normal file
@ -0,0 +1,70 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Oqtane.Models;
|
||||
|
||||
namespace Oqtane.Infrastructure
|
||||
{
|
||||
public class SiteOptionsCache<TOptions, TAlias> : IOptionsMonitorCache<TOptions>
|
||||
where TOptions : class
|
||||
where TAlias : class, IAlias, new()
|
||||
{
|
||||
private readonly IAliasAccessor _aliasAccessor;
|
||||
private readonly ConcurrentDictionary<string, IOptionsMonitorCache<TOptions>> map = new ConcurrentDictionary<string, IOptionsMonitorCache<TOptions>>();
|
||||
|
||||
public SiteOptionsCache(IAliasAccessor aliasAccessor)
|
||||
{
|
||||
_aliasAccessor = aliasAccessor;
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
var cache = map.GetOrAdd(GetKey(), new OptionsCache<TOptions>());
|
||||
cache.Clear();
|
||||
}
|
||||
|
||||
public void Clear(Alias alias)
|
||||
{
|
||||
var cache = map.GetOrAdd(alias.SiteKey, new OptionsCache<TOptions>());
|
||||
|
||||
cache.Clear();
|
||||
}
|
||||
|
||||
public void ClearAll()
|
||||
{
|
||||
foreach (var cache in map.Values)
|
||||
{
|
||||
cache.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
public TOptions GetOrAdd(string name, Func<TOptions> createOptions)
|
||||
{
|
||||
name = name ?? Options.DefaultName;
|
||||
var cache = map.GetOrAdd(GetKey(), new OptionsCache<TOptions>());
|
||||
|
||||
return cache.GetOrAdd(name, createOptions);
|
||||
}
|
||||
|
||||
public bool TryAdd(string name, TOptions options)
|
||||
{
|
||||
name = name ?? Options.DefaultName;
|
||||
var cache = map.GetOrAdd(GetKey(), new OptionsCache<TOptions>());
|
||||
|
||||
return cache.TryAdd(name, options);
|
||||
}
|
||||
|
||||
public bool TryRemove(string name)
|
||||
{
|
||||
name = name ?? Options.DefaultName;
|
||||
var cache = map.GetOrAdd(GetKey(), new OptionsCache<TOptions>());
|
||||
|
||||
return cache.TryRemove(name);
|
||||
}
|
||||
|
||||
private string GetKey()
|
||||
{
|
||||
return _aliasAccessor?.Alias?.SiteKey ?? "";
|
||||
}
|
||||
}
|
||||
}
|
77
Oqtane.Server/Infrastructure/Options/SiteOptionsFactory.cs
Normal file
77
Oqtane.Server/Infrastructure/Options/SiteOptionsFactory.cs
Normal file
@ -0,0 +1,77 @@
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Oqtane.Models;
|
||||
|
||||
namespace Oqtane.Infrastructure
|
||||
{
|
||||
public class SiteOptionsFactory<TOptions, TAlias> : IOptionsFactory<TOptions>
|
||||
where TOptions : class, new()
|
||||
where TAlias : class, IAlias, new()
|
||||
{
|
||||
private readonly IConfigureOptions<TOptions>[] _configureOptions;
|
||||
private readonly IPostConfigureOptions<TOptions>[] _postConfigureOptions;
|
||||
private readonly IValidateOptions<TOptions>[] _validations;
|
||||
private readonly ISiteOptions<TOptions, TAlias>[] _siteOptions;
|
||||
private readonly IAliasAccessor _aliasAccessor;
|
||||
|
||||
public SiteOptionsFactory(IEnumerable<IConfigureOptions<TOptions>> configureOptions, IEnumerable<IPostConfigureOptions<TOptions>> postConfigureOptions, IEnumerable<IValidateOptions<TOptions>> validations, IEnumerable<ISiteOptions<TOptions, TAlias>> siteOptions, IAliasAccessor aliasAccessor)
|
||||
{
|
||||
_configureOptions = configureOptions as IConfigureOptions<TOptions>[] ?? new List<IConfigureOptions<TOptions>>(configureOptions).ToArray();
|
||||
_postConfigureOptions = postConfigureOptions as IPostConfigureOptions<TOptions>[] ?? new List<IPostConfigureOptions<TOptions>>(postConfigureOptions).ToArray();
|
||||
_validations = validations as IValidateOptions<TOptions>[] ?? new List<IValidateOptions<TOptions>>(validations).ToArray();
|
||||
_siteOptions = siteOptions as ISiteOptions<TOptions, TAlias>[] ?? new List<ISiteOptions<TOptions, TAlias>>(siteOptions).ToArray();
|
||||
_aliasAccessor = aliasAccessor;
|
||||
}
|
||||
|
||||
public TOptions Create(string name)
|
||||
{
|
||||
// default options
|
||||
var options = new TOptions();
|
||||
foreach (var setup in _configureOptions)
|
||||
{
|
||||
if (setup is IConfigureNamedOptions<TOptions> 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<string>();
|
||||
// foreach (IValidateOptions<TOptions> 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;
|
||||
}
|
||||
}
|
||||
}
|
35
Oqtane.Server/Infrastructure/Options/SiteOptionsManager.cs
Normal file
35
Oqtane.Server/Infrastructure/Options/SiteOptionsManager.cs
Normal file
@ -0,0 +1,35 @@
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Oqtane.Infrastructure
|
||||
{
|
||||
public class SiteOptionsManager<TOptions> : IOptions<TOptions>, IOptionsSnapshot<TOptions> where TOptions : class, new()
|
||||
{
|
||||
private readonly IOptionsFactory<TOptions> _factory;
|
||||
private readonly IOptionsMonitorCache<TOptions> _cache; // private cache
|
||||
|
||||
public SiteOptionsManager(IOptionsFactory<TOptions> factory, IOptionsMonitorCache<TOptions> 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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
@ -32,6 +32,7 @@
|
||||
<EmbeddedResource Include="Scripts\MigrateTenant.sql" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="6.0.3" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="6.0.0" />
|
||||
|
3
Oqtane.Server/Pages/OIDC.cshtml
Normal file
3
Oqtane.Server/Pages/OIDC.cshtml
Normal file
@ -0,0 +1,3 @@
|
||||
@page "/pages/oidc"
|
||||
@namespace Oqtane.Pages
|
||||
@model Oqtane.Pages.OIDCModel
|
15
Oqtane.Server/Pages/OIDC.cshtml.cs
Normal file
15
Oqtane.Server/Pages/OIDC.cshtml.cs
Normal file
@ -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 : "/" });
|
||||
}
|
||||
}
|
||||
}
|
@ -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<UserRole> 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<UserRole> 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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<IAuthorizationHandler, PermissionHandler>()
|
||||
.AddOqtaneScopedServices();
|
||||
|
||||
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
|
||||
|
||||
services.AddIdentityCore<IdentityUser>(options => { })
|
||||
.AddEntityFrameworkStores<TenantDBContext>()
|
||||
.AddSignInManager()
|
||||
.AddDefaultTokenProviders()
|
||||
.AddClaimsPrincipalFactory<ClaimsPrincipalFactory<IdentityUser>>(); // 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<IdentityUser>(options => { })
|
||||
.AddEntityFrameworkStores<TenantDBContext>()
|
||||
.AddSignInManager()
|
||||
.AddDefaultTokenProviders()
|
||||
.AddClaimsPrincipalFactory<ClaimsPrincipalFactory<IdentityUser>>(); // role claims
|
||||
|
||||
services.ConfigureOqtaneIdentityOptions(Configuration);
|
||||
|
||||
services.AddAuthentication(options =>
|
||||
{
|
||||
options.DefaultAuthenticateScheme = Constants.AuthenticationScheme;
|
||||
options.DefaultChallengeScheme = Constants.AuthenticationScheme;
|
||||
})
|
||||
.AddCookie(Constants.AuthenticationScheme)
|
||||
.AddOpenIdConnect();
|
||||
|
||||
services.ConfigureOqtaneCookieOptions();
|
||||
|
||||
services.AddOqtaneSiteOptions<Alias>()
|
||||
.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 =>
|
||||
{
|
||||
|
@ -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;
|
||||
}
|
21
Oqtane.Shared/Interfaces/IAlias.cs
Normal file
21
Oqtane.Shared/Interfaces/IAlias.cs
Normal file
@ -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<string, string> SiteSettings { get; set; }
|
||||
}
|
||||
}
|
@ -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
|
||||
/// <summary>
|
||||
/// An Alias maps a url like `oqtane.my` or `oqtane.my/products` to a <see cref="Oqtane.Models.Site"/> and <see cref="Oqtane.Models.Tenant"/>
|
||||
/// </summary>
|
||||
public class Alias : IAuditable
|
||||
public class Alias : IAlias, IAuditable
|
||||
{
|
||||
/// <summary>
|
||||
/// The primary ID for internal use. It's also used in API calls to identify the site.
|
||||
@ -68,5 +69,22 @@ namespace Oqtane.Models
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unique key used for identifying a site within a runtime process (ie. cache, etc...)
|
||||
/// </summary>
|
||||
[NotMapped]
|
||||
public string SiteKey
|
||||
{
|
||||
get
|
||||
{
|
||||
return TenantId.ToString() + ":" + SiteId.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Site-specific settings
|
||||
/// </summary>
|
||||
[NotMapped]
|
||||
public Dictionary<string, string> SiteSettings { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -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}";
|
||||
}
|
||||
}
|
||||
|
@ -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; }
|
||||
|
Loading…
x
Reference in New Issue
Block a user