Jeremy Davis
Jeremy Davis
Sitecore, C# and web development
Article printed from: https://blog.jermdavis.dev/posts/2024/minimal-change-entra-sitecore

What's the 'minimal change' to use Entra External ID for public login with Sitecore?

How much work would be required to allow its use?

Published 07 October 2024

I did a little proof-of-concept hacking recently around the idea of "what's the least work required to allow your existing Sitecore website public login to move to Entra". I ended up with the bones of an interesting approach, which might be of interest to others. So read on for ideas:

The scenario

This research was for a client who already had a public Sitecore XP website which had login using the Membership & Roles features that Sitecore offer via .Net Framework. The site allowed users to register and log in using these features, and also made use of Roles to control what users could do.

The client is looking at a move to a more composable solution, and we'd had a discussion about launching an upcoming product as a separate website with a more composable architecture. Entra External ID (Which I'll abbreviate to EXID because it's tedious typing all that) was in the frame for login and registration features for the future website, but the client was keen to avoid a situation where users would need two separate sets of credentials - one for the composable site and one for the legacy Sitecore site.

So I spent a bit of time digging into what could be done here, from the perspective of "what is the least change we need to make to the legacy site".

Basics of login

The login process for EXID is fairly simple:

sequenceDiagram
  participant W as Your Website
  participant E as Entra External ID

  W->>W: Generate login link to Entra
  W->>E: User clicks login link data
  E->>E: Capture login details
  E->>W: Generate & send token data
  W->>W: Decode token to get user data

					

Or in words:

  • You craft a link from your site to your EXID tenant, which sends over some data and settings for the process. This includes a return URL for when the process is done.
  • The user follows that link, and does their login process on Microsoft's servers. Whatever you've configured in terms of styling and the process to validate their identity gets done.
  • EXID packages up data about the user and the login into a token, signs it and sends it back to your website via the return URL.
  • You receive that data, decode it and carry on.

So it's not too complex to get a user confirmed by EXID and Entra understands roles and profile properties, so it's possible to configure them there, and have them passed over as "claims" with the login data.

Now Microsoft do have some library code which can help mediate this process. Nuget packages exist which allow you to wire EXID directly into .Net Framework's Membership & Roles code. So one approach to the client's need might be to adopt that instead of the basic Sitecore login code.

But thinking about that in the context of this process, it seemed like quite a lot of change for two key reasons.

Firstly when I looked at the libraries MS supply they have somewhat newer dependencies than some of Sitecore's libraries. That means adding these into a Sitecore solution would involve some business with version redirections, and a bit of poking with ILSpy suggested there might be some breaking changes here. So that seemed like it might be tricky to make work reliably.

And secondly this is a fairly fundamental change to the existing solution. Removing Sitecore's default M&R code and replacing it with this library was going to involve quite a bit of effort and some rework of existing website code too.

Having an idea

In some office conversations about this it struck me that there was a much simpler approach that could be used for this piece of work. What if the legacy site's login workflow became something more like:

  • Login directs user to EXID
  • EXID sends back its token of user data
  • The handler for that request in Sitecore extracts the username from the token
  • The handler looks up that email address in Sitecore's M&R framework
  • If the user is found (and some other validations pass) the handler sets that Sitecore user as the context user

With that flow none of the roles and security code in the legacy site needs to change. And the login / registration processes need small-ish changes to get that user token from EXID and tie it up with the data in the Sitecore M&R store.

This seemed like a much lower change approach, if it could be made to work.

Implementing it

The first step is configuring Entra.

Other than the basic "make it look pretty" and "configure an App and a Flow" as per Microsoft's docs, you need to set two things here.

You need to make sure EXID is configured to send the email address as part of the token. Under the App Registration details for your App, you can look at the Token Configuration:

The Entra admin page for an App Registration, showing adding an optional claim to a token

If Email is not part of the claims for the token, you can add it via the "Add optional claim" button. From here pick "ID" and then find the "email" claim.

And the second thing is that for the .Net site to receive the token data it needs to be sent in an HTTP POST. But the default for this setting is to use a URL hash fragment instead - to suit client-side apps. And that's not visible to server-side .Net code. The EXID URL that the user is redirected to for login can control this. There is a response_mode parameter in that URL which can be set to form_post to enable that.

Next you need a controller to receive that data. The POST will receive will have two parameters:

public class EntraController : Controller
{
    [HttpPost]
    public ActionResult Login(string id_token, string session_state)
    {
    }
}

					

The route for this controller needs mapping, which is usually done in a processor for the initialize pipeline. Since we don't want this to be a page in Sitecore, but just a normal controller. So its easiest to map any methods on that to under /api/ with the usual business for mapping a custom controller. And then you need to make sure the controller method's url is added to the IgnoreUrlPrefixes config setting too, so Sitecore doesn't try to treat it as a content page.

This controller endpoint needs to be configured as the redirect after EXID does its login. Which requires setting it in the UI:

The Entra admin UI for App Registrations showing setting the allowed redirect URIs for the login process

But it also has to be specified as the return address for the login link the user clicks. Which in this case was a simple link in some Razor:

@{
    var baseUrl = "https://entraexperiments.ciamlogin.com/11111111-1111-1111-1111-111111111111/oauth2/v2.0/authorize";
    var redirect = "https://cm.entra.localhost/api/entra/login";
    var unqiqueId = FetchIdForThisLogin()
}

<a href="@(baseUrl)?client_id=22222222-2222-2222-2222-222222222222&nonce=@(unqiqueId)&redirect_uri=@(redirect)&scope=openid&response_type=id_token&prompt=login&response_mode=form_post">Entra login to Sitecore</a>

					

(The ID guids here can be found in the config for your EXID app - I've replaced the ones I was using here)

The interesting bit in the data EXID posts back to the controller is id_token. When the controller method above is called it gets this token data provided. It's a string formed of three parts, separated with . characters. The first section is a header which describes the following data. The second is the token data itself. And the third is the signature to prove that the data is valid. Each of these bits is base64 encoded. So the method's code can split it up and decode it:

var parts = id_token.Split('.');
var header = Base64DecodeToString(parts[0]);
var payload = Base64DecodeToString(parts[1]);
var signature = parts[2];

					

It's worth noting that the decoding here needs to be aware of an important point. Base64 strings need to have specific lengths for the decoding to work, but EXID does not pad the end of the data it sends. So "normal" Base64 decoding might give errors in some circumstances here if they come back too short. So after a bit of googling I ended up with this helper to make it work by adding in the right padding:

private static string Base64DecodeToString(string ToDecode)
{
    string decodePrepped = ToDecode.Replace("-", "+").Replace("_", "/");

    switch (decodePrepped.Length % 4)
    {
        case 0:
            break;
        case 2:
            decodePrepped += "==";
            break;
        case 3:
            decodePrepped += "=";
            break;
        default:
            throw new Exception("Not a legal base64 string!");
    }

    byte[] data = Convert.FromBase64String(decodePrepped);
    return System.Text.Encoding.UTF8.GetString(data);
 }

					

Once you have the payload data, logging in is fairly easy:

JObject json = JObject.Parse(payload);
var email = json["email"].Value<string>();

var usr = System.Web.Security.Membership.GetUserNameByEmail(email);

if(usr != null)
{
    var u = System.Web.Security.Membership.GetUser(usr);
    var result = Sitecore.Security.Authentication.AuthenticationManager.Login(u.UserName);
}

					

First parse the payload json into a readable format, and extract the email address from the data. Then use the membership API to fetch a username for the email address we got, and if it's valid log that user in.

Conclusions

It works! And it's pretty simple to get a basic process working...

Now there's more that a full implementation would require here - validating that signature for a start. Making sure that the data posted did indeed come from EXID and has not been tampered with. We don't want to introduce a security issue here. The site would also need some modifications to the registration process to ensure that the Sitecore-side and Entra-side data stays in sync.

But this tiny demo was good enough to prove the concept for me. As long as the Entra user and the Sitecore user share an email, a simple login works...

↑ Back to top