How to setup Content Delivery API with Entra ID (formerly Azure AD) and OpenIDConnect

  • Updated

<1> Setup Entra ID for CMS

Following the steps in the docs below to config Entra ID (formerly Azure AD) with CMS
https://support.optimizely.com/hc/en-us/articles/20767067525773

 

<2> Integrated CD with Entra ID into CMS site

Step 1: Install JwtBearer and content delivery API package:

Note: You can add the package directly to “.csproj” file to install like this
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="6.0.23" />
<PackageReference Include="EPiServer.ContentDeliveryApi.Cms" Version="3.9.0" />
<PackageReference Include="EPiServer.ContentDefinitionsApi" Version="3.9.0" />
<PackageReference Include="EPiServer.ContentManagementApi" Version="3.9.0" />

Step 2: Update Startup.cs file


var clientId = "YOUR CLIENT ID";
var clientSecret = "YOUR CLIENT SECRET";
var callbackPath = "/signin-oidc";
var azureAuthority = "https://login.microsoftonline.com/" + "YOUR TENANT ID" + "/v2.0";
var cookieSchema = "azure-cookie";
var challengeSchema = "azure";
var oidcConfig = new ConfigurationManager<OpenIdConnectConfiguration>($"{azureAuthority}/.well-known/openid-configuration", new OpenIdConnectConfigurationRetriever()).GetConfigurationAsync().Result;

// Authentication Config

services.AddAuthentication(options =>
{
  options.DefaultAuthenticateScheme = cookieSchema;
  options.DefaultChallengeScheme = challengeSchema;
})
.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme,
options =>
{
  options.TokenValidationParameters = new TokenValidationParameters
  {
      ValidIssuer = azureAuthority,
      ValidateIssuer = true,
      ValidAudience = clientId,
      ValidateAudience = true,
      ValidateLifetime = true,
      IssuerSigningKeys = oidcConfig.SigningKeys,
      ValidateIssuerSigningKey = true
  };
})

.AddCookie(cookieSchema, options =>
{
  options.Events.OnSignedIn = async ctx =>
  {
      if (ctx.Principal?.Identity is ClaimsIdentity claimsIdentity)
      {
          // Syncs user and roles so they are available to the CMS
          var synchronizingUserService = ctx
              .HttpContext
              .RequestServices
              .GetRequiredService<ISynchronizingUserService>();
          await synchronizingUserService.SynchronizeAsync(claimsIdentity);
      }
  };
})

.AddOpenIdConnect(challengeSchema, options =>
{
  options.SignInScheme = cookieSchema;
  //options.SignOutScheme = "azure-cookie";
  options.ResponseType = OpenIdConnectResponseType.Code;
  options.UsePkce = true;
  options.ClientId = clientId;
  options.Authority = azureAuthority;
  options.CallbackPath = callbackPath;
  options.ClientSecret = clientSecret;
  options.Scope.Clear();
  options.Scope.Add(OpenIdConnectScope.OpenIdProfile);
  options.Scope.Add(OpenIdConnectScope.OfflineAccess);
  options.Scope.Add(OpenIdConnectScope.Email);
  options.MapInboundClaims = false;
  options.TokenValidationParameters = new TokenValidationParameters
  {
      // get the role from azure
      RoleClaimType = "roles",
      NameClaimType = "preferred_username",
      ValidateIssuer = false
  };
  options.Events.OnRedirectToIdentityProvider = ctx =>
  {
      // Prevent redirect loop
      if (ctx.Response.StatusCode == 401)
      {
          ctx.HandleResponse();
      }
      return Task.CompletedTask;
  };
  options.Events.OnAuthenticationFailed = context =>
  {
      context.HandleResponse();
      context.Response.BodyWriter.WriteAsync(Encoding.ASCII.GetBytes(context.Exception.Message));
      return Task.CompletedTask;
  };
  options.Events.OnTokenValidated = (ctx) =>
  {
      var redirectUri = new Uri(ctx.Properties.RedirectUri, UriKind.RelativeOrAbsolute);
      if (redirectUri.IsAbsoluteUri)
      {
          ctx.Properties.RedirectUri = redirectUri.PathAndQuery;
      }
      //Sync user and the roles to EPiServer in the background
      ServiceLocator.Current.GetInstance<ISynchronizingUserService>().SynchronizeAsync(ctx.Principal.Identity as ClaimsIdentity);
      return Task.FromResult(0);
  };
});

services.AddContentDeliveryApi(JwtBearerDefaults.AuthenticationScheme)
  .WithFriendlyUrl()
  .WithSiteBasedCors();

services.AddContentDefinitionsApi(JwtBearerDefaults.AuthenticationScheme, c =>
{
  c.DisableScopeValidation = true;
});

services.AddContentManagementApi(JwtBearerDefaults.AuthenticationScheme, c =>
{
  c.DisableScopeValidation = true;
});

Note: in case you want to use ScopeValidation, you first need to have the proper scope from Azure and set DisableScopeValidation = false

Then you have to match the .AddJwtBearer() with the claim that you get from the Token (id_token or access_token). and add options.MapInboundClaims = false; into it

This must be done and checked from your side so we are not suggesting if you don't want to have a complexible code for this part.

 

Step 3: Try to test the CD API

  1. Make sure you can log into the CMS using the Entra ID account from Section 1
  2. After accessing Admin mode, set up a page that is not visible to Everyone but Administrators only


  3. This page has ID = 9 so we will send a request to get this content in Postman but with no authentication first. It should return a 401 Unauthorized error as this content will not be visible for Everyone Query: https://localhost:5000/api/episerver/v3.0/content/9


  4. In this example, we will get the JWT to communicate with CD using the URL: https://login.microsoftonline.com/{TENNANTID}/oauth2/v2.0/token

    For more information: https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth-ropc#authorization-request
  5. In this case, we will use the id_token to authenticate. Decode the token we can see that has 3 roles



  6. Try to get the content again with the Bearer token and the content should return now

Thanks for reading. If you have any questions or problems, please send a request to support@optimizely.com and cc my email address anhtuan.hoang@optimizely.com