1

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;
        }
    }
  • Does this answer your question? [What is a NullReferenceException, and how do I fix it?](https://stackoverflow.com/questions/4660142/what-is-a-nullreferenceexception-and-how-do-i-fix-it) – jazb Oct 18 '22 at 06:33
  • Can you share appsettings.json? – Okan Karadag Oct 18 '22 at 06:51
  • "AzureAD": { "auth_url": "https://login.microsoftonline.com//oauth2/v2.0/authorize", "token_uri": "https://login.microsoftonline.com//oauth2/v2.0/token", "client_id": "", "client_secret": "", "redirect_uris": [ "https://localhost:44328/umbraco-signin-microsoft" ], "javascript_origins": [ "https://localhost:44328" ], "callback_path": "/umbraco-signin-microsoft" } – Sanele Ngidi Oct 18 '22 at 07:32

1 Answers1

0

I've hit the same error in Umbraco 10.3.2. I've also based my code around the auto-linking and Azure AD auth documentations from Umbraco just like you've had done.

There were two issues in my code implementation:

  1. My AzureAdUsersExternalLoginProviderOptions class was checking for the wrong scheme name
  2. My Azure AD Account is an ExternalAzureAD identity type, which causes an invalid username error in the auto-link.

1. Fixing the scheme issue

The example code shows a connection to an Open ID Connect Service.

public const string SchemeName = "OpenIdConnect";
public void Configure(string name, BackOfficeExternalLoginProviderOptions options)
{
    if (name != "Umbraco." + SchemeName)
    {
        return;
    }

    Configure(options);
}

This is easily fixed by using the Microsoft AuthenticationScheme constant.

using Microsoft.AspNetCore.Authentication.MicrosoftAccount;

public void Configure(string name, BackOfficeExternalLoginProviderOptions options)
{
    if (name != "Umbraco." + MicrosoftAccountDefaults.AuthenticationScheme)
    {
        return;
    }

    this.Configure(options);
}

This seems to have been the main issue for your problem, as I can see you've found the solution here:

The above however did not fix the Value cannot be null. (Parameter 'culture') error for me.

2. Fixing the Invalid Username error

This is the second issue that caused the error for me.

The Microsoft Account I use to login to the Azure AD Tenant foobar.onmicrosoft.com is an External Azure AD identity type user from the contoso.onmicrosoft.com Azure AD Tenant.

  • The User properties look like this: User Details of external AD user in Azure AD Tenant
  • With a valid e-mail address inside the otherMails property: otherMails property contents

In my own code I used the following mapping for setting the BackOffice User's e-mail address:

options.ClaimActions.MapCustomJson(ClaimTypes.Email, user => user.GetString("mail");
  • This will not work since the mail property is empty on the fetched Azure AD User.
  • Using userPrincipleName as fallback will also not work since it contains illegal characters in anh-duc_contoso.com#EXT#@foobar.onmicrosoft.com.

The only property we can fetch a valid e-mail address from is the otherMails property. We can achieve the mapping with this:

options.UserInformationEndpoint = "https://graph.microsoft.com/v1.0/me?$select=otherMails,displayName,givenName,surname,id";
options.ClaimActions.MapCustomJson(ClaimTypes.Email, x =>
{
    return x.GetProperty("otherMails").EnumerateArray().First().ToString();
});

All credits for this solution to HalldorLyngmo who excellently explains the issue and workaround/solution here:

Anh-Duc
  • 154
  • 1
  • 12