Developer forum

Forum » Dynamicweb 10 » Entra ID login for front-end

Entra ID login for front-end

Adrian Ursu Dynamicweb Employee
Adrian Ursu
Reply

Hi guys,

Do we support Entra ID login for front-end users?

Thank you,

Adrian


Replies

 
Nicolai Pedersen Dynamicweb Employee
Nicolai Pedersen
Reply

Yes, read more here:

https://doc.dynamicweb.dev/manual/dynamicweb10/settings/areas/users/external-login-providers.html?q=external&tabs=entra

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

 
Adrian Ursu Dynamicweb Employee
Adrian Ursu
Reply

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

 
Nicolai Pedersen Dynamicweb Employee
Nicolai Pedersen
Reply

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:

  1. One would be that you add additional information to the ID token: https://learn.microsoft.com/en-us/entra/external-id/customers/how-to-add-attributes-to-token (But we have no way to understand that - but can be made custom)
  2. The other would be to call the MS graph when the token has validated and query Entra for the information you need.
Below our standard entra provider and a custom version that will query custom fields from the Entra graph.

Standard Dynamicweb Entra provider:

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


Update the SetAuthenticationOptions of your custom Entra provider to this

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()!));
                }
            }
        }
    };
}

What changed by moving to CodeIdToken

  1. You 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.

  2. options.SaveTokens = true makes ctx.TokenEndpointResponse.AccessToken non-null in your events. Without this, you’d still only have the ID token.

  3. You must request the Graph permission (User.Read) so the middleware can fetch it for you—otherwise Graph will reject your call.

  4. OnTokenValidated becomes async, because now you’re awaiting a Graph call.

In short, switching from IdTokenCodeIdToken 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