factor out auth constants, remove TAlias is Alias is not an extensible type, improve SiteOptions cache clearing, improve principal validation, localization improvements

This commit is contained in:
Shaun Walker 2022-03-26 17:30:06 -04:00
parent 79f427e10a
commit b92a888583
22 changed files with 113 additions and 111 deletions

View File

@ -42,7 +42,7 @@ else
</div> </div>
</div> </div>
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="twofactor" HelpText="Indicates if you are using two factor authentication" ResourceKey="TwoFactor"></Label> <Label Class="col-sm-3" For="twofactor" HelpText="Indicates if you are using two factor authentication. Two factor authentication requires you to enter a verification code sent via email after you sign in." ResourceKey="TwoFactor"></Label>
<div class="col-sm-9"> <div class="col-sm-9">
<select id="twofactor" class="form-select" @bind="@twofactor" required> <select id="twofactor" class="form-select" @bind="@twofactor" required>
<option value="True">@SharedLocalizer["Yes"]</option> <option value="True">@SharedLocalizer["Yes"]</option>

View File

@ -135,8 +135,8 @@ else
<div class="col-sm-9"> <div class="col-sm-9">
<select id="providertype" class="form-select" value="@_providertype" @onchange="(e => ProviderTypeChanged(e))"> <select id="providertype" class="form-select" value="@_providertype" @onchange="(e => ProviderTypeChanged(e))">
<option value="" selected>@Localizer["Not Specified"]</option> <option value="" selected>@Localizer["Not Specified"]</option>
<option value="oidc">@Localizer["OpenID Connect"]</option> <option value="@AuthenticationProviderTypes.OpenIDConnect">@Localizer["OpenID Connect"]</option>
<option value="oauth2">@Localizer["OAuth 2.0"]</option> <option value="@AuthenticationProviderTypes.OAuth2">@Localizer["OAuth 2.0"]</option>
</select> </select>
</div> </div>
</div> </div>
@ -149,7 +149,7 @@ else
</div> </div>
</div> </div>
} }
@if (_providertype == "oidc") @if (_providertype == AuthenticationProviderTypes.OpenIDConnect)
{ {
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="authority" HelpText="The Authority Url or Issuer Url associated with the OpenID Connect provider" ResourceKey="Authority">Authority:</Label> <Label Class="col-sm-3" For="authority" HelpText="The Authority Url or Issuer Url associated with the OpenID Connect provider" ResourceKey="Authority">Authority:</Label>
@ -164,7 +164,7 @@ else
</div> </div>
</div> </div>
} }
@if (_providertype == "oauth2") @if (_providertype == AuthenticationProviderTypes.OAuth2)
{ {
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="authorizationurl" HelpText="The endpoint for obtaining an Authorization Code" ResourceKey="AuthorizationUrl">Authorization Url:</Label> <Label Class="col-sm-3" For="authorizationurl" HelpText="The endpoint for obtaining an Authorization Code" ResourceKey="AuthorizationUrl">Authorization Url:</Label>
@ -220,7 +220,7 @@ else
<input id="redirecturl" class="form-control" @bind="@_redirecturl" readonly /> <input id="redirecturl" class="form-control" @bind="@_redirecturl" readonly />
</div> </div>
</div> </div>
@if (_providertype == "oidc") @if (_providertype == AuthenticationProviderTypes.OpenIDConnect)
{ {
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="emailclaimtype" HelpText="The type name for the email address claim provided by the provider" ResourceKey="EmailClaimType">Email Claim Type:</Label> <Label Class="col-sm-3" For="emailclaimtype" HelpText="The type name for the email address claim provided by the provider" ResourceKey="EmailClaimType">Email Claim Type:</Label>
@ -440,7 +440,7 @@ else
private void ProviderTypeChanged(ChangeEventArgs e) private void ProviderTypeChanged(ChangeEventArgs e)
{ {
_providertype = (string)e.Value; _providertype = (string)e.Value;
if (_providertype == "oidc") if (_providertype == AuthenticationProviderTypes.OpenIDConnect)
{ {
_scopes = "openid,profile,email"; _scopes = "openid,profile,email";
} }

View File

@ -52,7 +52,7 @@
<input type="file" id="@_fileinputid" name="file" accept="@_filter" /> <input type="file" id="@_fileinputid" name="file" accept="@_filter" />
} }
</div> </div>
<div class="col mt-2 text-center"> <div class="col mt-2 text-end">
<button type="button" class="btn btn-success" @onclick="UploadFile">@SharedLocalizer["Upload"]</button> <button type="button" class="btn btn-success" @onclick="UploadFile">@SharedLocalizer["Upload"]</button>
@if (ShowFiles && GetFileId() != -1) @if (ShowFiles && GetFileId() != -1)
{ {

View File

@ -127,7 +127,7 @@
<value>User Account Could Not Be Verified. Please Contact Your Administrator For Further Instructions.</value> <value>User Account Could Not Be Verified. Please Contact Your Administrator For Further Instructions.</value>
</data> </data>
<data name="Error.Login.Fail" xml:space="preserve"> <data name="Error.Login.Fail" xml:space="preserve">
<value>Login Failed. Please Remember That Passwords Are Case Sensitive. If You Have Attempted To Sign In 3 Times Unsuccessfully, Your Account Will Be Locked Out For 10 Minutes. Note That User Accounts Require Verification When They Are Initially Created So You May Wish To Check Your Email If You Are A New User.</value> <value>Login Failed. Please Remember That Passwords Are Case Sensitive. If You Have Attempted To Sign In Multiple Times Unsuccessfully, Your Account Will Be Locked Out For A Period Of Time. Note That User Accounts Require Verification When They Are Initially Created So You May Wish To Check Your Email If You Are A New User.</value>
</data> </data>
<data name="Message.Required.UserInfo" xml:space="preserve"> <data name="Message.Required.UserInfo" xml:space="preserve">
<value>Please Provide All Required Fields</value> <value>Please Provide All Required Fields</value>

View File

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<root> <root>
<!-- <!--
Microsoft ResX Schema Microsoft ResX Schema
@ -169,10 +169,10 @@
<value>Identity</value> <value>Identity</value>
</data> </data>
<data name="Confirm.HelpText" xml:space="preserve"> <data name="Confirm.HelpText" xml:space="preserve">
<value>If you are changing your password you must enter it again to confirm it matches</value> <value>If you are changing your password you must enter it again to confirm it matches the value entered above</value>
</data> </data>
<data name="Confirm.Text" xml:space="preserve"> <data name="Confirm.Text" xml:space="preserve">
<value>Confirm Password:</value> <value>Confirmation:</value>
</data> </data>
<data name="DisplayName.HelpText" xml:space="preserve"> <data name="DisplayName.HelpText" xml:space="preserve">
<value>Your full name</value> <value>Your full name</value>
@ -205,9 +205,9 @@
<value>Username:</value> <value>Username:</value>
</data> </data>
<data name="TwoFactor.HelpText" xml:space="preserve"> <data name="TwoFactor.HelpText" xml:space="preserve">
<value>Indicates if you are using two factor authentication</value> <value>Indicates if you are using two factor authentication. Two factor authentication requires you to enter a verification code sent via email after you sign in.</value>
</data> </data>
<data name="TwoFactor.Text" xml:space="preserve"> <data name="TwoFactor.Text" xml:space="preserve">
<value>Two Factor Authentication?</value> <value>Two Factor?</value>
</data> </data>
</root> </root>

View File

@ -21,7 +21,9 @@ namespace Oqtane.Services
_siteState = siteState; _siteState = siteState;
} }
private string ApiUrl => CreateApiUrl("Installation", null, ControllerRoutes.ApiRoute); // tenant agnostic private string ApiUrl => (_siteState.Alias == null)
? CreateApiUrl("Installation", null, ControllerRoutes.ApiRoute) // tenant agnostic needed for initial installation
: CreateApiUrl("Installation", _siteState.Alias);
public async Task<Installation> IsInstalled() public async Task<Installation> IsInstalled()
{ {

View File

@ -17,7 +17,6 @@ namespace Oqtane.Services
public SiteService(HttpClient http, SiteState siteState) : base(http) public SiteService(HttpClient http, SiteState siteState) : base(http)
{ {
_siteState = siteState; _siteState = siteState;
} }

View File

@ -8,9 +8,10 @@ using Oqtane.Enums;
using Oqtane.Infrastructure; using Oqtane.Infrastructure;
using Oqtane.Repository; using Oqtane.Repository;
using System.Net; using System.Net;
using Microsoft.Extensions.Options;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.AspNetCore.Authentication.OAuth;
using Microsoft.AspNetCore.Identity;
namespace Oqtane.Controllers namespace Oqtane.Controllers
{ {
@ -23,16 +24,16 @@ namespace Oqtane.Controllers
private readonly ISyncManager _syncManager; private readonly ISyncManager _syncManager;
private readonly ILogManager _logger; private readonly ILogManager _logger;
private readonly Alias _alias; private readonly Alias _alias;
private readonly IOptionsMonitorCache<OpenIdConnectOptions> _optionsMonitorCache; private readonly IAliasAccessor _aliasAccessor;
private readonly string _visitorCookie; private readonly string _visitorCookie;
public SettingController(ISettingRepository settings, IPageModuleRepository pageModules, IUserPermissions userPermissions, ITenantManager tenantManager, ISyncManager syncManager, IOptionsMonitorCache<OpenIdConnectOptions> optionsMonitorCache, ILogManager logger) public SettingController(ISettingRepository settings, IPageModuleRepository pageModules, IUserPermissions userPermissions, ITenantManager tenantManager, ISyncManager syncManager, IAliasAccessor aliasAccessor, ILogManager logger)
{ {
_settings = settings; _settings = settings;
_pageModules = pageModules; _pageModules = pageModules;
_userPermissions = userPermissions; _userPermissions = userPermissions;
_syncManager = syncManager; _syncManager = syncManager;
_optionsMonitorCache = optionsMonitorCache; _aliasAccessor = aliasAccessor;
_logger = logger; _logger = logger;
_alias = tenantManager.GetAlias(); _alias = tenantManager.GetAlias();
_visitorCookie = "APP_VISITOR_" + _alias.SiteId.ToString(); _visitorCookie = "APP_VISITOR_" + _alias.SiteId.ToString();
@ -141,7 +142,12 @@ namespace Oqtane.Controllers
[Authorize(Roles = RoleNames.Admin)] [Authorize(Roles = RoleNames.Admin)]
public void Clear(int id) public void Clear(int id)
{ {
_optionsMonitorCache.Clear(); var openIdConnectOptionsCache = new SiteOptionsCache<OpenIdConnectOptions>(_aliasAccessor);
openIdConnectOptionsCache.Clear();
var oAuthOptionsCache = new SiteOptionsCache<OAuthOptions>(_aliasAccessor);
oAuthOptionsCache.Clear();
var identityOptionsCache = new SiteOptionsCache<IdentityOptions>(_aliasAccessor);
identityOptionsCache.Clear();
_logger.Log(LogLevel.Information, this, LogFunction.Other, "Site Options Cache Cleared"); _logger.Log(LogLevel.Information, this, LogFunction.Other, "Site Options Cache Cleared");
} }

View File

@ -14,7 +14,6 @@ using System.Net;
using Oqtane.Enums; using Oqtane.Enums;
using Oqtane.Infrastructure; using Oqtane.Infrastructure;
using Oqtane.Repository; using Oqtane.Repository;
using Oqtane.Extensions;
namespace Oqtane.Controllers namespace Oqtane.Controllers
{ {

View File

@ -7,6 +7,8 @@ using System.Net.Http;
using System.Reflection; using System.Reflection;
using System.Runtime.Loader; using System.Runtime.Loader;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication.OAuth;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
@ -15,7 +17,6 @@ using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.OpenApi.Models; using Microsoft.OpenApi.Models;
using Oqtane.Infrastructure; using Oqtane.Infrastructure;
using Oqtane.Models;
using Oqtane.Modules; using Oqtane.Modules;
using Oqtane.Repository; using Oqtane.Repository;
using Oqtane.Security; using Oqtane.Security;
@ -59,10 +60,9 @@ namespace Microsoft.Extensions.DependencyInjection
return services; return services;
} }
public static OqtaneSiteOptionsBuilder<T> AddOqtaneSiteOptions<T>(this IServiceCollection services) public static OqtaneSiteOptionsBuilder AddOqtaneSiteOptions(this IServiceCollection services)
where T : class, IAlias, new()
{ {
return new OqtaneSiteOptionsBuilder<T>(services); return new OqtaneSiteOptionsBuilder(services);
} }
internal static IServiceCollection AddOqtaneSingletonServices(this IServiceCollection services) internal static IServiceCollection AddOqtaneSingletonServices(this IServiceCollection services)
@ -144,6 +144,15 @@ namespace Microsoft.Extensions.DependencyInjection
return services; return services;
} }
public static IServiceCollection ConfigureOqtaneAuthenticationOptions(this IServiceCollection services, IConfigurationRoot Configuration)
{
// settings defined in appsettings
services.Configure<OAuthOptions>(Configuration);
services.Configure<OpenIdConnectOptions>(Configuration);
return services;
}
public static IServiceCollection ConfigureOqtaneIdentityOptions(this IServiceCollection services, IConfigurationRoot Configuration) public static IServiceCollection ConfigureOqtaneIdentityOptions(this IServiceCollection services, IConfigurationRoot Configuration)
{ {
// default settings // default settings

View File

@ -11,7 +11,6 @@ using System.Security.Claims;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
using Oqtane.Repository; using Oqtane.Repository;
using System.IO;
using System.Collections.Generic; using System.Collections.Generic;
using Oqtane.Security; using Oqtane.Security;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
@ -24,30 +23,19 @@ namespace Oqtane.Extensions
{ {
public static class OqtaneSiteAuthenticationBuilderExtensions public static class OqtaneSiteAuthenticationBuilderExtensions
{ {
public static OqtaneSiteOptionsBuilder<TAlias> WithSiteAuthentication<TAlias>( public static OqtaneSiteOptionsBuilder WithSiteAuthentication(this OqtaneSiteOptionsBuilder builder)
this OqtaneSiteOptionsBuilder<TAlias> builder)
where TAlias : class, IAlias, new()
{
builder.WithSiteAuthenticationOptions();
return builder;
}
public static OqtaneSiteOptionsBuilder<TAlias> WithSiteAuthenticationOptions<TAlias>(
this OqtaneSiteOptionsBuilder<TAlias> builder)
where TAlias : class, IAlias, new()
{ {
// site OpenIdConnect options // site OpenIdConnect options
builder.AddSiteOptions<OpenIdConnectOptions>((options, alias) => builder.AddSiteOptions<OpenIdConnectOptions>((options, alias) =>
{ {
if (alias.SiteSettings.GetValue("ExternalLogin:ProviderType", "") == "oidc") if (alias.SiteSettings.GetValue("ExternalLogin:ProviderType", "") == AuthenticationProviderTypes.OpenIDConnect)
{ {
// default options // default options
options.SignInScheme = Constants.AuthenticationScheme; // identity cookie options.SignInScheme = Constants.AuthenticationScheme; // identity cookie
options.RequireHttpsMetadata = true; options.RequireHttpsMetadata = true;
options.SaveTokens = true; options.SaveTokens = true;
options.GetClaimsFromUserInfoEndpoint = true; options.GetClaimsFromUserInfoEndpoint = true;
options.CallbackPath = string.IsNullOrEmpty(alias.Path) ? "/signin-oidc" : "/" + alias.Path + "/signin-oidc"; options.CallbackPath = string.IsNullOrEmpty(alias.Path) ? "/signin-" + AuthenticationProviderTypes.OpenIDConnect : "/" + alias.Path + "/signin-" + AuthenticationProviderTypes.OpenIDConnect;
options.ResponseType = OpenIdConnectResponseType.Code; // authorization code flow options.ResponseType = OpenIdConnectResponseType.Code; // authorization code flow
options.ResponseMode = OpenIdConnectResponseMode.FormPost; // recommended as most secure options.ResponseMode = OpenIdConnectResponseMode.FormPost; // recommended as most secure
@ -77,11 +65,11 @@ namespace Oqtane.Extensions
// site OAuth2.0 options // site OAuth2.0 options
builder.AddSiteOptions<OAuthOptions>((options, alias) => builder.AddSiteOptions<OAuthOptions>((options, alias) =>
{ {
if (alias.SiteSettings.GetValue("ExternalLogin:ProviderType", "") == "oauth2") if (alias.SiteSettings.GetValue("ExternalLogin:ProviderType", "") == AuthenticationProviderTypes.OAuth2)
{ {
// default options // default options
options.SignInScheme = Constants.AuthenticationScheme; // identity cookie options.SignInScheme = Constants.AuthenticationScheme; // identity cookie
options.CallbackPath = string.IsNullOrEmpty(alias.Path) ? "/signin-oauth2" : "/" + alias.Path + "/signin-oauth2"; options.CallbackPath = string.IsNullOrEmpty(alias.Path) ? "/signin-" + AuthenticationProviderTypes.OAuth2 : "/" + alias.Path + "/signin-" + AuthenticationProviderTypes.OAuth2;
options.SaveTokens = true; options.SaveTokens = true;
// site options // site options

View File

@ -7,9 +7,7 @@ namespace Oqtane.Extensions
{ {
public static class OqtaneSiteIdentityBuilderExtensions public static class OqtaneSiteIdentityBuilderExtensions
{ {
public static OqtaneSiteOptionsBuilder<TAlias> WithSiteIdentity<TAlias>( public static OqtaneSiteOptionsBuilder WithSiteIdentity(this OqtaneSiteOptionsBuilder builder)
this OqtaneSiteOptionsBuilder<TAlias> builder)
where TAlias : class, IAlias, new()
{ {
// site identity options // site identity options
builder.AddSiteOptions<IdentityOptions>((options, alias) => builder.AddSiteOptions<IdentityOptions>((options, alias) =>

View File

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

View File

@ -20,7 +20,9 @@ namespace Oqtane.Infrastructure
{ {
// check if framework is installed // check if framework is installed
var config = context.RequestServices.GetService(typeof(IConfigManager)) as IConfigManager; var config = context.RequestServices.GetService(typeof(IConfigManager)) as IConfigManager;
if (config.IsInstalled()) string path = context.Request.Path.ToString();
if (config.IsInstalled() && !path.StartsWith("/_blazor"))
{ {
// get alias (note that this also sets SiteState.Alias) // get alias (note that this also sets SiteState.Alias)
var tenantManager = context.RequestServices.GetService(typeof(ITenantManager)) as ITenantManager; var tenantManager = context.RequestServices.GetService(typeof(ITenantManager)) as ITenantManager;
@ -28,7 +30,7 @@ namespace Oqtane.Infrastructure
if (alias != null) if (alias != null)
{ {
// get site settings // add site settings to alias
var cache = context.RequestServices.GetService(typeof(IMemoryCache)) as IMemoryCache; var cache = context.RequestServices.GetService(typeof(IMemoryCache)) as IMemoryCache;
alias.SiteSettings = cache.GetOrCreate("sitesettings:" + alias.SiteKey, entry => alias.SiteSettings = cache.GetOrCreate("sitesettings:" + alias.SiteKey, entry =>
{ {
@ -36,13 +38,14 @@ namespace Oqtane.Infrastructure
return settingRepository.GetSettings(EntityNames.Site, alias.SiteId) return settingRepository.GetSettings(EntityNames.Site, alias.SiteId)
.ToDictionary(setting => setting.SettingName, setting => setting.SettingValue); .ToDictionary(setting => setting.SettingName, setting => setting.SettingValue);
}); });
// save alias in HttpContext // save alias in HttpContext for server-side usage
context.Items.Add(Constants.HttpContextAliasKey, alias); context.Items.Add(Constants.HttpContextAliasKey, alias);
// remove site settings so they are not available client-side
alias.SiteSettings = null;
// rewrite path by removing alias path prefix from api and pages requests (for consistent routing) // rewrite path by removing alias path prefix from api and pages requests (for consistent routing)
if (!string.IsNullOrEmpty(alias.Path)) if (!string.IsNullOrEmpty(alias.Path))
{ {
string path = context.Request.Path.ToString();
if (path.StartsWith("/" + alias.Path) && (path.Contains("/api/") || path.Contains("/pages/"))) if (path.StartsWith("/" + alias.Path) && (path.Contains("/api/") || path.Contains("/pages/")))
{ {
context.Request.Path = path.Replace("/" + alias.Path, ""); context.Request.Path = path.Replace("/" + alias.Path, "");

View File

@ -1,12 +1,10 @@
using Oqtane.Models; using Oqtane.Models;
namespace Oqtane.Infrastructure namespace Oqtane.Infrastructure
{ {
public interface ISiteOptions<TOptions, TAlias> public interface ISiteOptions<TOptions>
where TOptions : class, new() where TOptions : class, new()
where TAlias : class, IAlias, new()
{ {
void Configure(TOptions options, TAlias siteOptions); void Configure(TOptions options, Alias alias);
} }
} }

View File

@ -3,20 +3,19 @@ using Oqtane.Models;
namespace Oqtane.Infrastructure namespace Oqtane.Infrastructure
{ {
public class SiteOptions<TOptions, TAlias> : ISiteOptions<TOptions, TAlias> public class SiteOptions<TOptions> : ISiteOptions<TOptions>
where TOptions : class, new() where TOptions : class, new()
where TAlias : class, IAlias, new()
{ {
private readonly Action<TOptions, TAlias> configureOptions; private readonly Action<TOptions, Alias> configureOptions;
public SiteOptions(Action<TOptions, TAlias> configureOptions) public SiteOptions(Action<TOptions, Alias> configureOptions)
{ {
this.configureOptions = configureOptions; this.configureOptions = configureOptions;
} }
public void Configure(TOptions options, TAlias siteOptions) public void Configure(TOptions options, Alias alias)
{ {
configureOptions(options, siteOptions); configureOptions(options, alias);
} }
} }
} }

View File

@ -5,9 +5,8 @@ using Oqtane.Models;
namespace Oqtane.Infrastructure namespace Oqtane.Infrastructure
{ {
public class SiteOptionsCache<TOptions, TAlias> : IOptionsMonitorCache<TOptions> public class SiteOptionsCache<TOptions> : IOptionsMonitorCache<TOptions>
where TOptions : class where TOptions : class, new()
where TAlias : class, IAlias, new()
{ {
private readonly IAliasAccessor _aliasAccessor; private readonly IAliasAccessor _aliasAccessor;
private readonly ConcurrentDictionary<string, IOptionsMonitorCache<TOptions>> map = new ConcurrentDictionary<string, IOptionsMonitorCache<TOptions>>(); private readonly ConcurrentDictionary<string, IOptionsMonitorCache<TOptions>> map = new ConcurrentDictionary<string, IOptionsMonitorCache<TOptions>>();

View File

@ -1,25 +1,21 @@
using System.Collections.Generic; using System.Collections.Generic;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Oqtane.Models;
namespace Oqtane.Infrastructure namespace Oqtane.Infrastructure
{ {
public class SiteOptionsFactory<TOptions, TAlias> : IOptionsFactory<TOptions> public class SiteOptionsFactory<TOptions> : IOptionsFactory<TOptions>
where TOptions : class, new() where TOptions : class, new()
where TAlias : class, IAlias, new()
{ {
private readonly IConfigureOptions<TOptions>[] _configureOptions; private readonly IConfigureOptions<TOptions>[] _configureOptions;
private readonly IPostConfigureOptions<TOptions>[] _postConfigureOptions; private readonly IPostConfigureOptions<TOptions>[] _postConfigureOptions;
private readonly IValidateOptions<TOptions>[] _validations; private readonly ISiteOptions<TOptions>[] _siteOptions;
private readonly ISiteOptions<TOptions, TAlias>[] _siteOptions;
private readonly IAliasAccessor _aliasAccessor; 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) public SiteOptionsFactory(IEnumerable<IConfigureOptions<TOptions>> configureOptions, IEnumerable<IPostConfigureOptions<TOptions>> postConfigureOptions, IEnumerable<ISiteOptions<TOptions>> siteOptions, IAliasAccessor aliasAccessor)
{ {
_configureOptions = configureOptions as IConfigureOptions<TOptions>[] ?? new List<IConfigureOptions<TOptions>>(configureOptions).ToArray(); _configureOptions = configureOptions as IConfigureOptions<TOptions>[] ?? new List<IConfigureOptions<TOptions>>(configureOptions).ToArray();
_postConfigureOptions = postConfigureOptions as IPostConfigureOptions<TOptions>[] ?? new List<IPostConfigureOptions<TOptions>>(postConfigureOptions).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>[] ?? new List<ISiteOptions<TOptions>>(siteOptions).ToArray();
_siteOptions = siteOptions as ISiteOptions<TOptions, TAlias>[] ?? new List<ISiteOptions<TOptions, TAlias>>(siteOptions).ToArray();
_aliasAccessor = aliasAccessor; _aliasAccessor = aliasAccessor;
} }
@ -44,7 +40,7 @@ namespace Oqtane.Infrastructure
{ {
foreach (var siteOption in _siteOptions) foreach (var siteOption in _siteOptions)
{ {
siteOption.Configure(options, _aliasAccessor.Alias as TAlias); siteOption.Configure(options, _aliasAccessor.Alias);
} }
} }

View File

@ -6,6 +6,8 @@ using Oqtane.Infrastructure;
using Oqtane.Repository; using Oqtane.Repository;
using Oqtane.Models; using Oqtane.Models;
using System.Collections.Generic; using System.Collections.Generic;
using Oqtane.Extensions;
using Oqtane.Shared;
namespace Oqtane.Security namespace Oqtane.Security
{ {
@ -13,50 +15,48 @@ namespace Oqtane.Security
{ {
public static Task ValidateAsync(CookieValidatePrincipalContext context) public static Task ValidateAsync(CookieValidatePrincipalContext context)
{ {
if (context != null && context.Principal.Identity.IsAuthenticated) if (context != null && context.Principal.Identity.IsAuthenticated && context.Principal.Identity.Name != null)
{ {
// check if framework is installed // check if framework is installed
var config = context.HttpContext.RequestServices.GetService(typeof(IConfigManager)) as IConfigManager; var config = context.HttpContext.RequestServices.GetService(typeof(IConfigManager)) as IConfigManager;
if (config.IsInstalled()) if (config.IsInstalled())
{ {
var tenantManager = context.HttpContext.RequestServices.GetService(typeof(ITenantManager)) as ITenantManager; // get current site
var alias = tenantManager.GetAlias(); var alias = context.HttpContext.GetAlias();
if (alias != null) if (alias != null)
{ {
// verify principal was authenticated for current tenant // check if principal matches current site
if (context.Principal.Claims.FirstOrDefault(item => item.Type == ClaimTypes.GroupSid)?.Value != alias.SiteKey) if (context.Principal.Claims.FirstOrDefault(item => item.Type == ClaimTypes.GroupSid)?.Value != alias.SiteKey)
{ {
// tenant agnostic requests must be ignored // principal does not match site
string path = context.Request.Path.ToString().ToLower();
if (path.StartsWith("/_blazor") || path.StartsWith("/api/installation/"))
{
return Task.CompletedTask;
}
// refresh principal
var userRepository = context.HttpContext.RequestServices.GetService(typeof(IUserRepository)) as IUserRepository; var userRepository = context.HttpContext.RequestServices.GetService(typeof(IUserRepository)) as IUserRepository;
var userRoleRepository = context.HttpContext.RequestServices.GetService(typeof(IUserRoleRepository)) as IUserRoleRepository; var userRoleRepository = context.HttpContext.RequestServices.GetService(typeof(IUserRoleRepository)) as IUserRoleRepository;
var _logger = context.HttpContext.RequestServices.GetService(typeof(ILogManager)) as ILogManager;
string path = context.Request.Path.ToString().ToLower();
if (context.Principal.Identity.Name != null)
{
User user = userRepository.GetUser(context.Principal.Identity.Name); User user = userRepository.GetUser(context.Principal.Identity.Name);
if (user != null) if (user != null)
{ {
// replace principal with roles for current site
List<UserRole> userroles = userRoleRepository.GetUserRoles(user.UserId, alias.SiteId).ToList(); List<UserRole> userroles = userRoleRepository.GetUserRoles(user.UserId, alias.SiteId).ToList();
var identity = UserSecurity.CreateClaimsIdentity(alias, user, userroles); var identity = UserSecurity.CreateClaimsIdentity(alias, user, userroles);
context.ReplacePrincipal(new ClaimsPrincipal(identity)); context.ReplacePrincipal(new ClaimsPrincipal(identity));
context.ShouldRenew = true; context.ShouldRenew = true;
} if (!path.StartsWith("/api/")) // reduce log verbosity
else
{ {
context.RejectPrincipal(); _logger.Log(alias.SiteId, LogLevel.Information, "LoginValidation", Enums.LogFunction.Security, "Permissions Updated For User {Username} Accessing Resource {Url}", context.Principal.Identity.Name, path);
}
}
} }
} }
else else
{ {
// user has no roles for site - remove principal
context.RejectPrincipal(); context.RejectPrincipal();
if (!path.StartsWith("/api/")) // reduce log verbosity
{
_logger.Log(alias.SiteId, LogLevel.Information, "LoginValidation", Enums.LogFunction.Security, "Permissions Removed For User {Username} Accessing Resource {Url}", context.Principal.Identity.Name, path);
}
}
}
} }
} }
} }

View File

@ -115,12 +115,13 @@ namespace Oqtane
options.DefaultChallengeScheme = Constants.AuthenticationScheme; options.DefaultChallengeScheme = Constants.AuthenticationScheme;
}) })
.AddCookie(Constants.AuthenticationScheme) .AddCookie(Constants.AuthenticationScheme)
.AddOpenIdConnect("oidc", options => { }) .AddOpenIdConnect(AuthenticationProviderTypes.OpenIDConnect, options => { })
.AddOAuth("oauth2", options => { }); .AddOAuth(AuthenticationProviderTypes.OAuth2, options => { });
services.ConfigureOqtaneCookieOptions(); services.ConfigureOqtaneCookieOptions();
services.ConfigureOqtaneAuthenticationOptions(Configuration);
services.AddOqtaneSiteOptions<Alias>() services.AddOqtaneSiteOptions()
.WithSiteIdentity() .WithSiteIdentity()
.WithSiteAuthentication(); .WithSiteAuthentication();

View File

@ -82,7 +82,7 @@ namespace Oqtane.Models
} }
/// <summary> /// <summary>
/// Site-specific settings /// Site-specific settings (only available on the server via HttpContext for security reasons)
/// </summary> /// </summary>
[NotMapped] [NotMapped]
public Dictionary<string, string> SiteSettings { get; set; } public Dictionary<string, string> SiteSettings { get; set; }

View File

@ -0,0 +1,6 @@
namespace Oqtane.Shared {
public class AuthenticationProviderTypes {
public const string OpenIDConnect = "oidc";
public const string OAuth2 = "oauth2";
}
}