Hi guys,
Do we support Entra ID login for front-end users?
Thank you,
Adrian
                                            
                                        
                                                    
                            
                                                    
                        							
                                                    
                                                    
                            
                    Hi guys,
Do we support Entra ID login for front-end users?
Thank you,
Adrian
                                                            
                            
                        
															
								
								
															
															
															
								
								
								
						Yes, read more here:
Currently you have to use the template tag based (original) login app. We are currently releasing 7 new user related modules using viewmodels, one of them authentication, and that will also support external logins from next release.
BR Nicolai
                                                            
                            
                        
															
								
								
															
															
															
								
						Hi Nicolai,
It looks interesting. Do you have a timeline for the release?
Upon login With Entra ID, will I be able to read some info from the AD? Like CustomerNumber?
Thank you,
Adrian
                                                            
                            
                        
															
								
								
															
															
															
								
								
								
						Hi Adrian
We release last Tuesday of every month - read more here: https://doc.dynamicweb.dev/documentation/fundamentals/dw10release/releasepolicy.html
So for viewmodels the feature has an ETA next Tuesday.
The current implementation only reads email and name from the claim we get back from the authentication.
You can modify that by creating your own version of the Entra provider.
To get additional information back to DW there are basically 2 options:
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 login provider
/// </summary>
[AddInName("Microsoft Entra"), AddInDescription("Microsoft Entra login provider.")]
public sealed class MicrosoftEntraLoginProvider : BaseOAuthLoginProvider, IUpdateOpenIdConnectOptions
{
    /// <inheritdoc />
    [AddInParameterGroup(CredentialsGroupName)]
    [AddInLabel("Provider scheme"), AddInParameter("ProviderScheme"), AddInParameterEditor(typeof(TextParameterEditor), "required;info=Note, the redirect Uri parameter of identity provider must be set as /signin-{provider-scheme}, e.g. /signin-msentra")]
    public override string ProviderScheme { get; set; } = "msentra";
    private const string CredentialsGroupName = "External credentials";
    [AddInParameterGroup(CredentialsGroupName)]
    [AddInLabel("Client id"), AddInParameter("ClientId"), AddInParameterEditor(typeof(TextParameterEditor), "required")]
    public string? ClientId { get; set; }
    [AddInParameterGroup(CredentialsGroupName)]
    [AddInLabel("Client secret"), AddInParameter("ClientSecret"), AddInParameterEditor(typeof(TextParameterEditor), "")]
    public string? ClientSecret { get; set; }
    [AddInParameterGroup(CredentialsGroupName)]
    [AddInLabel("Tenant"), AddInParameter("Tenant"), AddInParameterEditor(typeof(TextParameterEditor), "required")]
    public string? Tenant { get; set; } = "contoso.onmicrosoft.com";
    /// <inheritdoc />
    [AddInParameterGroup("Frontend")]
    [AddInLabel("Authentication error page"), AddInParameter("ErrorPage"), AddInParameterEditor(typeof(PageSelectEditor), "")]
    public override string? ErrorPage { get; set; }
    void IUpdateOpenIdConnectOptions.SetAuthenticationOptions(OpenIdConnectOptions options)
    {
        ArgumentNullException.ThrowIfNull(options);
        if (!string.IsNullOrEmpty(Tenant))
        {
            options.Authority = $"https://login.microsoftonline.com/{Tenant}/v2.0";
        }
        options.SignInScheme = SignInManager.ExternalAuthenticationScheme;
        options.ClientId = ClientId;
        options.ClientSecret = ClientSecret;
        options.CallbackPath = CallbackPath;
        options.ResponseType = OpenIdConnectResponseType.IdToken;
        options.Scope.Add("email");
        options.Events.OnRemoteFailure = OnRemoteFailure;
        options.Events.OnTokenValidated = ctx =>
        {
            if (ctx.Principal?.Identity is ClaimsIdentity identity)
            {
                var name = identity.Claims.SingleOrDefault(x => x.Type == "name");
                if (name is not null)
                    identity.AddClaim(new Claim(ClaimTypes.Name, name.Value));
            }
            return Task.CompletedTask;
        };
    }
}
Here’s the fully rewritten SetAuthenticationOptions that:
Switches you into the hybrid flow (CodeIdToken) so you get both an ID token and an authorization code (and thus an Access Token)
Saves those tokens on the authentication ticket (SaveTokens = true)
Asks for the Graph scope you need (e.g. User.Read)
Makes OnTokenValidated async so you can call Graph and still do your existing “name” mapping
void IUpdateOpenIdConnectOptions.SetAuthenticationOptions(OpenIdConnectOptions options)
{
    if (options is null) throw new ArgumentNullException(nameof(options));
    // authority
    if (!string.IsNullOrEmpty(Tenant))
    {
        options.Authority = $"https://login.microsoftonline.com/{Tenant}/v2.0";
    }
    options.SignInScheme    = SignInManager.ExternalAuthenticationScheme;
    options.ClientId        = ClientId;
    options.ClientSecret    = ClientSecret;
    options.CallbackPath    = CallbackPath;
    // HYBRID FLOW: ask for an auth code + ID token
    options.ResponseType    = OpenIdConnectResponseType.CodeIdToken;
    options.SaveTokens      = true;
    // standard scopes + Graph
    options.Scope.Clear();
    options.Scope.Add("openid");
    options.Scope.Add("profile");
    options.Scope.Add("email");
    options.Scope.Add("User.Read");    // Graph permission to read the user’s profile
    options.Events = new OpenIdConnectEvents
    {
        OnRemoteFailure = OnRemoteFailure,
        OnTokenValidated = async ctx =>
        {
            var identity = ctx.Principal?.Identity as ClaimsIdentity;
            if (identity is null)
                return;
            // 1) Preserve your existing “name” → ClaimTypes.Name mapping
            var name = identity.FindFirst("name")?.Value;
            if (!string.IsNullOrEmpty(name))
            {
                identity.AddClaim(new Claim(ClaimTypes.Name, name));
            }
            // 2) Grab the Access Token that the middleware saved for us
            var accessToken = ctx.TokenEndpointResponse?.AccessToken;
            if (!string.IsNullOrEmpty(accessToken))
            {
                // Build a Graph client that uses that token
                var graphClient = new GraphServiceClient(
                    new DelegateAuthenticationProvider(req =>
                    {
                        req.Headers.Authorization =
                            new AuthenticationHeaderValue("Bearer", accessToken);
                        return Task.CompletedTask;
                    }));
                // Pull just the custom attribute from Graph
                var me = await graphClient.Me
                    .Request()
                    .Select("extension_customerNumber")
                    .GetAsync();
                if (me.AdditionalData.TryGetValue("extension_customerNumber", out var cn))
                {
                    identity.AddClaim(new Claim("customerNumber", cn.ToString()!));
                }
            }
        }
    };
}
CodeIdTokenYou now get an authorization code in addition to the ID token.
This code is automatically redeemed by the OpenID middleware into an access_token (and refresh_token) when you set SaveTokens = true.
options.SaveTokens = true makes ctx.TokenEndpointResponse.AccessToken non-null in your events. Without this, you’d still only have the ID token.
You must request the Graph permission (User.Read) so the middleware can fetch it for you—otherwise Graph will reject your call.
OnTokenValidated becomes async, because now you’re awaiting a Graph call.
In short, switching from IdToken → CodeIdToken plus SaveTokens is exactly the change that lets you reach out to Graph in your validation event and pull in extra claims like your customer number.
You must be logged in to post in the forum