BlogThe Entra ID Surprise

The Entra ID Surprise

EntraIDOptimizelyCMSAuth
12 April 2026

How assumptions can lead to mysterious issues.

By Edvard Munch - National Museum of Art, Architecture and Design, Public Domain, https://commons.wikimedia.org/w/index.php?curid=69541493

In a previous project, I had an opportunity to integrate headless Optimizely CMS with the Okta authentication provider. I might even say I got lucky, because my next project required the same thing, but this time with Entra ID.

I didn't expect so much trouble with that. Since both methods use the OpenID Connect protocol, the setup should be pretty much the same. And it was, except for one main difference: user groups.

In Okta, with the standard groups claim configured, the user groups are already present in the token payload received when the user gets authenticated. But in the case of Entra ID, the only thing that we get is the user group IDs. To retrieve a group's details, such as name, which is typically the only information required, an additional call to the Microsoft Graph API is necessary. Since I needed to know whether the user was a web editor or a web admin - this was a must-have. However, it didn't sound complicated, so I completed that part quite easily. Of course, the important thing is to then synchronize those user groups with the Optimizely CMS database, the same way it was done with Okta. After that, I tested everything, and it worked without any issues, so the task was completed.

    internal async Task SyncRolesAsync(ClaimsIdentity identity, string accessToken)
    {
        await AddEntraIdGroupsToRoles(identity, accessToken);
        await _userSyncService.SynchronizeAsync(identity);
    }

    private async Task AddEntraIdGroupsToRoles(ClaimsIdentity identity, string accessToken)
    {
        var userName = identity.GetMaskedName();
        
        try
        {
            var groupClaims = identity
                .FindAll("groups")
                .ToList();

            if (groupClaims.Count == 0)
            {
                return;
            }

            var groupNames = await GetAllGroupNames(accessToken, groupClaims);

            foreach (var groupName in groupNames)
            {
                identity.AddClaim(new Claim(ClaimTypes.Role, groupName));
            }

            // Remove all group ID claims
            foreach (var groupClaim in groupClaims)
            {
                identity.RemoveClaim(groupClaim);
            }
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Failed to synchronize roles for user {UserName}", userName);
        }
    }

Happiness and satisfaction from the completed task haven't lasted long, though. After deployment, the client reported an authentication issue. They described the authentication as unresponsive and failing to proceed to CMS edit mode. It was unusual. Everything functioned correctly for my test account on my test tenant. My initial thought was that something might be wrong with the client's browsers. It was difficult to believe that something was wrong with the implementation. There was no place where it could break at all. The live session with the client and additional debug information confirmed an issue, though the behavior remained unusual. The authentication steps, together with synchronizing the CMS database, were all successful. The application flow was okay. The problem occurred in the interval between process completion and the browser redirect to the destination page.

How can I fix an issue that doesn't show any error message, and the entire application flow appears to be working fine?

The only trace was that it worked for my test account in my test tenant, but not for the client tenant, though not consistently. Sometimes the whole flow worked, but usually only when their browser was in a clear state; soon after, it stopped working.

After investigation and a pinch of intuition, it turned out that the cookie was too large to be handled by the browser. It wasn't an issue in my test tenant because my test account was assigned only to two user groups. However, the client accounts were assigned to almost a hundred groups, and all of this data was stored in the cookie. When the browser encounters a case like that, it doesn't display any error or warning; instead, it hangs and loads the site infinitely.

The solution was to store only the required user groups. In this case, it was limited to groups beginning with a specific prefix, which applies exclusively to our CMS site. That decreased the size of the cookie drastically, and authentication started to work for everyone.

    private async Task<List<string>> GetSupportedGroupNames(string accessToken, List<Claim> groupIdClaims)
    {
        var allGroupNames = await GetAllGroupNames(accessToken, groupIdClaims);
        var groupPrefix = _authOptions.EntraId?.GroupPrefixToStrip ?? string.Empty;
        var supportedGroups = string.IsNullOrWhiteSpace(groupPrefix)
            ? allGroupNames
            : allGroupNames.Where(x => x.StartsWith(groupPrefix, StringComparison.OrdinalIgnoreCase)).ToList();

        var result = supportedGroups
            .Select(name => StripGroupPrefix(groupPrefix, name))
            .Where(strippedName => !string.IsNullOrWhiteSpace(strippedName))
            .ToList();

        return result;
    }

To be fair, the Microsoft documentation did warn me about this. I definitely saw notes about fragile parts of the setup. But come on, what could go wrong? I was only interested in two groups: WebEditors or WebAdmins. What I didn't think about was the client assigning users to so many other groups, which, honestly, shouldn't have been surprising, since the site we were building wasn't the only application in the organization. The documentation also warned that storing all user groups in a cookie can get problematic fast, with a Redis cache or something similar being the suggested alternative. That still seems like over-engineering for simply authenticating a user to a CMS. But taken together, those were solid warning signs that fetching user groups from Entra ID wasn't as straightforward as it looked, and I shouldn't have ignored them.

This was a really good lesson learned: never store all user groups in a cookie. I think I will remember that for a long time.

More articles

OptimizelyGraphGetaCategories

Indexing Geta Categories in Optimizely Graph

Different ways to fully use categories in headless architecture.

The lock laying between loose keyboard buttons
OptimizelyAuthOktaASP.NET

Optimizely CMS Mixed Auth - Okta + ASP.NET Identity

Configuring mixed authentication and authorization in Optimizely CMS using Okta and ASP.NET Identity.

A finger is about to press the "Cancel" button.
OptimizelyCMSScheduled Jobs

Cancelling CMS Scheduled Jobs

From flags to tokens: making Optimizely CMS scheduled jobs more elegant with .NET cancellation tokens.

Have a question?

Don't hesitate, and send me an email

damian@smutek.dev