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