Posted on 22/01/2026 10:10:17
Microsoft Entra External ID (CIAM) uses a different authority model (ciamlogin.com) than both classic Entra ID (login.microsoftonline.com) and Azure AD B2C (policy-based URLs).
Because of that, it doesn’t fit cleanly into the existing built-in providers in Dynamicweb 10:
So CIAM will not work out-of-the-box with either of those providers.
We will add a dedicated Microsoft Entra External ID (CIAM) login provider to Dynamicweb 10 in the end-of-February R0 release, to keep the setup explicit and avoid hidden configuration or breaking changes.
Until that release is available, you can solve this by adding a custom external login provider. I’ve attached a ready-to-use provider implementation that:
-
Uses the ciamlogin.com authority
-
Follows Dynamicweb’s standard /signin-{scheme} callback convention
-
Integrates cleanly with the existing external authentication pipeline
You can compile it as a custom AddIn and load it into your solution until the built-in provider ships.
This keeps you unblocked now, and gives you a clean upgrade path once the official provider is included in DW10.
The code:
using System.Security.Claims;
using Dynamicweb.Extensibility.AddIns;
using Dynamicweb.Extensibility.Editors;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
namespace Dynamicweb.ExternalAuthentication;
/// <summary>
/// Microsoft Entra External ID (CIAM) login provider
/// Authority/metadata is hosted on {tenantSubdomain}.ciamlogin.com
/// </summary>
[AddInName("Entra External ID (CIAM)"), AddInDescription("Microsoft Entra External ID (CIAM) login provider.")]
public sealed class EntraExternalIdLoginProvider : BaseOAuthLoginProvider, IUpdateOpenIdConnectOptions
{
private const string CredentialsGroupName = "External credentials";
private const string TenantGroupName = "Tenant";
/// <inheritdoc />
[AddInParameterGroup(CredentialsGroupName)]
[AddInLabel("Authentication scheme"), AddInParameter("ProviderScheme")]
[AddInParameterEditor(typeof(TextParameterEditor),
"required;info=A unique scheme id for this provider instance. Redirect URI must be https://yoursite.com/signin-{scheme} (e.g. https://yoursite.com/signin-ciam).")]
public override string ProviderScheme { get; set; } = "ciam";
[AddInParameterGroup(CredentialsGroupName)]
[AddInLabel("Application (client) ID"), AddInParameter("ClientId")]
[AddInParameterEditor(typeof(TextParameterEditor),
"required;info=The Application (client) ID from the App Registration in the External ID tenant.")]
public string? ClientId { get; set; }
[AddInParameterGroup(CredentialsGroupName)]
[AddInLabel("Client secret (Value)"), AddInParameter("ClientSecret")]
[AddInParameterEditor(typeof(TextParameterEditor),
"required;info=A valid client secret value from Certificates & secrets.")]
public string? ClientSecret { get; set; }
/// <inheritdoc />
[AddInParameterGroup("Frontend")]
[AddInLabel("Authentication error page"), AddInParameter("ErrorPage")]
[AddInParameterEditor(typeof(PageSelectEditor),
"info=The page the user will be redirected to if authentication fails.")]
public override string? ErrorPage { get; set; }
[AddInParameterGroup(TenantGroupName)]
[AddInLabel("Tenant subdomain"), AddInParameter("TenantSubdomain")]
[AddInParameterEditor(typeof(TextParameterEditor),
"required;info=The CIAM tenant subdomain used in the login host. Example: contoso (for https://contoso.ciamlogin.com/...).")]
public string? TenantSubdomain { get; set; }
[AddInParameterGroup(TenantGroupName)]
[AddInLabel("Tenant ID"), AddInParameter("TenantId")]
[AddInParameterEditor(typeof(TextParameterEditor),
"required;info=The Directory (tenant) ID (GUID) of the External ID tenant. Used in the authority path: https://{tenantSubdomain}.ciamlogin.com/{tenantId}/v2.0")]
public string? TenantId { get; set; }
void IUpdateOpenIdConnectOptions.SetAuthenticationOptions(OpenIdConnectOptions options)
{
ArgumentNullException.ThrowIfNull(options);
if (string.IsNullOrWhiteSpace(TenantSubdomain))
throw new InvalidOperationException("TenantSubdomain is required.");
if (string.IsNullOrWhiteSpace(TenantId))
throw new InvalidOperationException("TenantId is required.");
if (string.IsNullOrWhiteSpace(ClientId))
throw new InvalidOperationException("ClientId is required.");
if (string.IsNullOrWhiteSpace(ClientSecret))
throw new InvalidOperationException("ClientSecret is required.");
// CIAM well-known metadata lives under:
// https://{tenantSubdomain}.ciamlogin.com/{tenantId}/v2.0/.well-known/openid-configuration
options.Authority = $"https://{TenantSubdomain}.ciamlogin.com/{TenantId}/v2.0";
options.SignInScheme = SignInManager.ExternalAuthenticationScheme;
options.ClientId = ClientId;
options.ClientSecret = ClientSecret;
options.CallbackPath = CallbackPath;
// Keep aligned with existing Entra provider behavior in DW:
options.ResponseType = OpenIdConnectResponseType.IdToken;
// Typical minimal scopes; add more if you need them.
options.Scope.Add("email");
options.Events.OnRemoteFailure = OnRemoteFailure;
options.Events.OnTokenValidated = ctx =>
{
if (ctx.Principal?.Identity is ClaimsIdentity identity)
{
// Ensure a standard Name claim exists
var name = identity.Claims.SingleOrDefault(x => x.Type == "name");
if (name is not null && !identity.HasClaim(c => c.Type == ClaimTypes.Name))
identity.AddClaim(new Claim(ClaimTypes.Name, name.Value));
}
return Task.CompletedTask;
};
}
}