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/Configuration/Models/BasicAuthSettings.cs b/src/Umbraco.Core/Configuration/Models/BasicAuthSettings.cs new file mode 100644 index 0000000000..054619d843 --- /dev/null +++ b/src/Umbraco.Core/Configuration/Models/BasicAuthSettings.cs @@ -0,0 +1,27 @@ +// Copyright (c) Umbraco. +// See LICENSE for more details. + +using System; +using System.ComponentModel; +using System.Net; + +namespace Umbraco.Cms.Core.Configuration.Models +{ + /// + /// Typed configuration options for basic authentication 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; } = Array.Empty(); + } +} 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.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.Infrastructure/Services/Implement/BasicAuthService.cs b/src/Umbraco.Infrastructure/Services/Implement/BasicAuthService.cs new file mode 100644 index 0000000000..9e413b7162 --- /dev/null +++ b/src/Umbraco.Infrastructure/Services/Implement/BasicAuthService.cs @@ -0,0 +1,33 @@ +using System.Net; +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core.Configuration.Models; + +namespace Umbraco.Cms.Core.Services.Implement +{ + 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 (IPNetwork.TryParse(allowedIpString, out IPNetwork allowedIp) && allowedIp.Contains(clientIpAddress)) + { + return true; + } + } + + return false; + } + } +} diff --git a/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj b/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj index 4266f1ee6a..3a3c46e3f2 100644 --- a/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj +++ b/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj @@ -20,6 +20,8 @@ + + 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..6322debaab --- /dev/null +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Core/Services/BasicAuthServiceTests.cs @@ -0,0 +1,37 @@ +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.Implement; + +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)] + [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 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})); + + 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.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.BackOffice/DependencyInjection/UmbracoBuilder.BackOfficeAuth.cs b/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilder.BackOfficeAuth.cs index a652136a0f..24ecee08ef 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 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..29216ba980 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 { diff --git a/src/Umbraco.Web.BackOffice/Security/BackOfficeCookieManager.cs b/src/Umbraco.Web.BackOffice/Security/BackOfficeCookieManager.cs index a05af07bb6..5d50981f6a 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 IBasicAuthService _basicAuthService; /// /// 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, + IBasicAuthService basicAuthService) + : this(umbracoContextAccessor, runtime, null, umbracoRequestPaths, basicAuthService) { } @@ -42,12 +46,14 @@ namespace Umbraco.Cms.Web.BackOffice.Security IUmbracoContextAccessor umbracoContextAccessor, IRuntimeState runtime, IEnumerable explicitPaths, - UmbracoRequestPaths umbracoRequestPaths) + UmbracoRequestPaths umbracoRequestPaths, + IBasicAuthService basicAuthService) { _umbracoContextAccessor = umbracoContextAccessor; _runtime = runtime; _explicitPaths = explicitPaths?.ToArray(); _umbracoRequestPaths = umbracoRequestPaths; + _basicAuthService = basicAuthService; } /// @@ -88,6 +94,11 @@ namespace Umbraco.Cms.Web.BackOffice.Security return true; } + if (_basicAuthService.IsBasicAuthEnabled()) + { + return true; + } + return false; } diff --git a/src/Umbraco.Web.BackOffice/Security/ConfigureBackOfficeCookieOptions.cs b/src/Umbraco.Web.BackOffice/Security/ConfigureBackOfficeCookieOptions.cs index 024ee50aaf..1457732c53 100644 --- a/src/Umbraco.Web.BackOffice/Security/ConfigureBackOfficeCookieOptions.cs +++ b/src/Umbraco.Web.BackOffice/Security/ConfigureBackOfficeCookieOptions.cs @@ -34,6 +34,8 @@ 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; /// /// Initializes a new instance of the class. @@ -59,7 +61,8 @@ namespace Umbraco.Cms.Web.BackOffice.Security IUserService userService, IIpResolver ipResolver, ISystemClock systemClock, - UmbracoRequestPaths umbracoRequestPaths) + UmbracoRequestPaths umbracoRequestPaths, + IBasicAuthService basicAuthService) { _serviceProvider = serviceProvider; _umbracoContextAccessor = umbracoContextAccessor; @@ -72,6 +75,7 @@ namespace Umbraco.Cms.Web.BackOffice.Security _ipResolver = ipResolver; _systemClock = systemClock; _umbracoRequestPaths = umbracoRequestPaths; + _basicAuthService = basicAuthService; } /// @@ -115,7 +119,9 @@ namespace Umbraco.Cms.Web.BackOffice.Security options.CookieManager = new BackOfficeCookieManager( _umbracoContextAccessor, _runtimeState, - _umbracoRequestPaths); + _umbracoRequestPaths, + _basicAuthService + ); 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..d6beb90c01 100644 --- a/src/Umbraco.Web.Common/Extensions/HttpContextExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/HttpContextExtensions.cs @@ -1,12 +1,60 @@ using System; using System.Security.Claims; +using System.Text; +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. + /// + public static bool TryGetBasicAuthCredentials(this HttpContext httpContext, out string username, out string password) + { + username = null; + password = null; + + if (httpContext.Request.Headers.TryGetValue("Authorization", out StringValues authHeaders)) + { + var authHeader = authHeaders.ToString(); + if (authHeader is not null && authHeader.StartsWith("Basic")) + { + // Extract credentials. + var encodedUsernamePassword = authHeader.Substring(6).Trim(); + Encoding 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 + /// + 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 /// 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.Website/Middleware/BasicAuthenticationMiddleware.cs b/src/Umbraco.Web.Website/Middleware/BasicAuthenticationMiddleware.cs new file mode 100644 index 0000000000..92836e2456 --- /dev/null +++ b/src/Umbraco.Web.Website/Middleware/BasicAuthenticationMiddleware.cs @@ -0,0 +1,90 @@ +using System.Net; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Web.BackOffice.Security; +using Umbraco.Extensions; + +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 BasicAuthenticationMiddleware : IMiddleware + { + private readonly IRuntimeState _runtimeState; + private readonly IBasicAuthService _basicAuthService; + + public BasicAuthenticationMiddleware( + IRuntimeState runtimeState, + IBasicAuthService basicAuthService) + { + _runtimeState = runtimeState; + _basicAuthService = basicAuthService; + } + + /// + public async Task InvokeAsync(HttpContext context, RequestDelegate next) + { + if (_runtimeState.Level < RuntimeLevel.Run || context.Request.IsBackOfficeRequest() || !_basicAuthService.IsBasicAuthEnabled()) + { + await next(context); + return; + } + + IPAddress clientIPAddress = context.Connection.RemoteIpAddress; + if (_basicAuthService.IsIpAllowListed(clientIPAddress)) + { + await next(context); + return; + } + + AuthenticateResult authenticateResult = await context.AuthenticateBackOfficeAsync(); + if (authenticateResult.Succeeded) + { + await next(context); + return; + } + + if (context.TryGetBasicAuthCredentials(out var username, out var password)) + { + IBackOfficeSignInManager backOfficeSignInManager = + context.RequestServices.GetService(); + + if (backOfficeSignInManager is not null) + { + SignInResult signInResult = + await backOfficeSignInManager.PasswordSignInAsync(username, password, false, true); + + if (signInResult.Succeeded) + { + await next.Invoke(context); + } + else + { + SetUnauthorizedHeader(context); + } + } + else + { + SetUnauthorizedHeader(context); + } + } + else + { + // no authorization header + SetUnauthorizedHeader(context); + } + } + + private static void SetUnauthorizedHeader(HttpContext context) + { + context.Response.StatusCode = 401; + context.Response.Headers.Add("WWW-Authenticate", "Basic realm=\"Umbraco login\""); + } + } +}