HomeDev GuideRecipesAPI Reference
Dev GuideAPI ReferenceUser GuideLegal TermsGitHubNuGetDev CommunityOptimizely AcademySubmit a ticketLog In
Dev Guide

Integrate Entra ID using OpenID Connect

Describes how to use OpenID Connect to integrate with Entra ID. It also describes how an Optimizely application can use the OpenID Connect to sign in users from a single/multi-tenant environment, using the ASP.NET OpenID Connect middleware.

📘

Note

If you have EPiServer.CMS.UI.AspNetIdentity installed, make sure you are not calling services.AddCmsAspNetIdentity() in startup.cs. Otherwise, the user interface may not look for synchronized users when setting access rights.

About Entra ID and OpenID

Entra ID (formerly Azure Active Directory (Azure AD)) is Microsoft's multi-tenant cloud-based directory and identity management service. Entra ID provides single sign-on (SSO) access to many cloud-based SaaS applications and includes a full suite of identity management capabilities.

OAuth is an open standard for authorization also used by Entra ID. OpenID Connect is built on top of OAuth and extends this so you can use it as an authentication protocol rather than just an authorization protocol.

For information about how the protocols work, see Authentication Scenarios for Entra ID and Secure your application by using OpenID Connect and Entra ID. For role-based access control, see Add application roles in Entra ID.

Prerequisites

Disable Role and Membership Providers

Disable the built-in Role and Membership providers in web.config.

Leave the profile system enabled because edit and admin views use this system for language settings. You can use another profile system on the website.

<authentication mode="None" />
<membership>
  <providers>
    <clear/>
  </providers>
</membership>
<roleManager enabled="false">
  <providers>
    <clear/>
  </providers>
</roleManager>

Configure Optimizely to support claims

Enable claims on virtual roles by setting the addClaims property. Also, add the provider SynchronizingRolesSecurityEntityProvider for security entities, which you use by the set access rights dialog box, content approval, and impersonating users, among other things.

Users and groups are synchronized to custom Optimizely tables in the database when a user is authenticated (see ISynchronizingUserService in the code example below). There is no background synchronization, so for a role change to take effect, the user has to login to the site again.

<episerver.framework>
  <securityEntity>
    <providers>
      <add name="SynchronizingProvider" 
           type="EPiServer.Security.SynchronizingRolesSecurityEntityProvider, EPiServer"/>
    </providers>
  </securityEntity>
  <virtualRoles addClaims="true">
     //existing virtual roles
  </virtualRoles>

You can replace virtual roles with roles defined in the manifest to delegate this control from the application to Azure. See Virtual roles.

Install NuGet packages

Open Package Manager in Visual Studio and install the following packages:

  • Install-Package Microsoft.Owin.Security.Cookies
  • Install-Package Microsoft.Owin.Security.OpenIdConnect
  • Install-Package Microsoft.Owin.Host.SystemWeb
  • Update-Package Microsoft.IdentityModel.Protocol.Extensions -Safe

📘

Note

Always use Microsoft.IdentityModel.Protocol.Extensions package version 1.0.2 or later. Previous versions contain a critical bug that might cause threads to hang.

Configure OpenID Connect

To configure the OpenID Connect, add the following code in the startup class for OWIN middleware. The SecurityTokenValidated event is used to sync the user and group membership to Optimizely. You can also use this event for custom logic (for example, adding custom data to the user profile).

public class Startup {
  // <add key="ida:AADInstance" value="https://login.microsoftonline.com/{0}" />
  private static readonly string aadInstance = ConfigurationManager.AppSettings["ida:AADInstance"];

  // <add key="ida:ClientId" value="Client ID from Entra ID application" />
  private static string clientId = ConfigurationManager.AppSettings["ida:ClientId"];

  // <add key="ida:PostLogoutRedirectUri" value="https://the logout post uri/" />
  private static readonly string postLogoutRedirectUri = ConfigurationManager.AppSettings["ida:PostLogoutRedirectUri"];

  // application id, or common for multi-tenant applications
  // <add key="ida:Authority" value="common" />
  private static readonly string authority = ConfigurationManager.AppSettings["ida:Authority"];

  private static string aadAuthority = string.Format(CultureInfo.InvariantCulture, aadInstance, authority);

  const string LogoutPath = "/logout";

  public void Configuration(IAppBuilder app) {
    app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
    app.UseCookieAuthentication(new CookieAuthenticationOptions());
    app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions {
      ClientId = clientId,
        Authority = aadAuthority,
        PostLogoutRedirectUri = postLogoutRedirectUri,
        TokenValidationParameters = new TokenValidationParameters {
          ValidateIssuer = false,
            RoleClaimType = ClaimTypes.Role
        },
        Notifications = new OpenIdConnectAuthenticationNotifications {
          AuthenticationFailed = context => {
              context.HandleResponse();
              context.Response.Write(context.Exception.Message);
              return Task.CompletedTask;
            },
            RedirectToIdentityProvider = context => {
              // Here you can change the return uri based on multisite
              HandleMultiSiteReturnUrl(context);

              // To avoid a redirect loop to the federation server send 403 
              // when user is authenticated but does not have access
              if (context.OwinContext.Response.StatusCode == 401 &&
                context.OwinContext.Authentication.User.Identity.IsAuthenticated) {
                context.OwinContext.Response.StatusCode = 403;
                context.HandleResponse();
              }
              //XHR requests cannot handle redirects to a login screen, return 401
              if (context.OwinContext.Response.StatusCode == 401 && IsXhrRequest(context.OwinContext.Request)) {
                context.HandleResponse();
              }
              return Task.CompletedTask;
            },
            SecurityTokenValidated = (ctx) => {
              var redirectUri = new Uri(ctx.AuthenticationTicket.Properties.RedirectUri,
                UriKind.RelativeOrAbsolute);
              if (redirectUri.IsAbsoluteUri) {
                ctx.AuthenticationTicket.Properties.RedirectUri = redirectUri.PathAndQuery;
              }

              //Sync user and the roles to Optimizely in the background
              ServiceLocator.Current.GetInstance < ISynchronizingUserService > ()
                .SynchronizeAsync(ctx.AuthenticationTicket.Identity);
              return Task.CompletedTask;
            }
        }
    });
    app.UseStageMarker(PipelineStage.Authenticate);
    app.Map(LogoutPath, map => {
      map.Run(context => {
        context.Authentication.SignOut();
        return Task.CompletedTask;
      });
    });
  }

  private void HandleMultiSiteReturnUrl(
    RedirectToIdentityProviderNotification < OpenIdConnectMessage, OpenIdConnectAuthenticationOptions > context) {
    // here you change the context.ProtocolMessage.RedirectUri to corresponding siteurl
    // this is a sample of how to change redirecturi in the multi-tenant environment
    if (context.ProtocolMessage.RedirectUri == null) {
      var currentUrl = HttpContext.Current.Request.Url;
      context.ProtocolMessage.RedirectUri = new UriBuilder(
        currentUrl.Scheme,
        currentUrl.Host,
        currentUrl.Port).ToString();
    }
  }

  private static bool IsXhrRequest(IOwinRequest request) {
    const string xRequestedWith = "X-Requested-With";

    var query = request.Query;
    if ((query != null) && (query[xRequestedWith] == "XMLHttpRequest")) {
      return true;
    }

    var headers = request.Headers;
    return (headers != null) && (headers[xRequestedWith] == "XMLHttpRequest");
  }
}

Add application roles in Entra ID

By default, you need to declare application roles in the active directory application, such as WebEditors and WebAdmins. The application owner (app developer) or the global administrator of the developer's directory can declare roles for an application.

  1. Go to the Active Directory node and the Applications tab in the Azure Management Portal.
  2. Click to open the application for which you want to declare application roles.
  3. Click Manage Manifest on the bottom bar and select Download Manifest.
  4. Open the manifest file using a JSON editor of your choice.
  5. Locate the appRoles setting and insert the appRole definitions in the array. The following example shows approles that declare WebAdmins and WebEditors. You can modify it according to your application roles. Note that you need to generate a Guid for each role declaration.
    "appRoles": [{
        "allowedMemberTypes": [
          "User"
        ],
        "description": "Editor can edit the site.",
        "displayName": "WebEditors",
        "id": "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
        "isEnabled": true,
        "value": "WebEditors"
      },
      {
        "allowedMemberTypes": [
          "User"
        ],
        "description": "Admins can manage roles and perform all task actions.",
        "displayName": "WebAdmins",
        "id": "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXX",
        "isEnabled": true,
        "value": "WebAdmins"
      },
      {
        "allowedMemberTypes": [
          "User"
        ],
        "description": "Admin the site.",
        "displayName": "Administrators",
        "id": "XXXXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXX",
        "isEnabled": true,
        "value": "Administrators"
      }
    ],
    
  6. After declaring the application roles, you need to upload the manifest to the Entra ID application. See Role-based access control.

Assign users and groups to application roles

When a global administrator of the customer's organization has installed your application, they (or a user accounts administrator) can assign users and groups to your application:

  1. Go to the user's tab under the application where you want to assign users and groups.
  2. Select a user and click on the Assign action on the bottom bar. Assign the desired role to the user.

Known issues

  • If you are using System.IdentityModel.Tokens.Jwt version 4.0.0 or lower, it is required to set RoleClaimType = "roles" in the TokenValidationParameters.
  • If the application throws an antiforgerytoken exception like AntiForgeryToken: A Claim of Type NameIdentifier or IdentityProvider Was Not Present on Provided ClaimsIdentity, set AntiForgeryConfig.UniqueClaimTypeIdentifier = ClaimTypes.NameIdentifier.