Using UMB_UCONTEXT with Umbraco 14+

When the “new” backoffice was released with Umbraco 14, it ushered in several paradigm shifts for Umbraco. One of those were backoffice authentication.
In Umbraco 13 and below, backoffice authentication is cookie based. Umbraco 14 and above uses bearer tokens for backoffice authentication.
”Why should I care?”, you ask? Well, hopefully you shouldn’t 😉
Unless of course you’re relying on the backoffice authentication cookie to do stuff 🫣
A brief history lesson
The backoffice authentication cookie UMB_UCONTEXT
has been around since forever. Over the years, the Umbraco community has fostered many creative ways of using it, because it allows for authenticating backoffice users outside the scope of the backoffice itself.
One could be prone to frown upon this piggybacking on the Umbraco authentication, but that’s really a moot point - it’s happening all the same 🙃
Fast-forward to present day
As it happens, the UMB_UCONTEXT
cookie is still around in Umbraco 14+, but it no longer carries the same responsibility. It now solely exists to facilitate the OpenID Connect sign-in flow, in order to obtain the first access token.
It is meant to expire quickly, not to be kept alive.
Or even more to the point: There is no guarantee that it will stick around forever 💀
This calls for a new mechanism to replace the UMB_UCONTEXT
cookie, if it’s still relevant to authenticate backoffice users in other contexts than the backoffice itself.
I propose… another cookie 😆
Cookies! Gimme cookies!
ASP.NET Core is more than capable of juggling multiple authentication schemes at the same time. Case in point, Umbraco runs several schemes simultaneously to facilitate various sign-in options for both members and backoffice users.
Adding a new one is the easy part:
using Umbraco.Cms.Core.Composing;
namespace Site.CookieAuth;
public class MyCookieAuthComposer : IComposer
{
// configure a custom authentication scheme (cookie based)
public void Compose(IUmbracoBuilder builder)
=> builder.Services
.AddAuthentication()
.AddCookie(
"MyCookieScheme",
options => options.Cookie.Name = "MyCookieName"
);
}
The trick is to trigger the sign-in and to keep the authentication cookie alive.
Umbraco exposes several notifications tied to backoffice user sessions - for example, the UserLoginSuccessNotification
. In principle these could be used to manage the cookie, but as they’re only fired once per user session, the cookie would require a very long lifetime to keep from expiring.
Luckily, there is a better alternative.
OpenIddict
Under the hood, Umbraco uses OpenIddict to power everything OpenID Connect. And OpenIddict in turn has an abundance of events to hook into.
For the authentication cookie management, I’m particularly interested in:
- When access tokens are created or refreshed (for creating or refreshing the cookie).
- When access tokens are revoked (for clearing the cookie).
An event handler for OpenIddict an implementation of IOpenIddictServerHandler<T>
for the concrete events of interest:
using OpenIddict.Server;
namespace Site.CookieAuth;
public class OpenIddictServerEventsHandler :
IOpenIddictServerHandler<OpenIddictServerEvents.GenerateTokenContext>,
IOpenIddictServerHandler<OpenIddictServerEvents.ApplyRevocationResponseContext>
{
// event handler for when access tokens are generated (created or refreshed)
public async ValueTask HandleAsync(OpenIddictServerEvents.GenerateTokenContext context)
{
// ...
}
// event handler for when access tokens are revoked
public async ValueTask HandleAsync(OpenIddictServerEvents.ApplyRevocationResponseContext context)
{
// ...
}
}
Among other things, the context
parameter of the GenerateTokenContext
handler contains the ClaimsPrincipal
of the authenticated backoffice user. This can be used to create a new, authenticated principal for the custom cookie authentication scheme.
The OpenIddict events are wired up in the service collection:
using OpenIddict.Server;
using Umbraco.Cms.Core.Composing;
namespace Site.CookieAuth;
public class OpenIddictServerEventsComposer : IComposer
{
public void Compose(IUmbracoBuilder builder)
{
builder.Services.AddSingleton<OpenIddictServerEventsHandler>();
// register event handlers with OpenIddict
builder.Services.Configure<OpenIddictServerOptions>(options =>
{
options.Handlers.Add(
OpenIddictServerHandlerDescriptor
.CreateBuilder<OpenIddictServerEvents.GenerateTokenContext>()
.UseSingletonHandler<OpenIddictServerEventsHandler>()
.Build()
);
options.Handlers.Add(
OpenIddictServerHandlerDescriptor
.CreateBuilder<OpenIddictServerEvents.ApplyRevocationResponseContext>()
.UseSingletonHandler<OpenIddictServerEventsHandler>()
.Build()
);
});
}
}
Putting it to use
That was the hard part. From here on out, ASP.NET Core authorization takes care of things 🔒
A common use case for all of this is conditional output rendering for authenticated backoffice users. That could look something like this:
@using Microsoft.AspNetCore.Authentication
@{
Layout = null;
var result = await Context.AuthenticateAsync("MyCookieScheme");
var isAuthenticated = result.Succeeded;
var userName = result.Principal?.Identity?.Name;
}
<header>
@if (isAuthenticated)
{
<p>Welcome back, @userName 👋</p>
}
else
{
<p>Hello, Anonymous 👋</p>
}
</header>
Another common use case is restricting access to 3rd party add-ons hosted on the same instance as Umbraco - for example, protecting the Hangfire dashboard:
using Hangfire;
using Hangfire.Dashboard;
using Microsoft.AspNetCore.Authentication;
using Umbraco.Cms.Core.Composing;
using Umbraco.Cms.Web.Common.ApplicationBuilder;
namespace Site.Hangfire;
public class MyCookieAuthorizationFilter : IDashboardAsyncAuthorizationFilter
{
public async Task<bool> AuthorizeAsync(DashboardContext context)
{
var result = await context.GetHttpContext().AuthenticateAsync("MyCookieScheme");
return result.Succeeded;
}
}
public class HangfireComposer : IComposer
{
public void Compose(IUmbracoBuilder builder)
{
builder.Services.AddHangfire(...);
builder.Services.AddHangfireServer();
builder.Services.Configure<UmbracoPipelineOptions>(options =>
{
options.AddFilter(new UmbracoPipelineFilter("HangfireDashboard")
{
Endpoints = app => app.UseHangfireDashboard(
"/umbraco/hangfire",
new DashboardOptions
{
AsyncAuthorization = [new MyCookieAuthorizationFilter()]
})
});
});
}
}
Sample project
As already mentioned, I have created a GitHub repo for this blog post. It contains all the necessary bits to set up and manage the custom authentication cookie, as well as working samples of the use cases from the previous section:
- The home template showcases conditional rendering for authenticated backoffice users.
- The Hangfire composer sets up protection for the Hangfire dashboard.
I have included the Umbraco and Hangfire databases in the repo, so if you clone it down, you can run the samples with dotnet run
from the /src/Site
folder. The Umbraco administrator credentials are:
- Username: admin@localhost
- Password: SuperSecret123
Happy hacking 💜