From 4f2cb09939c1073e47a0c1bcea155311bf97dc30 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Thu, 5 Aug 2021 21:42:36 +0200 Subject: [PATCH 01/10] Added POC of basic auth middleware --- .../Configuration/Models/BasicAuthSettings.cs | 26 ++++ src/Umbraco.Core/Constants-Configuration.cs | 1 + .../UmbracoBuilder.Configuration.cs | 1 + .../UmbracoBuilder.BackOfficeAuth.cs | 2 + .../Extensions/HttpContextExtensions.cs | 5 +- .../UmbracoApplicationBuilder.BackOffice.cs | 2 + .../BasicAuthAuthenticationMiddleware.cs | 120 ++++++++++++++++++ .../Security/BackOfficeCookieManager.cs | 17 ++- .../ConfigureBackOfficeCookieOptions.cs | 9 +- .../UmbracoBuilderExtensions.cs | 4 +- .../ApplicationBuilderExtensions.cs | 2 +- .../Extensions/ControllerExtensions.cs | 8 +- .../Extensions/HttpContextExtensions.cs | 18 +++ 13 files changed, 199 insertions(+), 16 deletions(-) create mode 100644 src/Umbraco.Core/Configuration/Models/BasicAuthSettings.cs create mode 100644 src/Umbraco.Web.BackOffice/Middleware/BasicAuthAuthenticationMiddleware.cs 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 /// From 0e7f9d93ca335ed9b79087a3f223b75ef7f69bfa Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Fri, 6 Aug 2021 09:51:08 +0200 Subject: [PATCH 02/10] Introduced IBasicAuthService --- src/JsonSchema/AppSettings.cs | 1 + src/Umbraco.Core/Services/BasicAuthService.cs | 33 +++++++++++++++++ .../Services/IBasicAuthService.cs | 10 ++++++ .../UmbracoBuilder.Services.cs | 1 + .../Services/BasicAuthServiceTests.cs | 35 +++++++++++++++++++ .../Security/BackOfficeCookieManagerTests.cs | 12 ++++--- .../BasicAuthAuthenticationMiddleware.cs | 31 ++++------------ .../Security/BackOfficeCookieManager.cs | 12 +++---- .../ConfigureBackOfficeCookieOptions.cs | 7 ++-- 9 files changed, 104 insertions(+), 38 deletions(-) create mode 100644 src/Umbraco.Core/Services/BasicAuthService.cs create mode 100644 src/Umbraco.Core/Services/IBasicAuthService.cs create mode 100644 src/Umbraco.Tests.UnitTests/Umbraco.Core/Services/BasicAuthServiceTests.cs diff --git a/src/JsonSchema/AppSettings.cs b/src/JsonSchema/AppSettings.cs index 616d641751..4045421eb1 100644 --- a/src/JsonSchema/AppSettings.cs +++ b/src/JsonSchema/AppSettings.cs @@ -46,6 +46,7 @@ namespace JsonSchema public UnattendedSettings Unattended { get; set; } public RichTextEditorSettings RichTextEditor { get; set; } public RuntimeMinificationSettings RuntimeMinification { get; set; } + public BasicAuthSettings BasicAuth { get; set; } } /// diff --git a/src/Umbraco.Core/Services/BasicAuthService.cs b/src/Umbraco.Core/Services/BasicAuthService.cs new file mode 100644 index 0000000000..99a6f930c5 --- /dev/null +++ b/src/Umbraco.Core/Services/BasicAuthService.cs @@ -0,0 +1,33 @@ +using System.Net; +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core.Configuration.Models; + +namespace Umbraco.Cms.Core.Services +{ + public class BasicAuthService : IBasicAuthService + { + private BasicAuthSettings _basicAuthSettings; + + public BasicAuthService(IOptionsMonitor optionsMonitor) + { + _basicAuthSettings = optionsMonitor.CurrentValue; + + optionsMonitor.OnChange(basicAuthSettings => _basicAuthSettings = basicAuthSettings); + } + + public bool IsBasicAuthEnabled() => _basicAuthSettings.Enabled; + + public bool IsIpAllowListed(IPAddress clientIpAddress) + { + foreach (var allowedIpString in _basicAuthSettings.AllowedIPs) + { + if(IPAddress.TryParse(allowedIpString, out var allowedIp) && clientIpAddress.Equals(allowedIp)) + { + return true; + }; + } + + return false; + } + } +} diff --git a/src/Umbraco.Core/Services/IBasicAuthService.cs b/src/Umbraco.Core/Services/IBasicAuthService.cs new file mode 100644 index 0000000000..84173a629a --- /dev/null +++ b/src/Umbraco.Core/Services/IBasicAuthService.cs @@ -0,0 +1,10 @@ +using System.Net; + +namespace Umbraco.Cms.Core.Services +{ + public interface IBasicAuthService + { + bool IsBasicAuthEnabled(); + bool IsIpAllowListed(IPAddress clientIpAddress); + } +} diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Services.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Services.cs index 8b34289c9c..e535b399e4 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Services.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Services.cs @@ -40,6 +40,7 @@ namespace Umbraco.Cms.Infrastructure.DependencyInjection builder.Services.AddUnique(); builder.Services.AddUnique(); builder.Services.AddUnique(); + builder.Services.AddUnique(); builder.Services.AddUnique(); builder.Services.AddUnique(); builder.Services.AddUnique(); diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Core/Services/BasicAuthServiceTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Core/Services/BasicAuthServiceTests.cs new file mode 100644 index 0000000000..f406acc03d --- /dev/null +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Core/Services/BasicAuthServiceTests.cs @@ -0,0 +1,35 @@ +using System.Linq; +using System.Net; +using Microsoft.Extensions.Options; +using Moq; +using NUnit.Framework; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Services; + +namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Services +{ + [TestFixture] + public class BasicAuthServiceTests + { + [TestCase(true, ExpectedResult = true)] + [TestCase(false, ExpectedResult = false)] + public bool IsBasicAuthEnabled(bool enabled) + { + var sut = new BasicAuthService(Mock.Of>(_ => _.CurrentValue == new BasicAuthSettings() {Enabled = enabled})); + + return sut.IsBasicAuthEnabled(); + } + + [TestCase("::1", "1.1.1.1", ExpectedResult = false)] + [TestCase("::1", "1.1.1.1, ::1", ExpectedResult = true)] + [TestCase("127.0.0.1", "127.0.0.1, ::1", ExpectedResult = true)] + [TestCase("127.0.0.1", "", ExpectedResult = false)] + public bool IsBasicAuthEnabled(string clientIpAddress, string commaSeperatedAllowlist) + { + var allowedIPs = commaSeperatedAllowlist.Split(",").Select(x=>x.Trim()).ToArray(); + var sut = new BasicAuthService(Mock.Of>(_ => _.CurrentValue == new BasicAuthSettings() {AllowedIPs = allowedIPs})); + + return sut.IsIpAllowListed(IPAddress.Parse(clientIpAddress)); + } + } +} diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Security/BackOfficeCookieManagerTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Security/BackOfficeCookieManagerTests.cs index 3c04fe5f40..9754c2b0ab 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Security/BackOfficeCookieManagerTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Security/BackOfficeCookieManagerTests.cs @@ -30,7 +30,8 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.BackOffice.Security var mgr = new BackOfficeCookieManager( Mock.Of(), runtime, - new UmbracoRequestPaths(Options.Create(globalSettings), TestHelper.GetHostingEnvironment())); + new UmbracoRequestPaths(Options.Create(globalSettings), TestHelper.GetHostingEnvironment()), + Mock.Of()); var result = mgr.ShouldAuthenticateRequest("/umbraco"); @@ -48,7 +49,8 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.BackOffice.Security runtime, new UmbracoRequestPaths( Options.Create(globalSettings), - Mock.Of(x => x.ApplicationVirtualPath == "/" && x.ToAbsolute(globalSettings.UmbracoPath) == "/umbraco"))); + Mock.Of(x => x.ApplicationVirtualPath == "/" && x.ToAbsolute(globalSettings.UmbracoPath) == "/umbraco")), + Mock.Of()); var result = mgr.ShouldAuthenticateRequest("/umbraco"); @@ -69,7 +71,8 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.BackOffice.Security runtime, new UmbracoRequestPaths( Options.Create(globalSettings), - Mock.Of(x => x.ApplicationVirtualPath == "/" && x.ToAbsolute(globalSettings.UmbracoPath) == "/umbraco" && x.ToAbsolute(Constants.SystemDirectories.Install) == "/install"))); + Mock.Of(x => x.ApplicationVirtualPath == "/" && x.ToAbsolute(globalSettings.UmbracoPath) == "/umbraco" && x.ToAbsolute(Constants.SystemDirectories.Install) == "/install")), + Mock.Of()); var result = mgr.ShouldAuthenticateRequest(remainingTimeoutSecondsPath); Assert.IsTrue(result); @@ -90,7 +93,8 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.BackOffice.Security runtime, new UmbracoRequestPaths( Options.Create(globalSettings), - Mock.Of(x => x.ApplicationVirtualPath == "/" && x.ToAbsolute(globalSettings.UmbracoPath) == "/umbraco" && x.ToAbsolute(Constants.SystemDirectories.Install) == "/install"))); + Mock.Of(x => x.ApplicationVirtualPath == "/" && x.ToAbsolute(globalSettings.UmbracoPath) == "/umbraco" && x.ToAbsolute(Constants.SystemDirectories.Install) == "/install")), + Mock.Of()); var result = mgr.ShouldAuthenticateRequest("/notbackoffice"); Assert.IsFalse(result); diff --git a/src/Umbraco.Web.BackOffice/Middleware/BasicAuthAuthenticationMiddleware.cs b/src/Umbraco.Web.BackOffice/Middleware/BasicAuthAuthenticationMiddleware.cs index a036858929..7027285bf3 100644 --- a/src/Umbraco.Web.BackOffice/Middleware/BasicAuthAuthenticationMiddleware.cs +++ b/src/Umbraco.Web.BackOffice/Middleware/BasicAuthAuthenticationMiddleware.cs @@ -1,15 +1,11 @@ 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; @@ -22,32 +18,28 @@ namespace Umbraco.Cms.Web.Common.Middleware /// public class BasicAuthAuthenticationMiddleware : IMiddleware { - private readonly ILogger _logger; - private readonly IOptionsSnapshot _basicAuthSettings; private readonly IRuntimeState _runtimeState; + private readonly IBasicAuthService _basicAuthService; public BasicAuthAuthenticationMiddleware( - ILogger logger, - IOptionsSnapshot basicAuthSettings, - IRuntimeState runtimeState) + IRuntimeState runtimeState, + IBasicAuthService basicAuthService) { - _logger = logger; - _basicAuthSettings = basicAuthSettings; _runtimeState = runtimeState; + _basicAuthService = basicAuthService; } /// public async Task InvokeAsync(HttpContext context, RequestDelegate next) { - var options = _basicAuthSettings.Value; - if (!options.Enabled || _runtimeState.Level < RuntimeLevel.Run) + if (_runtimeState.Level < RuntimeLevel.Run || !_basicAuthService.IsBasicAuthEnabled()) { await next(context); return; } var clientIPAddress = context.Connection.RemoteIpAddress; - if (IsIpAllowListed(clientIPAddress, options.AllowedIPs)) + if (_basicAuthService.IsIpAllowListed(clientIPAddress)) { await next(context); return; @@ -98,18 +90,7 @@ namespace Umbraco.Cms.Web.Common.Middleware } } - 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) { diff --git a/src/Umbraco.Web.BackOffice/Security/BackOfficeCookieManager.cs b/src/Umbraco.Web.BackOffice/Security/BackOfficeCookieManager.cs index 5ba2fff613..5d50981f6a 100644 --- a/src/Umbraco.Web.BackOffice/Security/BackOfficeCookieManager.cs +++ b/src/Umbraco.Web.BackOffice/Security/BackOfficeCookieManager.cs @@ -25,7 +25,7 @@ namespace Umbraco.Cms.Web.BackOffice.Security private readonly IRuntimeState _runtime; private readonly string[] _explicitPaths; private readonly UmbracoRequestPaths _umbracoRequestPaths; - private readonly IOptionsMonitor _basicAuthSettingsMonitor; + private readonly IBasicAuthService _basicAuthService; /// /// Initializes a new instance of the class. @@ -34,8 +34,8 @@ namespace Umbraco.Cms.Web.BackOffice.Security IUmbracoContextAccessor umbracoContextAccessor, IRuntimeState runtime, UmbracoRequestPaths umbracoRequestPaths, - IOptionsMonitor basicAuthSettings) - : this(umbracoContextAccessor, runtime, null, umbracoRequestPaths, basicAuthSettings) + IBasicAuthService basicAuthService) + : this(umbracoContextAccessor, runtime, null, umbracoRequestPaths, basicAuthService) { } @@ -47,13 +47,13 @@ namespace Umbraco.Cms.Web.BackOffice.Security IRuntimeState runtime, IEnumerable explicitPaths, UmbracoRequestPaths umbracoRequestPaths, - IOptionsMonitor basicAuthSettingsMonitor) + IBasicAuthService basicAuthService) { _umbracoContextAccessor = umbracoContextAccessor; _runtime = runtime; _explicitPaths = explicitPaths?.ToArray(); _umbracoRequestPaths = umbracoRequestPaths; - _basicAuthSettingsMonitor = basicAuthSettingsMonitor; + _basicAuthService = basicAuthService; } /// @@ -94,7 +94,7 @@ namespace Umbraco.Cms.Web.BackOffice.Security return true; } - if (_basicAuthSettingsMonitor.CurrentValue.Enabled) + if (_basicAuthService.IsBasicAuthEnabled()) { return true; } diff --git a/src/Umbraco.Web.BackOffice/Security/ConfigureBackOfficeCookieOptions.cs b/src/Umbraco.Web.BackOffice/Security/ConfigureBackOfficeCookieOptions.cs index 2d87735cab..1457732c53 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 IBasicAuthService _basicAuthService; private readonly IOptionsMonitor _optionsSnapshot; /// @@ -61,7 +62,7 @@ namespace Umbraco.Cms.Web.BackOffice.Security IIpResolver ipResolver, ISystemClock systemClock, UmbracoRequestPaths umbracoRequestPaths, - IOptionsMonitor optionsSnapshot) + IBasicAuthService basicAuthService) { _serviceProvider = serviceProvider; _umbracoContextAccessor = umbracoContextAccessor; @@ -74,7 +75,7 @@ namespace Umbraco.Cms.Web.BackOffice.Security _ipResolver = ipResolver; _systemClock = systemClock; _umbracoRequestPaths = umbracoRequestPaths; - _optionsSnapshot = optionsSnapshot; + _basicAuthService = basicAuthService; } /// @@ -119,7 +120,7 @@ namespace Umbraco.Cms.Web.BackOffice.Security _umbracoContextAccessor, _runtimeState, _umbracoRequestPaths, - _optionsSnapshot + _basicAuthService ); options.Events = new CookieAuthenticationEvents From 2d6b382ce75bc551b3055c08e568825128ba3757 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Fri, 6 Aug 2021 10:41:21 +0200 Subject: [PATCH 03/10] Added support for Ip Ranges --- .../Services/Implement}/BasicAuthService.cs | 4 ++-- src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj | 1 + .../Umbraco.Core/Services/BasicAuthServiceTests.cs | 4 +++- 3 files changed, 6 insertions(+), 3 deletions(-) rename src/{Umbraco.Core/Services => Umbraco.Infrastructure/Services/Implement}/BasicAuthService.cs (84%) diff --git a/src/Umbraco.Core/Services/BasicAuthService.cs b/src/Umbraco.Infrastructure/Services/Implement/BasicAuthService.cs similarity index 84% rename from src/Umbraco.Core/Services/BasicAuthService.cs rename to src/Umbraco.Infrastructure/Services/Implement/BasicAuthService.cs index 99a6f930c5..a685e4baca 100644 --- a/src/Umbraco.Core/Services/BasicAuthService.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/BasicAuthService.cs @@ -2,7 +2,7 @@ using System.Net; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration.Models; -namespace Umbraco.Cms.Core.Services +namespace Umbraco.Cms.Core.Services.Implement { public class BasicAuthService : IBasicAuthService { @@ -21,7 +21,7 @@ namespace Umbraco.Cms.Core.Services { foreach (var allowedIpString in _basicAuthSettings.AllowedIPs) { - if(IPAddress.TryParse(allowedIpString, out var allowedIp) && clientIpAddress.Equals(allowedIp)) + if(IPNetwork.TryParse(allowedIpString, out var allowedIp) && allowedIp.Contains(clientIpAddress)) { return true; }; diff --git a/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj b/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj index 712323656d..47af185872 100644 --- a/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj +++ b/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj @@ -19,6 +19,7 @@ + diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Core/Services/BasicAuthServiceTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Core/Services/BasicAuthServiceTests.cs index f406acc03d..603ccfd9a1 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Core/Services/BasicAuthServiceTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Core/Services/BasicAuthServiceTests.cs @@ -4,7 +4,7 @@ using Microsoft.Extensions.Options; using Moq; using NUnit.Framework; using Umbraco.Cms.Core.Configuration.Models; -using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services.Implement; namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Services { @@ -24,6 +24,8 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Services [TestCase("::1", "1.1.1.1, ::1", ExpectedResult = true)] [TestCase("127.0.0.1", "127.0.0.1, ::1", ExpectedResult = true)] [TestCase("127.0.0.1", "", ExpectedResult = false)] + [TestCase("125.125.125.1", "125.125.125.0/24", ExpectedResult = true)] + [TestCase("125.125.124.1", "125.125.125.0/24", ExpectedResult = false)] public bool IsBasicAuthEnabled(string clientIpAddress, string commaSeperatedAllowlist) { var allowedIPs = commaSeperatedAllowlist.Split(",").Select(x=>x.Trim()).ToArray(); From db6002196d6917c7471e24bbfb4395da9cf96bc6 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Fri, 6 Aug 2021 10:44:35 +0200 Subject: [PATCH 04/10] Fix test name --- .../Umbraco.Core/Services/BasicAuthServiceTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Core/Services/BasicAuthServiceTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Core/Services/BasicAuthServiceTests.cs index 603ccfd9a1..6322debaab 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Core/Services/BasicAuthServiceTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Core/Services/BasicAuthServiceTests.cs @@ -26,7 +26,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Services [TestCase("127.0.0.1", "", ExpectedResult = false)] [TestCase("125.125.125.1", "125.125.125.0/24", ExpectedResult = true)] [TestCase("125.125.124.1", "125.125.125.0/24", ExpectedResult = false)] - public bool IsBasicAuthEnabled(string clientIpAddress, string commaSeperatedAllowlist) + public bool IsIpAllowListed(string clientIpAddress, string commaSeperatedAllowlist) { var allowedIPs = commaSeperatedAllowlist.Split(",").Select(x=>x.Trim()).ToArray(); var sut = new BasicAuthService(Mock.Of>(_ => _.CurrentValue == new BasicAuthSettings() {AllowedIPs = allowedIPs})); From 266af1de126f28089d6f303426a87bd8df0d7e6f Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Fri, 6 Aug 2021 10:59:54 +0200 Subject: [PATCH 05/10] Update src/Umbraco.Core/Configuration/Models/BasicAuthSettings.cs Co-authored-by: Andy Butland --- src/Umbraco.Core/Configuration/Models/BasicAuthSettings.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Core/Configuration/Models/BasicAuthSettings.cs b/src/Umbraco.Core/Configuration/Models/BasicAuthSettings.cs index e001166a08..d8daa61cb5 100644 --- a/src/Umbraco.Core/Configuration/Models/BasicAuthSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/BasicAuthSettings.cs @@ -7,7 +7,7 @@ using System.Net; namespace Umbraco.Cms.Core.Configuration.Models { /// - /// Typed configuration options for security settings. + /// Typed configuration options for basic authentication settings. /// [UmbracoOptions(Constants.Configuration.ConfigBasicAuth)] public class BasicAuthSettings From aaf28cba21560b6fcfc0bcf7e6af3e89d83dc2d7 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Fri, 6 Aug 2021 11:00:27 +0200 Subject: [PATCH 06/10] Update src/Umbraco.Web.BackOffice/Middleware/BasicAuthAuthenticationMiddleware.cs Co-authored-by: Andy Butland --- .../Middleware/BasicAuthAuthenticationMiddleware.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Umbraco.Web.BackOffice/Middleware/BasicAuthAuthenticationMiddleware.cs b/src/Umbraco.Web.BackOffice/Middleware/BasicAuthAuthenticationMiddleware.cs index 7027285bf3..719dfed6c0 100644 --- a/src/Umbraco.Web.BackOffice/Middleware/BasicAuthAuthenticationMiddleware.cs +++ b/src/Umbraco.Web.BackOffice/Middleware/BasicAuthAuthenticationMiddleware.cs @@ -13,8 +13,7 @@ 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 + /// Provides basic authentication via back-office credentials for public website access if configured for use and the client IP is not allow listed. /// public class BasicAuthAuthenticationMiddleware : IMiddleware { From bbd935a0b89c822900be4275bc7921884f0ec1d4 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Fri, 6 Aug 2021 11:26:31 +0200 Subject: [PATCH 07/10] Updates based on review feedback --- .../Configuration/Models/BasicAuthSettings.cs | 3 +- .../UmbracoBuilder.BackOfficeAuth.cs | 2 +- .../BasicAuthAuthenticationMiddleware.cs | 20 ++---------- .../Extensions/HttpContextExtensions.cs | 31 +++++++++++++++++++ 4 files changed, 36 insertions(+), 20 deletions(-) diff --git a/src/Umbraco.Core/Configuration/Models/BasicAuthSettings.cs b/src/Umbraco.Core/Configuration/Models/BasicAuthSettings.cs index d8daa61cb5..054619d843 100644 --- a/src/Umbraco.Core/Configuration/Models/BasicAuthSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/BasicAuthSettings.cs @@ -1,6 +1,7 @@ // Copyright (c) Umbraco. // See LICENSE for more details. +using System; using System.ComponentModel; using System.Net; @@ -21,6 +22,6 @@ namespace Umbraco.Cms.Core.Configuration.Models public bool Enabled { get; set; } = StaticEnabled; - public string[] AllowedIPs { get; set; } = new string[0]; + public string[] AllowedIPs { get; set; } = Array.Empty(); } } diff --git a/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilder.BackOfficeAuth.cs b/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilder.BackOfficeAuth.cs index 8c57ab9978..c7d7df33a7 100644 --- a/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilder.BackOfficeAuth.cs +++ b/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilder.BackOfficeAuth.cs @@ -50,7 +50,7 @@ namespace Umbraco.Extensions builder.Services.ConfigureOptions(); builder.Services.AddSingleton(); - builder.Services.AddScoped(); + builder.Services.AddSingleton(); builder.Services.AddUnique(); builder.Services.AddUnique, PasswordChanger>(); diff --git a/src/Umbraco.Web.BackOffice/Middleware/BasicAuthAuthenticationMiddleware.cs b/src/Umbraco.Web.BackOffice/Middleware/BasicAuthAuthenticationMiddleware.cs index 719dfed6c0..bd594169d2 100644 --- a/src/Umbraco.Web.BackOffice/Middleware/BasicAuthAuthenticationMiddleware.cs +++ b/src/Umbraco.Web.BackOffice/Middleware/BasicAuthAuthenticationMiddleware.cs @@ -51,25 +51,11 @@ namespace Umbraco.Cms.Web.Common.Middleware return; } - - string authHeader = context.Request.Headers["Authorization"]; - if (authHeader != null && authHeader.StartsWith("Basic")) + if (context.TryGetBasicAuthCredentials(out var username, out var password)) { - //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); @@ -89,12 +75,10 @@ namespace Umbraco.Cms.Web.Common.Middleware } } - - private static void SetUnauthorizedHeader(HttpContext context) { context.Response.StatusCode = 401; - context.Response.Headers.Add("WWW-Authenticate", "Basic realm=\"Umbraco as a Service login\""); + context.Response.Headers.Add("WWW-Authenticate", "Basic realm=\"Umbraco login\""); } } } diff --git a/src/Umbraco.Web.Common/Extensions/HttpContextExtensions.cs b/src/Umbraco.Web.Common/Extensions/HttpContextExtensions.cs index 4b3915f387..afd0c5be48 100644 --- a/src/Umbraco.Web.Common/Extensions/HttpContextExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/HttpContextExtensions.cs @@ -1,5 +1,6 @@ using System; using System.Security.Claims; +using System.Text; using System.Threading.Tasks; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Http; @@ -11,6 +12,36 @@ namespace Umbraco.Extensions { + /// + /// Try to get the basic auth username and password from the http context. + /// + public static bool TryGetBasicAuthCredentials(this HttpContext httpContext, out string username, out string password) + { + username = null; + password = null; + + if ( httpContext.Request.Headers.TryGetValue("Authorization", out var authHeaders)) + { + var authHeader = authHeaders.ToString(); + if (authHeader is not 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(':'); + + username = usernamePassword.Substring(0, seperatorIndex); + password = usernamePassword.Substring(seperatorIndex + 1); + } + + return true; + } + + return false; + } + /// /// Runs the authentication process /// From 7859798e8b1cf40a573db7e9a013abdd40469983 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Fri, 6 Aug 2021 11:37:10 +0200 Subject: [PATCH 08/10] Move basic auth to website part --- .../DependencyInjection/UmbracoBuilder.BackOfficeAuth.cs | 1 - .../Extensions/UmbracoApplicationBuilder.BackOffice.cs | 1 - .../Security/IBackOfficeSignInManager.cs | 0 .../DependencyInjection/UmbracoBuilderExtensions.cs | 2 ++ .../Extensions/UmbracoApplicationBuilder.Website.cs | 2 ++ .../Middleware/BasicAuthenticationMiddleware.cs} | 6 ++---- 6 files changed, 6 insertions(+), 6 deletions(-) rename src/{Umbraco.Web.BackOffice => Umbraco.Web.Common}/Security/IBackOfficeSignInManager.cs (100%) rename src/{Umbraco.Web.BackOffice/Middleware/BasicAuthAuthenticationMiddleware.cs => Umbraco.Web.Website/Middleware/BasicAuthenticationMiddleware.cs} (94%) diff --git a/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilder.BackOfficeAuth.cs b/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilder.BackOfficeAuth.cs index c7d7df33a7..24ecee08ef 100644 --- a/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilder.BackOfficeAuth.cs +++ b/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilder.BackOfficeAuth.cs @@ -50,7 +50,6 @@ namespace Umbraco.Extensions builder.Services.ConfigureOptions(); builder.Services.AddSingleton(); - builder.Services.AddSingleton(); builder.Services.AddUnique(); builder.Services.AddUnique, PasswordChanger>(); diff --git a/src/Umbraco.Web.BackOffice/Extensions/UmbracoApplicationBuilder.BackOffice.cs b/src/Umbraco.Web.BackOffice/Extensions/UmbracoApplicationBuilder.BackOffice.cs index 70be703fc5..29216ba980 100644 --- a/src/Umbraco.Web.BackOffice/Extensions/UmbracoApplicationBuilder.BackOffice.cs +++ b/src/Umbraco.Web.BackOffice/Extensions/UmbracoApplicationBuilder.BackOffice.cs @@ -31,7 +31,6 @@ namespace Umbraco.Extensions a => a.UseMiddleware()); builder.AppBuilder.UseMiddleware(); - builder.AppBuilder.UseMiddleware(); return builder; } diff --git a/src/Umbraco.Web.BackOffice/Security/IBackOfficeSignInManager.cs b/src/Umbraco.Web.Common/Security/IBackOfficeSignInManager.cs similarity index 100% rename from src/Umbraco.Web.BackOffice/Security/IBackOfficeSignInManager.cs rename to src/Umbraco.Web.Common/Security/IBackOfficeSignInManager.cs diff --git a/src/Umbraco.Web.Website/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.Web.Website/DependencyInjection/UmbracoBuilderExtensions.cs index 59905e5626..62ec5a9921 100644 --- a/src/Umbraco.Web.Website/DependencyInjection/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Web.Website/DependencyInjection/UmbracoBuilderExtensions.cs @@ -3,6 +3,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Infrastructure.DependencyInjection; +using Umbraco.Cms.Web.Common.Middleware; using Umbraco.Cms.Web.Common.Routing; using Umbraco.Cms.Web.Website.Collections; using Umbraco.Cms.Web.Website.Controllers; @@ -47,6 +48,7 @@ namespace Umbraco.Extensions builder.Services.AddSingleton(); builder.Services.AddSingleton(); + builder.Services.AddSingleton(); builder .AddDistributedCache() diff --git a/src/Umbraco.Web.Website/Extensions/UmbracoApplicationBuilder.Website.cs b/src/Umbraco.Web.Website/Extensions/UmbracoApplicationBuilder.Website.cs index 10cbbfdc4d..843d3030ec 100644 --- a/src/Umbraco.Web.Website/Extensions/UmbracoApplicationBuilder.Website.cs +++ b/src/Umbraco.Web.Website/Extensions/UmbracoApplicationBuilder.Website.cs @@ -2,6 +2,7 @@ using System; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Web.Common.ApplicationBuilder; +using Umbraco.Cms.Web.Common.Middleware; using Umbraco.Cms.Web.Website.Middleware; using Umbraco.Cms.Web.Website.Routing; @@ -20,6 +21,7 @@ namespace Umbraco.Extensions public static IUmbracoMiddlewareBuilder WithWebsite(this IUmbracoMiddlewareBuilder builder) { builder.AppBuilder.UseMiddleware(); + builder.AppBuilder.UseMiddleware(); return builder; } diff --git a/src/Umbraco.Web.BackOffice/Middleware/BasicAuthAuthenticationMiddleware.cs b/src/Umbraco.Web.Website/Middleware/BasicAuthenticationMiddleware.cs similarity index 94% rename from src/Umbraco.Web.BackOffice/Middleware/BasicAuthAuthenticationMiddleware.cs rename to src/Umbraco.Web.Website/Middleware/BasicAuthenticationMiddleware.cs index bd594169d2..7766867991 100644 --- a/src/Umbraco.Web.BackOffice/Middleware/BasicAuthAuthenticationMiddleware.cs +++ b/src/Umbraco.Web.Website/Middleware/BasicAuthenticationMiddleware.cs @@ -1,5 +1,3 @@ -using System; -using System.Text; using System.Threading.Tasks; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Http; @@ -15,12 +13,12 @@ namespace Umbraco.Cms.Web.Common.Middleware /// /// Provides basic authentication via back-office credentials for public website access if configured for use and the client IP is not allow listed. /// - public class BasicAuthAuthenticationMiddleware : IMiddleware + public class BasicAuthenticationMiddleware : IMiddleware { private readonly IRuntimeState _runtimeState; private readonly IBasicAuthService _basicAuthService; - public BasicAuthAuthenticationMiddleware( + public BasicAuthenticationMiddleware( IRuntimeState runtimeState, IBasicAuthService basicAuthService) { From 9732381933af612f2a755d229f36d3e7fcade0fe Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Fri, 6 Aug 2021 12:05:08 +0200 Subject: [PATCH 09/10] Added unit test for TryGetBasicAuthCredentials extension method and minor code tidy. --- .../Services/Implement/BasicAuthService.cs | 4 +- .../Extensions/HttpContextExtensionTests.cs | 42 +++++++++++++++++++ .../Extensions/HttpContextExtensions.cs | 9 ++-- .../BasicAuthenticationMiddleware.cs | 3 +- 4 files changed, 50 insertions(+), 8 deletions(-) create mode 100644 src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Extensions/HttpContextExtensionTests.cs diff --git a/src/Umbraco.Infrastructure/Services/Implement/BasicAuthService.cs b/src/Umbraco.Infrastructure/Services/Implement/BasicAuthService.cs index a685e4baca..9e413b7162 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/BasicAuthService.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/BasicAuthService.cs @@ -21,10 +21,10 @@ namespace Umbraco.Cms.Core.Services.Implement { foreach (var allowedIpString in _basicAuthSettings.AllowedIPs) { - if(IPNetwork.TryParse(allowedIpString, out var allowedIp) && allowedIp.Contains(clientIpAddress)) + if (IPNetwork.TryParse(allowedIpString, out IPNetwork allowedIp) && allowedIp.Contains(clientIpAddress)) { return true; - }; + } } return false; diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Extensions/HttpContextExtensionTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Extensions/HttpContextExtensionTests.cs new file mode 100644 index 0000000000..ba87c6b9c5 --- /dev/null +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Extensions/HttpContextExtensionTests.cs @@ -0,0 +1,42 @@ +// Copyright (c) Umbraco. +// See LICENSE for more details. + +using System; +using System.Text; +using Microsoft.AspNetCore.Http; +using NUnit.Framework; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.Common.Extensions +{ + [TestFixture] + public class HttpContextExtensionTests + { + [Test] + public void TryGetBasicAuthCredentials_WithoutHeader_ReturnsFalse() + { + var httpContext = new DefaultHttpContext(); + + var result = httpContext.TryGetBasicAuthCredentials(out string _, out string _); + + Assert.IsFalse(result); + } + + [Test] + public void TryGetBasicAuthCredentials_WithHeader_ReturnsTrueWithCredentials() + { + const string Username = "fred"; + const string Password = "test"; + + var httpContext = new DefaultHttpContext(); + var credentials = Convert.ToBase64String(Encoding.ASCII.GetBytes($"{Username}:{Password}")); + httpContext.Request.Headers.Add("Authorization", $"Basic {credentials}"); + + bool result = httpContext.TryGetBasicAuthCredentials(out string username, out string password); + + Assert.IsTrue(result); + Assert.AreEqual(Username, username); + Assert.AreEqual(Password, password); + } + } +} diff --git a/src/Umbraco.Web.Common/Extensions/HttpContextExtensions.cs b/src/Umbraco.Web.Common/Extensions/HttpContextExtensions.cs index afd0c5be48..d6beb90c01 100644 --- a/src/Umbraco.Web.Common/Extensions/HttpContextExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/HttpContextExtensions.cs @@ -5,13 +5,12 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; +using Microsoft.Extensions.Primitives; namespace Umbraco.Extensions { public static class HttpContextExtensions { - - /// /// Try to get the basic auth username and password from the http context. /// @@ -20,14 +19,14 @@ namespace Umbraco.Extensions username = null; password = null; - if ( httpContext.Request.Headers.TryGetValue("Authorization", out var authHeaders)) + if (httpContext.Request.Headers.TryGetValue("Authorization", out StringValues authHeaders)) { var authHeader = authHeaders.ToString(); if (authHeader is not null && authHeader.StartsWith("Basic")) { - //Extract credentials + // Extract credentials. var encodedUsernamePassword = authHeader.Substring(6).Trim(); - var encoding = Encoding.UTF8; + Encoding encoding = Encoding.UTF8; var usernamePassword = encoding.GetString(Convert.FromBase64String(encodedUsernamePassword)); var seperatorIndex = usernamePassword.IndexOf(':'); diff --git a/src/Umbraco.Web.Website/Middleware/BasicAuthenticationMiddleware.cs b/src/Umbraco.Web.Website/Middleware/BasicAuthenticationMiddleware.cs index 7766867991..be2ee24d4f 100644 --- a/src/Umbraco.Web.Website/Middleware/BasicAuthenticationMiddleware.cs +++ b/src/Umbraco.Web.Website/Middleware/BasicAuthenticationMiddleware.cs @@ -1,3 +1,4 @@ +using System.Net; using System.Threading.Tasks; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Http; @@ -35,7 +36,7 @@ namespace Umbraco.Cms.Web.Common.Middleware return; } - var clientIPAddress = context.Connection.RemoteIpAddress; + IPAddress clientIPAddress = context.Connection.RemoteIpAddress; if (_basicAuthService.IsIpAllowListed(clientIPAddress)) { await next(context); From 641546d427cb8f3eb321affd717873a83d31969a Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Fri, 6 Aug 2021 12:29:05 +0200 Subject: [PATCH 10/10] Skip for backoffice requests + Handle potential missing backoffice signin manager --- .../BasicAuthenticationMiddleware.cs | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/src/Umbraco.Web.Website/Middleware/BasicAuthenticationMiddleware.cs b/src/Umbraco.Web.Website/Middleware/BasicAuthenticationMiddleware.cs index 7766867991..4241374c76 100644 --- a/src/Umbraco.Web.Website/Middleware/BasicAuthenticationMiddleware.cs +++ b/src/Umbraco.Web.Website/Middleware/BasicAuthenticationMiddleware.cs @@ -29,7 +29,7 @@ namespace Umbraco.Cms.Web.Common.Middleware /// public async Task InvokeAsync(HttpContext context, RequestDelegate next) { - if (_runtimeState.Level < RuntimeLevel.Run || !_basicAuthService.IsBasicAuthEnabled()) + if (_runtimeState.Level < RuntimeLevel.Run || context.Request.IsBackOfficeRequest() || !_basicAuthService.IsBasicAuthEnabled()) { await next(context); return; @@ -52,14 +52,21 @@ namespace Umbraco.Cms.Web.Common.Middleware if (context.TryGetBasicAuthCredentials(out var username, out var password)) { IBackOfficeSignInManager backOfficeSignInManager = - context.RequestServices.GetRequiredService(); + context.RequestServices.GetService(); - SignInResult signInResult = - await backOfficeSignInManager.PasswordSignInAsync(username, password, false, true); - - if (signInResult.Succeeded) + if (backOfficeSignInManager is not null) { - await next.Invoke(context); + SignInResult signInResult = + await backOfficeSignInManager.PasswordSignInAsync(username, password, false, true); + + if (signInResult.Succeeded) + { + await next.Invoke(context); + } + else + { + SetUnauthorizedHeader(context); + } } else {