Added POC of basic auth middleware

This commit is contained in:
Bjarke Berg
2021-08-05 21:42:36 +02:00
parent 61b343da8b
commit 4f2cb09939
13 changed files with 199 additions and 16 deletions

View File

@@ -0,0 +1,26 @@
// Copyright (c) Umbraco.
// See LICENSE for more details.
using System.ComponentModel;
using System.Net;
namespace Umbraco.Cms.Core.Configuration.Models
{
/// <summary>
/// Typed configuration options for security settings.
/// </summary>
[UmbracoOptions(Constants.Configuration.ConfigBasicAuth)]
public class BasicAuthSettings
{
private const bool StaticEnabled = false;
/// <summary>
/// Gets or sets a value indicating whether to keep the user logged in.
/// </summary>
[DefaultValue(StaticEnabled)]
public bool Enabled { get; set; } = StaticEnabled;
public string[] AllowedIPs { get; set; } = new string[0];
}
}

View File

@@ -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";

View File

@@ -71,6 +71,7 @@ namespace Umbraco.Cms.Core.DependencyInjection
.AddUmbracoOptions<UmbracoPluginSettings>()
.AddUmbracoOptions<UnattendedSettings>()
.AddUmbracoOptions<RichTextEditorSettings>()
.AddUmbracoOptions<BasicAuthSettings>()
.AddUmbracoOptions<RuntimeMinificationSettings>();
return builder;

View File

@@ -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<ConfigureBackOfficeCookieOptions>();
builder.Services.AddSingleton<BackOfficeExternalLoginProviderErrorMiddleware>();
builder.Services.AddScoped<BasicAuthAuthenticationMiddleware>();
builder.Services.AddUnique<IBackOfficeAntiforgery, BackOfficeAntiforgery>();
builder.Services.AddUnique<IPasswordChanger<BackOfficeIdentityUser>, PasswordChanger<BackOfficeIdentityUser>>();

View File

@@ -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;
}
}

View File

@@ -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<KeepAliveMiddleware>());
builder.AppBuilder.UseMiddleware<BackOfficeExternalLoginProviderErrorMiddleware>();
builder.AppBuilder.UseMiddleware<BasicAuthAuthenticationMiddleware>();
return builder;
}

View File

@@ -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
{
/// <summary>
/// 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
/// </summary>
public class BasicAuthAuthenticationMiddleware : IMiddleware
{
private readonly ILogger<BasicAuthAuthenticationMiddleware> _logger;
private readonly IOptionsSnapshot<BasicAuthSettings> _basicAuthSettings;
private readonly IRuntimeState _runtimeState;
public BasicAuthAuthenticationMiddleware(
ILogger<BasicAuthAuthenticationMiddleware> logger,
IOptionsSnapshot<BasicAuthSettings> basicAuthSettings,
IRuntimeState runtimeState)
{
_logger = logger;
_basicAuthSettings = basicAuthSettings;
_runtimeState = runtimeState;
}
/// <inheritdoc />
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<IBackOfficeSignInManager>();
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\"");
}
}
}

View File

@@ -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<BasicAuthSettings> _basicAuthSettingsMonitor;
/// <summary>
/// Initializes a new instance of the <see cref="BackOfficeCookieManager"/> 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> basicAuthSettings)
: this(umbracoContextAccessor, runtime, null, umbracoRequestPaths, basicAuthSettings)
{
}
@@ -42,12 +46,14 @@ namespace Umbraco.Cms.Web.BackOffice.Security
IUmbracoContextAccessor umbracoContextAccessor,
IRuntimeState runtime,
IEnumerable<string> explicitPaths,
UmbracoRequestPaths umbracoRequestPaths)
UmbracoRequestPaths umbracoRequestPaths,
IOptionsMonitor<BasicAuthSettings> basicAuthSettingsMonitor)
{
_umbracoContextAccessor = umbracoContextAccessor;
_runtime = runtime;
_explicitPaths = explicitPaths?.ToArray();
_umbracoRequestPaths = umbracoRequestPaths;
_basicAuthSettingsMonitor = basicAuthSettingsMonitor;
}
/// <summary>
@@ -88,6 +94,11 @@ namespace Umbraco.Cms.Web.BackOffice.Security
return true;
}
if (_basicAuthSettingsMonitor.CurrentValue.Enabled)
{
return true;
}
return false;
}

View File

@@ -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<BasicAuthSettings> _optionsSnapshot;
/// <summary>
/// Initializes a new instance of the <see cref="ConfigureBackOfficeCookieOptions"/> class.
@@ -59,7 +60,8 @@ namespace Umbraco.Cms.Web.BackOffice.Security
IUserService userService,
IIpResolver ipResolver,
ISystemClock systemClock,
UmbracoRequestPaths umbracoRequestPaths)
UmbracoRequestPaths umbracoRequestPaths,
IOptionsMonitor<BasicAuthSettings> optionsSnapshot)
{
_serviceProvider = serviceProvider;
_umbracoContextAccessor = umbracoContextAccessor;
@@ -72,6 +74,7 @@ namespace Umbraco.Cms.Web.BackOffice.Security
_ipResolver = ipResolver;
_systemClock = systemClock;
_umbracoRequestPaths = umbracoRequestPaths;
_optionsSnapshot = optionsSnapshot;
}
/// <inheritdoc />
@@ -115,7 +118,9 @@ namespace Umbraco.Cms.Web.BackOffice.Security
options.CookieManager = new BackOfficeCookieManager(
_umbracoContextAccessor,
_runtimeState,
_umbracoRequestPaths);
_umbracoRequestPaths,
_optionsSnapshot
);
options.Events = new CookieAuthenticationEvents
{

View File

@@ -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(

View File

@@ -37,7 +37,7 @@ namespace Umbraco.Extensions
IOptions<UmbracoPipelineOptions> startupOptions = app.ApplicationServices.GetRequiredService<IOptions<UmbracoPipelineOptions>>();
app.RunPrePipeline(startupOptions.Value);
app.UseUmbracoCore();
app.UseUmbracoRequestLogging();

View File

@@ -14,13 +14,7 @@ namespace Umbraco.Extensions
/// <returns></returns>
public static async Task<AuthenticateResult> 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();
}
/// <summary>

View File

@@ -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
{
/// <summary>
/// Runs the authentication process
/// </summary>
public static async Task<AuthenticateResult> AuthenticateBackOfficeAsync(this HttpContext httpContext)
{
if (httpContext == null)
{
return AuthenticateResult.NoResult();
}
var result = await httpContext.AuthenticateAsync(Cms.Core.Constants.Security.BackOfficeAuthenticationType);
return result;
}
/// <summary>
/// Get the value in the request form or query string for the key
/// </summary>