I'm trying to implement Microsoft Active Directory External Auth for my Backoffice user in Umbraco version 10.2.0 following the documentations (https://our.umbraco.com/documentation/Reference/Security/Authenticate-with-Active-Directory/ and https://our.umbraco.com/documentation/Reference/Security/Auto-linking/) the implementation works when the user already exists and I did manual linking, however when when user doesn't exist the site becomes blank on redirect from authenticating and when I check console the following error occurred:
[07:29:43 ERR] An unhandled exception has occurred while executing the request. System.ArgumentNullException: Value cannot be null. (Parameter 'culture') at Umbraco.Cms.Core.Services.LocalizedTextService.GetAllStoredValues(CultureInfo culture) at Umbraco.Cms.Web.BackOffice.Controllers.BackOfficeController.LocalizedText(String culture) at lambda_method57(Closure , Object ) at Microsoft.AspNetCore.Mvc.Infrastructure.ActionMethodExecutor.AwaitableObjectResultExecutor.Execute(IActionResultTypeMapper mapper, ObjectMethodExecutor executor, Object controller, Object[] arguments) at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.g__Awaited|12_0(ControllerActionInvoker invoker, ValueTask`1 actionResultValueTask) at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.g__Awaited|10_0(ControllerActionInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted) at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Rethrow(ActionExecutedContextSealed context) at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted) at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.InvokeInnerFilterAsync() --- End of stack trace from previous location --- at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.g__Awaited|25_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted) at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Rethrow(ResourceExecutedContextSealed context) at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted) at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.InvokeFilterPipelineAsync() --- End of stack trace from previous location --- at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.g__Awaited|17_0(ResourceInvoker invoker, Task task, IDisposable scope) at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.g__Awaited|17_0(ResourceInvoker invoker, Task task, IDisposable scope) at Microsoft.AspNetCore.Routing.EndpointMiddleware.g__AwaitRequestTask|6_0(Endpoint endpoint, Task requestTask, ILogger logger) at Umbraco.Cms.Web.Common.Middleware.BasicAuthenticationMiddleware.InvokeAsync(HttpContext context, RequestDelegate next) at Microsoft.AspNetCore.Builder.UseMiddlewareExtensions.<>c__DisplayClass6_1.<b__1>d.MoveNext() --- End of stack trace from previous location --- at Umbraco.Cms.Web.BackOffice.Middleware.BackOfficeExternalLoginProviderErrorMiddleware.InvokeAsync(HttpContext context, RequestDelegate next) at Microsoft.AspNetCore.Builder.UseMiddlewareExtensions.<>c__DisplayClass6_1.<b__1>d.MoveNext() --- End of stack trace from previous location --- at Microsoft.AspNetCore.Session.SessionMiddleware.Invoke(HttpContext context) at Microsoft.AspNetCore.Session.SessionMiddleware.Invoke(HttpContext context) at Microsoft.AspNetCore.Localization.RequestLocalizationMiddleware.Invoke(HttpContext context) at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context) at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context) at SixLabors.ImageSharp.Web.Middleware.ImageSharpMiddleware.Invoke(HttpContext httpContext, Boolean retry) at StackExchange.Profiling.MiniProfilerMiddleware.Invoke(HttpContext context) in C:\projects\dotnet\src\MiniProfiler.AspNetCore\MiniProfilerMiddleware.cs:line 121 at Umbraco.Cms.Web.Common.Middleware.UmbracoRequestMiddleware.InvokeAsync(HttpContext context, RequestDelegate next) at Umbraco.Cms.Web.Common.Middleware.UmbracoRequestMiddleware.InvokeAsync(HttpContext context, RequestDelegate next) at Microsoft.AspNetCore.Builder.UseMiddlewareExtensions.<>c__DisplayClass6_1.<b__1>d.MoveNext() --- End of stack trace from previous location --- at Umbraco.Cms.Web.Common.Middleware.PreviewAuthenticationMiddleware.InvokeAsync(HttpContext context, RequestDelegate next) at Microsoft.AspNetCore.Builder.UseMiddlewareExtensions.<>c__DisplayClass6_1.<b__1>d.MoveNext() --- End of stack trace from previous location --- at Umbraco.Cms.Web.Common.Middleware.UmbracoRequestLoggingMiddleware.InvokeAsync(HttpContext context, RequestDelegate next) at Microsoft.AspNetCore.Builder.UseMiddlewareExtensions.<>c__DisplayClass6_1.<b__1>d.MoveNext() --- End of stack trace from previous location --- at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context)
My implementation is as follows:
- OpenIdConnectBackOfficeExternalLoginProviderOptions.cs
public class OpenIdConnectBackOfficeExternalLoginProviderOptions : IConfigureNamedOptions<BackOfficeExternalLoginProviderOptions>
{
public const string SchemeName = "OpenIdConnect";
/// <summary>
/// Configures an instance of <see cref="OpenIdConnectBackOfficeExternalLoginProviderOptions"/> class
/// </summary>
/// <param name="name">
/// A <see cref="string"/> representing a name of the scheme
/// </param>
/// <param name="options">
/// A <see cref="BackOfficeExternalLoginProviderOptions"/> representing the options to configure
/// </param>
public void Configure(string name, BackOfficeExternalLoginProviderOptions options)
{
if (name != "Umbraco." + SchemeName)
{
return;
}
Configure(options);
}
/// <summary>
/// Configures an instance of <see cref="OpenIdConnectBackOfficeExternalLoginProviderOptions"/> class
/// </summary>
/// <param name="options">
/// A <see cref="BackOfficeExternalLoginProviderOptions"/> representing the options to configure
/// </param>
public void Configure(BackOfficeExternalLoginProviderOptions options)
{
options.ButtonStyle = "btn-danger";
options.Icon = "fa fa-windows";
options.AutoLinkOptions = new ExternalSignInAutoLinkOptions(
autoLinkExternalAccount: true,
defaultUserGroups: new[] { Constants.Security.EditorGroupAlias },
defaultCulture: "en-US",
allowManualLinking: false
)
{
OnAutoLinking = (autoLinkUser, loginInfo) =>
{
autoLinkUser.IsApproved = true;
},
OnExternalLogin = (user, loginInfo) =>
{
return true;
}
};
options.DenyLocalLogin = false;
options.AutoRedirectLoginToExternalProvider = false;
}
- MicrosoftIdentityExtensions.cs
public static class MicrosoftIdentityExtensions
{
/// <summary>
/// Adds Microsoft Authentication configuration <see cref="AddMicrosoftIdentity" />.
/// </summary>
/// <param name="builder">
/// Umbraco builder builds initializes services for umbraco
/// </param>
/// <param name="config">
/// Config has the options to configure Google Authentication
/// </param>
public static IUmbracoBuilder AddMicrosoftIdentity(this IUmbracoBuilder builder, IConfiguration config)
{
//TODO: to follow-up on the issue logged in Umbraco for Auto-Linking not working correctly: https://our.umbraco.com/forum/using-umbraco-and-getting-started//110241-value-cannot-be-null-parameter-culture-on-ms-aad-external-auth-auto-linking
builder.Services.ConfigureOptions<OpenIdConnectBackOfficeExternalLoginProviderOptions>();
builder.AddBackOfficeExternalLogins(logins =>
{
const string schema = MicrosoftAccountDefaults.AuthenticationScheme;
logins.AddBackOfficeLogin(backOfficeAuthenticationBuilder =>
{
backOfficeAuthenticationBuilder.AddMicrosoftAccount(backOfficeAuthenticationBuilder.SchemeForBackOffice(schema),
options =>
{
options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.AuthorizationEndpoint = config.GetValue<string>("AuthProviders:AzureAD:auth_url");
options.TokenEndpoint = config.GetValue<string>("AuthProviders:AzureAD:token_uri");
options.ClientId = config.GetValue<string>("AuthProviders:AzureAD:client_id");
options.ClientSecret = config.GetValue<string>("AuthProviders:AzureAD:client_secret");
options.CallbackPath = config.GetValue<string>("AuthProviders:AzureAD:callback_path");
options.ClaimActions.MapJsonKey(ClaimTypes.Role, "role");
options.SaveTokens = true;
options.Scope.Add("openid");
options.Scope.Add("email");
options.Scope.Add("profile");
options.UsePkce = true;
});
});
});
return builder;
}
}