Sunday 8 October 2023

Security & Permissions with headless Sitecore

Security is a big headline these days (and should always be in the forefront of anyone's mind) and when it comes to content permission and user authorization the Sitecore "monolith" has always provided these capabilities out-of-the-box; however with the giant shift to headless over the last couple of years (or more) and now the move to SaaS these have grown vastly more complicated than the simple out-of-the-box content-permission-setting capabilities we used to know and love.

The issue

So why is it that things are now more complicated?
With the move to headless and composable, we have more of a separation of concerns - in this case, separation of the authentication/authorization, the display (/render) of content, and the provision of content.  This is most evident (technically, at least) in the use of REST and GraphQL by the headless code to retrieve content from the CMS (or DXP if you want to be fancy) where renderings were previously automatically provided their content by Sitecore.

The Sitecore "monolith" provides all 3 of these capabilities: 

  • Auth* through old ASP.NET users/roles, Federated Authentication, or Sitecore Identity 
    • Authenticates and assigns roles to user for authorization
  • Display/render of content through .NET MVC controller/view renderings (let's not talk about webforms or XSLT)
    • Authorizes user by checking permissions set on content in Sitecore XM
  • Provision of content through (amongst other things) datasources provided and consumed by the renderings 

In the new headless/composable world, these would be:

  • Auth though a separate identity provider (IDP) such as Okta / Auth0 / OneLogin / MS Entra
    • Authenticates and assigns roles to user for authorization
  • Display/render of content through headless SDKs such as Next.js (using REST / GraphQL)
  • Provision of content from Sitecore XM / Content Hub / Content Hub One

Notice something missing in the second list? Hopefully you did, as it's highlighted in bold in the first list!
There is now a disconnect between the roles assigned to the user by the IDP, and any roles you create in Sitecore / the permissions set on content.

This disconnect has no doubt been encountered by anyone working in a headless environment which requires their users to log in, and I have seen a few examples of how various people have thought about / tackled a solution. The following sections outline my approach.

A solution

There are 2 gaps mentioned above, and in case you missed them these are:
  1. A disconnect between roles assigned to a user by the IDP and roles in Sitecore (ie roles assigned to content)
  2. No out-of-the-box capabilities by the headless SDK to determine whether users are authorized to view content

You might be thinking "but both the GraphQL and REST endpoints support authorisation!". This was my initial thought, and I spent quite a bit of time deep diving into whether this was a viable option (ie. calling the layout service / GraphQL using the authenticated user details from the headless code). Let me save you a lot of time and headache: short of a quick and dirty option (simple logged in / not logged in user content, for example by swapping which API key you use to call the layout service) this isn't going to be an option you can use for anything serious.

I have seen other solutions which propose mapping the permissions (ie. roles <=> content) in a separate system, however my preference was to keep this within Sitecore. Sitecore XM still offers a flexible and robust ability to assign which content should be accessible by which users/roles, and building this again / finding a separate solution just seems like extra effort for no reason. I also personally do not feel like using this existing functionality goes against the spirit of composable at all. This mindset was the foundation for the remainder of the approach below.

I'm not going to dive too deeply in to the first point in the list above - suffice it to say there will need to be a way to ensure your roles in Sitecore match the roles assigned by your IDP. There are at least a couple of options:

  1. Configure role serialization, dynamically generate a yml file containing the roles, and dotnet sitecore ser push it
  2. Create an API endpoint on your Sitecore CM which calls System.Web.Security.Roles.CreateRole()

The meat of the dev work required, as far as I'm concerned, lies in exposing the roles and consuming them as part of authorization in the headless code.

Exposing the permissions

After setting the content (item) permissions (an out-of-the-box Sitecore XM exercise), the first thing that needs to be done is exposing these permissions. Content is consumed by the headless code either via the layout service, or GraphQL, so these are the 2 scenarios that need to be covered.

GraphQL: 

The quick and dirty way is to patch out the standard field filter, and add the security fields into your schema.
Note: this is not going to work if you're using Edge (/XM Cloud).

If you want a version which is compatible with Edge / XM Cloud, you'll want to create your own "faux security" field (you might call it "permissions"), and copy the standard (__Security) field value to this custom (permissions) field in a item:saved event handler. See this stackexchange answer for an example of a similar event handler.

Layout service: 

See sample repo

The layout service filters out all the standard fields (including __Security) in the FieldFilter method of JssItemSerializer (or whichever item serializer your site is using) so we will need to patch that out and add our own filter.
We then want to serialize our security field in a more readable format, which we can do in a custom SecurityFieldSerializer.

After this is done we can see all the __Security fields in our layout service result!

Authorization in Next.js

When it comes to Next.js, authentication really comes down to next-auth which has a host of IDP plugins out of the box (or you can develop your own easily enough if you really need / want to).  Authorization, however, is a custom exercise.  Long story short: once the SDK has called the layout service, the headless code needs to parse the permissions and prevent the user from seeing the page, or hide the appropriate content on the page from the user. 

Again, there are 2 parts to this: security at a page level (not authorized to view the page) or at a component level (hide a component from the user by removing it from the layout).

These can both be accomplished largely by customising normal-mode.ts (at least in Sitecore 10.2) with a helper to parse un-formatted security fields.

Page level (lines 68-74) and component-level (lines 77-80):

In the former we simply check the page security field, and in the latter we loop through all placeholders and check the security field set on the datasource.  This does not necessarily cover 100% of your use cases (components without datasources for example) but the rest can certainly be implemented in a similar fashion.

Conclusion

Securing certain content - to be shown only for authorized users - has been a site requirement for almost as long as the web has been around.  The advent of headless and/or composable mandates a new way of thinking and new approach to securing your site content.

With the logic provided above you should be able to both cover the vast majority of your permissions cases, as well as have a solid foundation for custom use cases where permissions might be needed within each of your components.

If you've made it this far - firstly thanks for reading! - I hope you learned something new today, or maybe re-enforced something you already knew. I'd love to get your feedback either way, so please leave a comment below, whether it's in agreement or some constructive criticism!