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
///