Automatically Adding Member Groups to Umbraco Public Access
23rd February, 2025
How to use middleware to enforce/add a default Member group to group-based Public Access restrictions in Umbraco.
Recently, I worked on a project that used Umbraco's Public Access Restrictions to give frontend Members access to different parts of the site. Additionally, the members were split into different groups and, therefore, had access to different areas depending on which group they were in.
As part of this, the client required an "Administrators" member group, so that non-backoffice users could moderate the user-generated content in these group-restricted frontend pages. And, as these groups and group-restricted areas were going to be regularly created, updated, and deleted, I needed to figure out a way to allow access for the "Administrators" group in a blanket manner.
Unfortunately, Public Access in Umbraco is quite limited, there's no out-of-the-box way to setup default allowed Member Groups, set a default login page or even a default access denied page... so if you're content editors need to set public access regularly, then adding these every time is honestly a bit of a bit of a pain as you'll need to select these each time you want to enable Public Access restrictions on a node.
How do I automatically include a group when a content editor sets up Public Access?
UPDATE: 09/05/2025
Whilst reading the Umbraco documentation today, I stumbled upon the API Documentation website, which contained a much more expansive list of Notifications than what was listed in the Docs site:
API Docs: Namespace Umbraco.Cms.Core.Notifications
and one of the notifications listed was PublicAccessEntrySavingNotification!
API Docs: Class PublicAccessEntrySavingNotification
Ahhh! I honestly have no idea how I missed this when looking at the docs back in February...
So yup, it turns out Umbraco does in fact have notifications for Public Access!!! 🤦
Here's an updated code sample of how you can use that notification to automatically add a Member Group to group-based public access rules:
// Our Public Access Notification Handler
public class PublicAccessNotificationHandler : INotificationHandler<PublicAccessEntrySavingNotification>
{
public void Handle(PublicAccessEntrySavingNotification notification)
{
foreach (var accessEntry in notification.SavedEntities)
{
// Check for Admin role
var includesAdmin = false;
var isGroupProtected = false;
foreach (var publicAccessRule in accessEntry.Rules)
{
if (publicAccessRule.RuleType != Constants.Conventions.PublicAccess.MemberRoleRuleType) { continue; }
isGroupProtected = true;
includesAdmin = publicAccessRule.RuleValue == "Administrators" || includesAdmin;
}
// Add Admin role if not present
if (!isGroupProtected || includesAdmin) { continue; }
accessEntry.AddRule("Administrators", Constants.Conventions.PublicAccess.MemberRoleRuleType);
}
}
}
// Register the Notification Handler
public class Composer : IComposer
{
public void Compose(IUmbracoBuilder builder)
{
builder.AddNotificationHandler<PublicAccessEntrySavingNotification, PublicAccessNotificationHandler>();
}
}
And that's it! That's all you'll need!
I'll leave the rest of the original blog below as a reference on how to do it using middleware interception.
As there's no Notification that I could use to hook into the saving of public access, like we can with content saving etc.., I instead created some Middleware to intercept the Public Access save request; and inject my group that way:
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Primitives;
using System.Text;
namespace MyProject.Web.Middleware
{
public class PublicAccessMiddleware
{
private readonly RequestDelegate _next;
public PublicAccessMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext context)
{
if (context.Request.Path.StartsWithSegments("/umbraco/backoffice/umbracoapi/publicaccess/PostPublicAccess") == false)
{
await _next(context);
return;
}
// Read the query string parameters
var query = Microsoft.AspNetCore.WebUtilities.QueryHelpers.ParseQuery(context.Request.QueryString.Value);
// Check if "groups[]" exists in the query string
if (query.TryGetValue("groups[]", out StringValues values) == false)
{
await _next(context);
return;
}
// Check if we are setting any groups
var groups = values.ToArray();
if (groups.Any() == false)
{
await _next(context);
return;
}
// Check if Admins are already included
var pos = Array.IndexOf(groups, "Administrators");
if (pos > -1)
{
await _next(context);
return;
}
// Modify the groups[] value to include Admins
Array.Resize(ref groups, groups.Length + 1);
groups[^1] = "Administrators";
// Create a new query string with the modified groups[]
var modifiedQuery = new Dictionary<string, StringValues>(query)
{
["groups[]"] = new StringValues(groups)
};
// Rebuild the query string
var newQueryString = new StringBuilder("?");
foreach (var kvp in modifiedQuery)
{
foreach (var value in kvp.Value)
{
newQueryString.Append($"{kvp.Key}={Uri.EscapeDataString(value)}&");
}
}
// Update the request with the modified query string
context.Request.QueryString = new QueryString(newQueryString.ToString().TrimEnd('&'));
await _next(context);
}
}
public static class PublicAccessMiddlewareExtensions
{
/// <summary>
/// Registers the Public Access Middleware which will intercept requests to the
/// "/umbraco/backoffice/umbracoapi/publicaccess/PostPublicAccess" endpoint and inject
/// the Administrators Member group if it is not present in the list of groups.
/// </summary>
/// <param name="app"></param>
public static void RegisterPublicAccessMiddleware(this WebApplication app)
{
app.UseWhen(context =>
context.Request.Path.StartsWithSegments("/umbraco/backoffice/umbracoapi/publicaccess/PostPublicAccess"),
appBuilder => appBuilder.UseMiddleware<PublicAccessMiddleware>());
}
}
}
As you can see in the above code snippets, I intercept the Public Access save request, check whether we're saving groups, check whether the Administrators group is already included, and add it to the query string if not.
Tip: I would recommend sticking the group name in a "constants" class so that you only need to change it in one place if needed.
namespace MyProject.Models.Constants
{
public static class MemberGroupConstants
{
public const string Admin = "Administrators";
}
}
Now all we need to do is call our registration code in our Program.cs, before, and off we go!
//...
app.RegisterPublicAccessMiddleware();
app.UseUmbraco()
//....
Demo:

And there we go, now my content editors don't have to remember to add the Admin group manually every time!
Now, I just need to figure out how to automatically include the login and access denied pages... 🤔