Securing and Expanding the Delivery Api
Kaspar Boel KjeldsenIn the last post I promised we'd talk about securing the Content Delivery API. I also promised to keep these posts shorter. Let's see which promise survives.
If you've read Headless — Not Hovedløst, you already know this gripe. Umbraco's Content Delivery API is public by default. Turn off public access, and you get a single API key. One key. For everything. Every site, every integration, every external consumer — same key, same access, same "I hope nobody loses this" energy.

One key to the kingdom. And it really is the kingdom. That key doesn't scope to a specific site or content tree. If you're running a multi-site Umbraco, every integration with that key can query every site's content. Site A's frontend can fetch Site B's unpublished pricing page. Your client's developer poking around with Postman has access to every domain in the solution. And if you need to revoke access because a third party shouldn't have it anymore? You rotate the single key and break every other integration using it. Delightful.
The first two are real, production requirements. I'm already running multi-key access for a client project where the client has their own key to experiment with the Delivery API for integrations. It is a key I can rotate or revoke without touching my own. That's not exotic. That's Tuesday.
The third one is this blog post.
But before we get to the insane part, let's start with what Umbraco actually gives us to work with, and why PublicAccess: true is, counterintuitively, the starting point for more control.
The Trick: PublicAccess true, Access Controlled
This sounds backwards, and I know that. Bear with me.
When PublicAccess is false, Umbraco requires its single API key on every request. You're locked into their access model. When PublicAccess is true, Umbraco doesn't enforce any access control at all. But it does still call IRequestMemberAccessService.MemberHasAccessToAsync for every content item.
That interface is your hook.
Replace the default implementation, and suddenly you control who gets what. Want multiple API keys? Check them yourself. Want to scope keys to specific sites or content trees? Go ahead. Want to let authenticated backoffice users access content through the Delivery API?
Nobody's stopping you.
First, let's replace Umbraco's implementation with our own:
From here, the MemberHasAccessToAsync method becomes your gatekeeper. Every request to the Delivery API passes through it with the content node the caller is asking for. You return AccessAccepted or AccessDenied. That's the contract.
(You've also got NotLoggedIn, NotApproved and LockedOut — save those for member-security)
Let's walk through what my implementation does, layer by layer.

Layer 1: The Public Slice
The simplest case. What if you want most content locked down, but a few specific pages publicly available? Maybe you're exposing a subset of your content to a partner, or — and this is the part you should try before I explain it — maybe you want readers of your blog to be able to call the Delivery API and get back the very page they're reading.
Still here? Good.
The first button fetched this page through the Content Delivery API — no API key, no authentication. The second one authenticated against the Management API using credentials I'm about to show you, then fetched a specific content node through the Delivery API using the resulting bearer token.
Yes, those are real credentials, yes you can find them in your devtools. We'll get to that.
The public access works because of this:
I'm checking for a magic string inside the seoKeywords property. This is not what I'd call a best practice. It's what I'd call "I didn't want to add a composition to my document types for a blog experiment."
A more reasonable implementation would be a boolean property on the document type — allowPublicDeliveryAccess or similar; or a composition if you want it across multiple types.
But the point stands: MemberHasAccessToAsync receives the content node. You can check anything on it. A property, a content type alias, its position in the tree, its parent's favourite colour. Whatever your access model needs.

Layer 2: Multiple API Keys
This is the practical one. The one I'm running in production.
In the version on this site, I'm still validating against a single key from config. For the production implementation, the pattern extends naturally: pull your keys from configuration (or a database, or a key vault), check the provided key against the list, and optionally scope each key to specific content or domains.
The important thing is that you own the validation. You can give a client their own key, let them integrate against the Delivery API, and revoke that key without touching a single other integration. You can issue temporary keys. You can log which key accessed what. None of this is possible with Umbraco's built-in single-key model.
Layer 3: Backoffice Users on the Delivery API
This is where it gets interesting. Or irresponsible. Depending on your perspective.
Umbraco already has the concept of API users for the Management API. You create them in the backoffice, assign them a client ID and secret, and authenticate via OAuth client credentials. The permissions system lets you scope access down to individual content nodes.
My CustomRequestMemberAccessService checks if the caller is an authenticated backoffice user and, if so, validates their permissions against the requested content:
This means you can create an API user in the backoffice and give its access scope read permissions to exactly the content nodes you want, and use the resulting bearer token to call the Delivery API. The fine-grained permission system Umbraco already built for the Management API now works for content delivery too.
The second button you pressed earlier? That's exactly what it did. It authenticated as umbraco-back-office-boring with the secret something-super-duper-secret (I told you they were real), got a bearer token, and fetched content through the Delivery API.
I want to be clear: I am putting an unreasonable amount of faith in Umbraco's security here. The client ID and secret are public. Anyone reading this can authenticate against my Management API.
I'm comfortable with that because:
The API user has read access to exactly one content node
There is no write access
If you "hack" me, congratulations, you've obtained a blog post. It was already public.
If you actually hack me - I get to make a nice bug submission to Umbraco. One of the big ones.
This is a demonstration, not a recommendation. For production use, your client secrets should be in a key vault, not in a blog post. But the pattern — API users scoped to narrow read access on the Delivery API — is sound, and it's something I think Umbraco should expand on.
Possibly with a "read published content" permission.
The Caveat: Draft Content
There's one thing to be aware of. The bearer token comes from the Management API, and it works there too. Your narrowly scoped API user can't do much — it has read access to a single content node — but "read access" on the Management API means the raw content, including unpublished drafts. The Delivery API still only serves published content and possibly a preview. The exposure isn't there. It's on the Management side, where the same token that gives you clean published delivery also lets you read the draft your editor saved ten minutes ago.
For my use case, that's a non-issue. For a production scenario with sensitive draft content, it matters. The proper solution would be for Umbraco to distinguish between "read content" and "read published content" at the permission level — the granularity is already there in the system, it just needs one more entry.
That's what I think anyway.
This is the kind of thing that makes me think API users on the Delivery API isn't just a hack, it's a direction. The infrastructure is right there. Umbraco just hasn't walked it to the doorstep yet.
Or (and that is very likely) I am missing something big and ugly.
Rounding Up
The practical takeaway is simple. If you need more than one API key, or you need to scope access per site or per content tree, don't fight Umbraco's single-key model. Set PublicAccess: true, replace IRequestMemberAccessService, and implement the access control you actually need.
I'm doing this in production already. Multi-key access, per-client keys, the ability to revoke without collateral damage. It's not exotic. It works.
The less practical takeaway is that Umbraco's existing permission model is closer to solving this properly than it might appear. API users, granular permissions, OAuth flows — it's all there for the Management API. Extending it to the Delivery API is mostly a matter of deciding it should be done.
Until then, we have IRequestMemberAccessService, a composer, and — as is tradition — the willingness to replace the parts of Umbraco, to make it fit your usecase. This is one of my favorite things about Umbraco.
As always, everything powering this site is on GitHub. Including, yes, the credentials, and the most relevant part for this post, this bad boy.
Below is the full source code for the CustomRequestMemberAccessService for the lazy.
