Securing the backoffice of a content management system is crucial for maintaining integrity and preventing unauthorized access. In the case of Umbraco, which runs on ASP.NET Core, one effective way to do this is by restricting access to the backoffice URL based on the request’s host domain. This article explores a clean and extensible approach using custom middleware and configuration.
TLDR: Package link: https://marketplace.umbraco.com/package/umbraco.community.domainrestriction
⚙️ What Problem Are We Solving?
The Umbraco backoffice is accessible by default from any valid request hitting the /umbraco
path. This can be problematic in scenarios where the environment has multiple domains or where strict domain rules are required for administrative access.
To solve this, we implement a middleware that intercepts requests to /umbraco
, checks whether the request originates from an authorized domain and either allows the request or blocks it with a 404 Not Found
response or a redirection.
🧱 Core Components of the Solution
Our solution consists of three main parts:
Middleware for intercepting and validating incoming requests
Configuration settings class
Extension methods for easy setup
Let’s explore each in detail.
🧩 Configuration Class
The DomainRestrictionSettings
class is designed to be bound from the appsettings.json
file. It includes keys for enabling the feature, setting the authorized domain and defining a custom redirect URL:
public class DomainRestrictionSettings
{
public const string SectionName = "DomainRestriction";
public bool Enabled { get; init; } = false;
public string UmbracoPath { get; init; } = "/umbraco";
public string UmbracoDomain { get; init; } = string.Empty;
public string RedirectUrl { get; init; } = string.Empty;
}
This class provides a strongly typed representation of the settings and supports validation through data annotations.
🚦 Custom Middleware Logic
The middleware inspects each request and performs logic based on the configuration:
public class DomainRestrictionMiddleware(
RequestDelegate next,
DomainRestrictionConfigService domainRestrictionConfigService
)
{
public async Task Invoke(HttpContext context)
{
var isEnabled = domainRestrictionConfigService.Settings.Enabled;
var umbracoDomain = domainRestrictionConfigService.Settings.UmbracoDomain;
var umbracoPath = domainRestrictionConfigService.Settings.UmbracoPath.TrimStart('~');
var requestedPath = context.Request.Path.ToString();
var requestedHost = context.Request.Host.ToString();
if (!isEnabled || string.IsNullOrWhiteSpace(umbracoDomain) || string.IsNullOrWhiteSpace(umbracoPath))
{
await next.Invoke(context);
return;
}
if (requestedPath.InvariantContains(umbracoPath) && !requestedHost.InvariantContains(umbracoDomain))
{
context.Response.StatusCode = (int)HttpStatusCode.NotFound;
if (!string.IsNullOrWhiteSpace(domainRestrictionConfigService.Settings.RedirectUrl))
{
context.Response.Redirect(domainRestrictionConfigService.Settings.RedirectUrl);
}
}
await next.Invoke(context);
}
}
The middleware gracefully continues the pipeline if the settings are disabled or invalid. Otherwise, it applies domain checks before proceeding.
🧰 Extension Methods for Setup
To integrate the feature cleanly, we define two extension methods:
public static class BuilderExtensions
{
public static IApplicationBuilder UseDomainRestriction(this IApplicationBuilder builder)
{
return builder.UseMiddleware<DomainRestrictionMiddleware>();
}
public static IUmbracoBuilder AddDomainRestrictionConfigs(this IUmbracoBuilder builder, Action<DomainRestrictionSettings> defaultOptions = default)
{
if (builder.Services.FirstOrDefault(x => x.ServiceType == typeof(DomainRestrictionConfigService)) != null)
{
return builder;
}
var configSection = builder.Config.GetSection(DomainRestrictionSettings.SectionName);
var options = builder.Services.AddOptions<DomainRestrictionSettings>()
.Bind(configSection);
if (defaultOptions != default)
{
options.Configure(defaultOptions);
}
options.ValidateDataAnnotations();
builder.Services.AddSingleton<DomainRestrictionConfigService>();
return builder;
}
}
This makes integration into the Startup pipeline seamless and maintainable.
🧪 Sample Configuration in appsettings.json
Here is how the settings can be added:
"DomainRestriction": {
"Enabled": true,
"UmbracoPath": "/umbraco",
"UmbracoDomain": "admin.example.com",
"RedirectUrl": "https://www.example.com/not-authorized"
}
🧩 Usage in Startup
Finally, wire everything up in Startup.cs
:
builder.AddDomainRestrictionConfigs();
app.UseDomainRestriction();
This ensures that the domain restriction check is applied globally to all incoming requests.
✅ Conclusion
By combining configuration-driven logic with ASP.NET Core middleware, we have implemented a safe and scalable way to restrict access to the Umbraco backoffice. This pattern can easily be extended or reused for other paths or domain-based security features.
Use this approach in multi-tenant or production environments where domain-specific access control is required.