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

  • Updated

<1> Set up Entra ID for CMS

Following the steps in the docs below to configure 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 the 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 the 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 that you don't want to have a complex 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 to 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 it 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 feedback to support@optimizely.com and cc my email address anhtuan.hoang@optimizely.com