How to setup Entra ID (formerly Azure AD) for CMS site

  • Updated

<1> Create Entra ID in the Azure portal

Following these steps to create Entra ID in the Azure portal

Step 1: Create a new Entra

Note: By default, when using your company/team’s Azure subscription, you’re not allowed to access Entra ID (by opening the left sidebar > Select “Azure Active Directory) So, you must create a new Entra ID by the step below:

Picture2.png

  1. Go to Azure portal homepage (https://portal.azure.com/#home)
  2. Click on “Create a resource” and search “Azure Active Directory” & select “Azure Acitve Directory” in the search results, then click “Create”
    Picture3.png
  3. In “Create a tenant” page
    • [Basic tab] Select a tenant type “Microsoft Entra ID”
    • [Configuration tab]
      • Orginazation name: EX: OptimizelyAS
      • Initial domain name: EX: OptimizelyAS
      • Country/Region: (optional) keep it as default: United States
    • [Review + create] Re-check all information
    • Then, click “Create” to create new AAD

 

Step 2: Create a new user

  1. Open left sidebar > select “Microsoft Entra ID” > click “Users” menu item under “All User” section in left sidebar > click “New user” > then, fill in user info > click “Create” Note: Password can auto-generated as you have to change it after the first login.

Picture5.png

Step 3: Register a web application

Open AAD from the left sidebar > Select “App registrations” > New registration > Fill in the app’s info as follows:

  • Name: OptimizelyASapp
  • Supported account types: “Accounts in this organizational directory only (OptimizelyAS only – Single tenant)
  • Redirect URI (optional): leave it for now (can be changed later)
  • Then, click “Register”

Picture6.png

 

Step 4: In “App registrations” Select your App then “Authentication” from the left sidebar

Click “Add a platform” > Select “Web” > Add the following info:

Redirect URIs:

  • https://localhost:5000/
  • https://localhost:5000/EPiServer/
  • https://localhost:5000/signin-oidc
  • https://localhost:5000/EPiServer/EPiServer.CMS.UI.Admin/default#/

Note: If you run a CMS site on non-local env, just replace localhost:8000 by your server’s name, for example: https://10.120.18.249:8080/

Front-channel logout URL: https://localhost:5000/util/Logout

Select “ID tokens”
(As I know, the access token no longer has the roles inside it so please choose this only)

Then, click “Save”

Picture7.png

Picture8.png

 

Step 5: Select “Certificates & secrets” from the left sidebar.

Go to Client secrets tab > Add “New client secret” > Enter a “Description” > Click “Add”

Picture9.png

Note: make sure to copy the Secret ID because it will be used in the config step

 

Step 6: Select “Token configuration” from the left sidebar.

Add a “groups claim” as below:

Picture10.png

 

Step 7: Select “API permission” from left sidebar

Add permission for AAD. After that, make sure the green tick showing

Picture11.png

 

Step 8: Select “Expose an API” from the left sidebar

  1. [Application ID URI] with the format: https://{domain}/{registered_app_name}
    For example: https://OptimizelyAS.onmicrosoft.com/OptimizelyAS
    Tip: hover mouse over the username in top-right corner of the Azure portal, you can see the domain
  2. [Add a scope]

Picture12.png

 

Step 9: Select “Manifest” from left sidebar

In the editor window, replace “appRoles”:[] by following the settings including some default CMS roles (WebAdmins, WebEditors, Administrators)

Note: DO NOT need to change “id” values provided because they are unique in each registered app

"appRoles": [
{
"allowedMemberTypes": [ "User" ],
"description": "Editor can edit the site.",
"displayName": "WebEditors",
"id": "7dacc4ee-fbcb-4ef4-a29e-6272d48ab0be",
"isEnabled": true,
"value": "WebEditors"
},
{
"allowedMemberTypes": [ "User" ],
"description": "Admins can manage roles and perform all task actions.",
"displayName": "WebAdmins",
"id": "c54e99d2-9467-48a9-a9c1-b6fcba74b33a",
"isEnabled": true,
"value": "WebAdmins"
},
{
"allowedMemberTypes": [ "User" ],
"description": "Admin the site.",
"displayName": "Administrators",
"id": "a7b2f640-91c7-4c58-ad29-0cae734d76f5",
"isEnabled": true,
"value": "Administrators"
}
]

 

Step 10: Select the “Managed application in local directory” link from the Overview in the left panel.

Picture13.png

Then, select “Users and groups” from left sidebar > Add user/group

In the Add Assignment page, select

  1. User: select the user created from step #2 or any available user
  2. Role: select a role that is defined in the Manifest setting at step #9 (this role should match with their own role as it will be used to authenticate and authorize)

Picture14.png

 

<2> Integrated AAD into CMS site

I set up integrated AAD into a CMS site on my local machine. Follow the link below to install an Alloy Mvc Sample site:

https://docs.developers.optimizely.com/content-management-system/docs/installing-optimizely-net-5

If you deploy the site to another env (non-local) you need to update all URLs correctly.

For example, when deploying site Linux, instead of using https://localhost:5000/, you should use https://10.120.18.247:8080/

Step 1: Update config in appsettings.json file, you need this AAD info (go to App registrations > Select your registered app)

  • Application (client) ID
  • Directory (tenant) ID

Then, update appsettings.json file like the sample below:

Picture15.png

 

Step 2: Install the OpenIdConnect package

Note: You can add the package directly to “.csproj” file to install it like this
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="6.0.23" />

 

Step 3: Update Startup.cs file

Update in the ‘ConfigureServices’ method:

//Comment out AddCmsAspNetIdentity adn AddAdminUserRegistration to Authenticate using AAD
services
               //.AddCmsAspNetIdentity<ApplicationUser>()
               .AddCms()
               .AddAlloy()
               //.AddAdminUserRegistration()
               .AddEmbeddedLocalization<Startup>();

  //add below code to define login link
           services.ConfigureApplicationCookie(c =>
           {
               c.LoginPath = "/Login";
               c.LogoutPath = "/Logout";
           });
           Microsoft.IdentityModel.Logging.IdentityModelEventSource.ShowPII = true;

 // AAD Auth info
           var clientId = _configuration["Authentication:AzureClientID"];
           var clientSecret = "YOUR CLIENT SECRET";
           var callbackPath = "/signin-oidc";
           var azureAuthority = _configuration["Authentication:azureAuthority"];
           var cookieSchema = "azure-cookie";
           var challengeSchema = "azure";

// Authentication Config
services
             .AddAuthentication(options =>
             {
                 options.DefaultAuthenticateScheme = cookieSchema;
                 options.DefaultChallengeScheme = challengeSchema;
             })            

              .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);
                 };
             });

             

Update in the ‘Configure’ method

app.UseEndpoints(endpoints =>
           {
               endpoints.MapContent();
               endpoints.MapControllerRoute("AspNetCore", "/Login", new { controller = "DefaultPage", action = "Login" });
               endpoints.MapControllerRoute("AspNetCore", "/Logout", new { controller = "DefaultPage", action = "Logout" });
           });

 

Step 4: Update “Logout” and “Login” methods in PageControllerBase.cs inside the Controllers folder:

public async Task<IActionResult> Logout()
       {
           //comment out old logout code
           //await UISignInManager.Service.SignOutAsync();
           //return Redirect(HttpContext.RequestServices.GetService<UrlResolver>().GetUrl(PageContext.ContentLink, PageContext.LanguageID));
           await ControllerContext.HttpContext.SignOutAsync("azure-cookie");
           HttpContext.Response.Cookies.Delete($".AspNetCore.{"azure-cookie"}");
           return Redirect(HttpContext.RequestServices.GetService<UrlResolver>().GetUrl(PageContext.ContentLink, PageContext.LanguageID) ?? "/");
       }

        public IActionResult Login()
       {
           return Challenge(new AuthenticationProperties { RedirectUri = "/" }, "azure");
       }

 

Step 5: Login CMS site with an AAD account

Launch the site with URLs: https://localhost:5000, then click the “Login” link in the footer of the home page. It will be redirected to the login page with AAD

Log in as a created user created before on the Azure Portal

The user will be synced to CMS as below:

Picture16.png