diff --git a/src/Umbraco.Core/Configuration/Models/BasicAuthSettings.cs b/src/Umbraco.Core/Configuration/Models/BasicAuthSettings.cs new file mode 100644 index 0000000000..e001166a08 --- /dev/null +++ b/src/Umbraco.Core/Configuration/Models/BasicAuthSettings.cs @@ -0,0 +1,26 @@ +// Copyright (c) Umbraco. +// See LICENSE for more details. + +using System.ComponentModel; +using System.Net; + +namespace Umbraco.Cms.Core.Configuration.Models +{ + /// + /// Typed configuration options for security settings. + /// + [UmbracoOptions(Constants.Configuration.ConfigBasicAuth)] + public class BasicAuthSettings + { + private const bool StaticEnabled = false; + + /// + /// Gets or sets a value indicating whether to keep the user logged in. + /// + [DefaultValue(StaticEnabled)] + public bool Enabled { get; set; } = StaticEnabled; + + + public string[] AllowedIPs { get; set; } = new string[0]; + } +} diff --git a/src/Umbraco.Core/Constants-Configuration.cs b/src/Umbraco.Core/Constants-Configuration.cs index d596d3feec..0c7657d07e 100644 --- a/src/Umbraco.Core/Constants-Configuration.cs +++ b/src/Umbraco.Core/Constants-Configuration.cs @@ -46,6 +46,7 @@ public const string ConfigRuntimeMinification = ConfigPrefix + "RuntimeMinification"; public const string ConfigRuntimeMinificationVersion = ConfigRuntimeMinification + ":Version"; public const string ConfigSecurity = ConfigPrefix + "Security"; + public const string ConfigBasicAuth = ConfigPrefix + "BasicAuth"; public const string ConfigTours = ConfigPrefix + "Tours"; public const string ConfigTypeFinder = ConfigPrefix + "TypeFinder"; public const string ConfigWebRouting = ConfigPrefix + "WebRouting"; diff --git a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Configuration.cs b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Configuration.cs index b5edfdf818..77902cc5c1 100644 --- a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Configuration.cs +++ b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Configuration.cs @@ -71,6 +71,7 @@ namespace Umbraco.Cms.Core.DependencyInjection .AddUmbracoOptions() .AddUmbracoOptions() .AddUmbracoOptions() + .AddUmbracoOptions() .AddUmbracoOptions(); return builder; diff --git a/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilder.BackOfficeAuth.cs b/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilder.BackOfficeAuth.cs index a652136a0f..8c57ab9978 100644 --- a/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilder.BackOfficeAuth.cs +++ b/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilder.BackOfficeAuth.cs @@ -11,6 +11,7 @@ using Umbraco.Cms.Web.Common.Security; using Umbraco.Cms.Core.Actions; using Umbraco.Cms.Core.Notifications; using Umbraco.Cms.Web.BackOffice.Authorization; +using Umbraco.Cms.Web.Common.Middleware; namespace Umbraco.Extensions @@ -49,6 +50,7 @@ namespace Umbraco.Extensions builder.Services.ConfigureOptions(); builder.Services.AddSingleton(); + builder.Services.AddScoped(); builder.Services.AddUnique(); builder.Services.AddUnique, PasswordChanger>(); diff --git a/src/Umbraco.Web.BackOffice/Extensions/HttpContextExtensions.cs b/src/Umbraco.Web.BackOffice/Extensions/HttpContextExtensions.cs index bf9e8f48e0..937caa8701 100644 --- a/src/Umbraco.Web.BackOffice/Extensions/HttpContextExtensions.cs +++ b/src/Umbraco.Web.BackOffice/Extensions/HttpContextExtensions.cs @@ -1,4 +1,6 @@ -using Microsoft.AspNetCore.Http; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Http; using Umbraco.Cms.Core.Security; namespace Umbraco.Extensions @@ -10,5 +12,6 @@ namespace Umbraco.Extensions public static BackOfficeExternalLoginProviderErrors GetExternalLoginProviderErrors(this HttpContext httpContext) => httpContext.Items[nameof(BackOfficeExternalLoginProviderErrors)] as BackOfficeExternalLoginProviderErrors; + } } diff --git a/src/Umbraco.Web.BackOffice/Extensions/UmbracoApplicationBuilder.BackOffice.cs b/src/Umbraco.Web.BackOffice/Extensions/UmbracoApplicationBuilder.BackOffice.cs index 59cefa0574..70be703fc5 100644 --- a/src/Umbraco.Web.BackOffice/Extensions/UmbracoApplicationBuilder.BackOffice.cs +++ b/src/Umbraco.Web.BackOffice/Extensions/UmbracoApplicationBuilder.BackOffice.cs @@ -8,6 +8,7 @@ using Umbraco.Cms.Web.BackOffice.Middleware; using Umbraco.Cms.Web.BackOffice.Routing; using Umbraco.Cms.Web.Common.ApplicationBuilder; using Umbraco.Cms.Web.Common.Extensions; +using Umbraco.Cms.Web.Common.Middleware; namespace Umbraco.Extensions { @@ -30,6 +31,7 @@ namespace Umbraco.Extensions a => a.UseMiddleware()); builder.AppBuilder.UseMiddleware(); + builder.AppBuilder.UseMiddleware(); return builder; } diff --git a/src/Umbraco.Web.BackOffice/Middleware/BasicAuthAuthenticationMiddleware.cs b/src/Umbraco.Web.BackOffice/Middleware/BasicAuthAuthenticationMiddleware.cs new file mode 100644 index 0000000000..a036858929 --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Middleware/BasicAuthAuthenticationMiddleware.cs @@ -0,0 +1,120 @@ +using System; +using System.Net; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Web.BackOffice.Security; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Web.Common.Middleware +{ + /// + /// Ensures that preview pages (front-end routed) are authenticated with the back office identity appended to the + /// principal alongside any default authentication that takes place + /// + public class BasicAuthAuthenticationMiddleware : IMiddleware + { + private readonly ILogger _logger; + private readonly IOptionsSnapshot _basicAuthSettings; + private readonly IRuntimeState _runtimeState; + + public BasicAuthAuthenticationMiddleware( + ILogger logger, + IOptionsSnapshot basicAuthSettings, + IRuntimeState runtimeState) + { + _logger = logger; + _basicAuthSettings = basicAuthSettings; + _runtimeState = runtimeState; + } + + /// + public async Task InvokeAsync(HttpContext context, RequestDelegate next) + { + var options = _basicAuthSettings.Value; + if (!options.Enabled || _runtimeState.Level < RuntimeLevel.Run) + { + await next(context); + return; + } + + var clientIPAddress = context.Connection.RemoteIpAddress; + if (IsIpAllowListed(clientIPAddress, options.AllowedIPs)) + { + await next(context); + return; + } + + AuthenticateResult authenticateResult = await context.AuthenticateBackOfficeAsync(); + if (authenticateResult.Succeeded) + { + await next(context); + return; + } + + + string authHeader = context.Request.Headers["Authorization"]; + if (authHeader != null && authHeader.StartsWith("Basic")) + { + //Extract credentials + var encodedUsernamePassword = authHeader.Substring(6).Trim(); + var encoding = Encoding.UTF8; + var usernamePassword = encoding.GetString(Convert.FromBase64String(encodedUsernamePassword)); + + var seperatorIndex = usernamePassword.IndexOf(':'); + + var username = usernamePassword.Substring(0, seperatorIndex); + var password = usernamePassword.Substring(seperatorIndex + 1); + + + IBackOfficeSignInManager backOfficeSignInManager = + context.RequestServices.GetRequiredService(); + + + SignInResult signInResult = + await backOfficeSignInManager.PasswordSignInAsync(username, password, false, true); + + if (signInResult.Succeeded) + { + await next.Invoke(context); + } + else + { + SetUnauthorizedHeader(context); + } + } + else + { + // no authorization header + SetUnauthorizedHeader(context); + } + } + + private bool IsIpAllowListed(IPAddress clientIpAddress, string[] allowlist) + { + foreach (var allowedIpString in allowlist) + { + if(IPAddress.TryParse(allowedIpString, out var allowedIp) && clientIpAddress.Equals(allowedIp)) + { + return true; + }; + } + + return false; + } + + private static void SetUnauthorizedHeader(HttpContext context) + { + context.Response.StatusCode = 401; + context.Response.Headers.Add("WWW-Authenticate", "Basic realm=\"Umbraco as a Service login\""); + } + } +} diff --git a/src/Umbraco.Web.BackOffice/Security/BackOfficeCookieManager.cs b/src/Umbraco.Web.BackOffice/Security/BackOfficeCookieManager.cs index a05af07bb6..5ba2fff613 100644 --- a/src/Umbraco.Web.BackOffice/Security/BackOfficeCookieManager.cs +++ b/src/Umbraco.Web.BackOffice/Security/BackOfficeCookieManager.cs @@ -2,7 +2,9 @@ using System.Collections.Generic; using System.Linq; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Options; using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Routing; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Web; @@ -23,6 +25,7 @@ namespace Umbraco.Cms.Web.BackOffice.Security private readonly IRuntimeState _runtime; private readonly string[] _explicitPaths; private readonly UmbracoRequestPaths _umbracoRequestPaths; + private readonly IOptionsMonitor _basicAuthSettingsMonitor; /// /// Initializes a new instance of the class. @@ -30,8 +33,9 @@ namespace Umbraco.Cms.Web.BackOffice.Security public BackOfficeCookieManager( IUmbracoContextAccessor umbracoContextAccessor, IRuntimeState runtime, - UmbracoRequestPaths umbracoRequestPaths) - : this(umbracoContextAccessor, runtime, null, umbracoRequestPaths) + UmbracoRequestPaths umbracoRequestPaths, + IOptionsMonitor basicAuthSettings) + : this(umbracoContextAccessor, runtime, null, umbracoRequestPaths, basicAuthSettings) { } @@ -42,12 +46,14 @@ namespace Umbraco.Cms.Web.BackOffice.Security IUmbracoContextAccessor umbracoContextAccessor, IRuntimeState runtime, IEnumerable explicitPaths, - UmbracoRequestPaths umbracoRequestPaths) + UmbracoRequestPaths umbracoRequestPaths, + IOptionsMonitor basicAuthSettingsMonitor) { _umbracoContextAccessor = umbracoContextAccessor; _runtime = runtime; _explicitPaths = explicitPaths?.ToArray(); _umbracoRequestPaths = umbracoRequestPaths; + _basicAuthSettingsMonitor = basicAuthSettingsMonitor; } /// @@ -88,6 +94,11 @@ namespace Umbraco.Cms.Web.BackOffice.Security return true; } + if (_basicAuthSettingsMonitor.CurrentValue.Enabled) + { + return true; + } + return false; } diff --git a/src/Umbraco.Web.BackOffice/Security/ConfigureBackOfficeCookieOptions.cs b/src/Umbraco.Web.BackOffice/Security/ConfigureBackOfficeCookieOptions.cs index 024ee50aaf..2d87735cab 100644 --- a/src/Umbraco.Web.BackOffice/Security/ConfigureBackOfficeCookieOptions.cs +++ b/src/Umbraco.Web.BackOffice/Security/ConfigureBackOfficeCookieOptions.cs @@ -34,6 +34,7 @@ namespace Umbraco.Cms.Web.BackOffice.Security private readonly IIpResolver _ipResolver; private readonly ISystemClock _systemClock; private readonly UmbracoRequestPaths _umbracoRequestPaths; + private readonly IOptionsMonitor _optionsSnapshot; /// /// Initializes a new instance of the class. @@ -59,7 +60,8 @@ namespace Umbraco.Cms.Web.BackOffice.Security IUserService userService, IIpResolver ipResolver, ISystemClock systemClock, - UmbracoRequestPaths umbracoRequestPaths) + UmbracoRequestPaths umbracoRequestPaths, + IOptionsMonitor optionsSnapshot) { _serviceProvider = serviceProvider; _umbracoContextAccessor = umbracoContextAccessor; @@ -72,6 +74,7 @@ namespace Umbraco.Cms.Web.BackOffice.Security _ipResolver = ipResolver; _systemClock = systemClock; _umbracoRequestPaths = umbracoRequestPaths; + _optionsSnapshot = optionsSnapshot; } /// @@ -115,7 +118,9 @@ namespace Umbraco.Cms.Web.BackOffice.Security options.CookieManager = new BackOfficeCookieManager( _umbracoContextAccessor, _runtimeState, - _umbracoRequestPaths); + _umbracoRequestPaths, + _optionsSnapshot + ); options.Events = new CookieAuthenticationEvents { diff --git a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs index 957d5976e1..8502eeef66 100644 --- a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs @@ -101,10 +101,10 @@ namespace Umbraco.Extensions services.AddSingleton(httpContextAccessor); var requestCache = new HttpContextRequestAppCache(httpContextAccessor); - var appCaches = AppCaches.Create(requestCache); + var appCaches = AppCaches.Create(requestCache); IProfiler profiler = GetWebProfiler(config); - + ILoggerFactory loggerFactory = LoggerFactory.Create(cfg => cfg.AddSerilog(Log.Logger, false)); TypeLoader typeLoader = services.AddTypeLoader( diff --git a/src/Umbraco.Web.Common/Extensions/ApplicationBuilderExtensions.cs b/src/Umbraco.Web.Common/Extensions/ApplicationBuilderExtensions.cs index efc95a59f7..64839d2dd3 100644 --- a/src/Umbraco.Web.Common/Extensions/ApplicationBuilderExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/ApplicationBuilderExtensions.cs @@ -37,7 +37,7 @@ namespace Umbraco.Extensions IOptions startupOptions = app.ApplicationServices.GetRequiredService>(); app.RunPrePipeline(startupOptions.Value); - + app.UseUmbracoCore(); app.UseUmbracoRequestLogging(); diff --git a/src/Umbraco.Web.Common/Extensions/ControllerExtensions.cs b/src/Umbraco.Web.Common/Extensions/ControllerExtensions.cs index b5f665ae9c..6ae94ab57f 100644 --- a/src/Umbraco.Web.Common/Extensions/ControllerExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/ControllerExtensions.cs @@ -14,13 +14,7 @@ namespace Umbraco.Extensions /// public static async Task AuthenticateBackOfficeAsync(this ControllerBase controller) { - if (controller.HttpContext == null) - { - return AuthenticateResult.NoResult(); - } - - var result = await controller.HttpContext.AuthenticateAsync(Cms.Core.Constants.Security.BackOfficeAuthenticationType); - return result; + return await controller.HttpContext.AuthenticateBackOfficeAsync(); } /// diff --git a/src/Umbraco.Web.Common/Extensions/HttpContextExtensions.cs b/src/Umbraco.Web.Common/Extensions/HttpContextExtensions.cs index fbb9e77770..4b3915f387 100644 --- a/src/Umbraco.Web.Common/Extensions/HttpContextExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/HttpContextExtensions.cs @@ -1,5 +1,7 @@ using System; using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; @@ -7,6 +9,22 @@ namespace Umbraco.Extensions { public static class HttpContextExtensions { + + + /// + /// Runs the authentication process + /// + public static async Task AuthenticateBackOfficeAsync(this HttpContext httpContext) + { + if (httpContext == null) + { + return AuthenticateResult.NoResult(); + } + + var result = await httpContext.AuthenticateAsync(Cms.Core.Constants.Security.BackOfficeAuthenticationType); + return result; + } + /// /// Get the value in the request form or query string for the key ///