Automatically Adding Member Groups to Umbraco Public Access

23rd February, 2025

Owain Jones

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?

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:

GIF demoing the automatic adding of a member group when saving Public Access settings
GIF showcasing the middleware in action

 

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... 🤔