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 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".
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:
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.
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:
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.
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:
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:
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.
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