Umbraco-CMS: Azure AD integration in backoffice throws a ArgumentNullException
Which Umbraco version are you using? (Please write the exact version, example: 10.1.0)
10.2.1
Bug summary
I have added Azure AD B2C login in an Umbraco 10.2.1 installation, I followed this documentation here: https://our.umbraco.com/Documentation/Reference/Security/Authenticate-with-Active-Directory/ Only difference is I have also added options.AuthorizationEndPoint and options.TokenEndPoint. Otherwise its the same, I have provided the code snippet further below. The reason for adding AutorizationEndpoint and TokenEndPoint is because the application is setup in our Azure AD as single tenant. The documentation is for multi tenant setups (which IMO is not very good practice for the backoffice in some cases) where these endpoints are not needed. The documentation should be expanded with this.
Either way, the issue is the authorisation process works, but once you get to the backoffice its just a blank screen. So I checked the umbracotracelog to see if I could find anything, and one exception appeared every time I used azure ad login, and not a local user:
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_method102(Closure , Object )
at Microsoft.AspNetCore.Mvc.Infrastructure.ActionMethodExecutor.AwaitableObjectResultExecutor.Execute(IActionResultTypeMapper mapper, ObjectMethodExecutor executor, Object controller, Object[] arguments)
at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeActionMethodAsync>g__Awaited|12_0(ControllerActionInvoker invoker, ValueTask`1 actionResultValueTask)
at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeNextActionFilterAsync>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.<InvokeNextResourceFilter>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.<InvokeAsync>g__Awaited|17_0(ResourceInvoker invoker, Task task, IDisposable scope)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Awaited|17_0(ResourceInvoker invoker, Task task, IDisposable scope)
at Microsoft.AspNetCore.Routing.EndpointMiddleware.<Invoke>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.<<UseMiddlewareInterface>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.<<UseMiddlewareInterface>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 119
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.<<UseMiddlewareInterface>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.<<UseMiddlewareInterface>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.<<UseMiddlewareInterface>b__1>d.MoveNext()
--- End of stack trace from previous location ---
at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context)
I thought perhaps a claim was missing, but there is no such claim from azure ad with the name “culture”.
I also have an umbraco 8.17 installation with azure ad backoffice integration and that works just fine with the exact same settings in azure.
Specifics
Code snippet:
namespace xxx.Docs.Web.Extensions
{
public static class BackofficeAuthenticationExtensions
{
public static IUmbracoBuilder ConfigureAuthentication(this IUmbracoBuilder builder)
{
var configuration = builder.Config;
var microsoftTenantId = configuration["AzureAd:TenantId"];
var microsoftClientId = configuration["AzureAd:ClientId"];
var microsoftClientSecret = configuration["AzureAd:ClientSecret"];
builder.AddBackOfficeExternalLogins(logins =>
{
const string schema = MicrosoftAccountDefaults.AuthenticationScheme;
logins.AddBackOfficeLogin(
backOfficeAuthenticationBuilder =>
{
backOfficeAuthenticationBuilder.AddMicrosoftAccount(
// the scheme must be set with this method to work for the back office
backOfficeAuthenticationBuilder.SchemeForBackOffice(schema) ?? string.Empty,
options =>
{
options.AuthorizationEndpoint = $"https://login.microsoftonline.com/{microsoftTenantId}/oauth2/v2.0/authorize";
options.TokenEndpoint = $"https://login.microsoftonline.com/{microsoftTenantId}/oauth2/v2.0/token";
//By default this is '/signin-microsoft' but it needs to be changed to this
options.CallbackPath = "/umbraco-signin-microsoft/";
//Obtained from the AZURE AD B2C WEB APP
options.ClientId = microsoftClientId;
//Obtained from the AZURE AD B2C WEB APP
options.ClientSecret = microsoftClientSecret;
});
});
});
return builder;
}
}
}
Steps to reproduce
- Add an app registration in Azure AD for the website, setup as single tenant.
- Create a fresh install of Umbraco 10.2.1.
- Add azure AD login functionality by following this documentation: https://our.umbraco.com/Documentation/Reference/Security/Authenticate-with-Active-Directory/
- Add
options.AuthorizationEndPointandoptions.TokenEndPointalong with the other options in the documentation above, with the relevant endpoints found in azure AD for that app registration. - Start the application, go to the backoffice and login with the microsoft account option.
Expected result / actual result
Expected result was that the authentication was successful, the backoffice was available for use, and that my microsoft account user is visible in the user section of the backoffice.
Actual result was a blank white screen, and when logging in with a local user, the microsoft account user is NOT in the list of backoffice users.
About this issue
- Original URL
- State: closed
- Created 2 years ago
- Comments: 16 (2 by maintainers)
Hi guys, here are some results from a debug session with @bergmania on this issue.
Using the code example from the original post we reproduced the issue on Umbraco v10.2.1. We saw it both for Single and Multi-Tenant setups when using
AddMicrosoftAccount. Following the sign-in flow we could see that it fails here in theBackOfficeSignInManagerBy default, the
AutoLinkExternalAccountvalue is set to false and since there are noAutoLinkOptionsspecified this returns a failed sign-in. It’s a bit surprising that we don’t see an error on the frontend instead of the blank screen.The
BackOfficeControllerwhich receives the failed sign-in creates this error message which never seems to reach the user.Comparing the code from v10.2.1 to v9.5.4 I could not spot any changes surrounding these actions so I’m not sure how this same setup could work in v9…
Solution/Workaround
Adding a
BackOfficeExternalLoginProviderOptionsand enablingAutoLinkExternalAccountwe were able to progress further into the sign-in flow but we did hit an issue with the claims coming back from Azure. After the sign-in the backoffice was still blank and looking at the database the user was not created. Turns out that if the user does not have an email value the fallback is the “User Principal Name”.For our user who did not have an email, the “User Principal Name” looked something like this
user_gmail.com#EXT#@usergmail.onmicrosoft.com.The hashtags turned out to be an issue and therefore Umbraco failed to create the user. Not all users will have this version of the “User Principal Name” but we found posts from other people hitting the same situation f.ex. - https://stackoverflow.com/questions/62457365/ms-graph-api-does-not-return-users-email-address-from-azure-ad-b2cAdding in an email value for the user resulted in a successful login and creation of the user. One interesting thing was that the user did have an email in another field but that was not sent with the claims.
That property “Alternate email” was possible to get by using the
UserInformationEndpointlike in the code example below.Code
Besides, adding the following snippet does not work for me, it says the collection is empty:
It’s really strange, I will have to dig a bit deeper into the settings in Azure it looks like. Its just really odd, it worked totally fine in Umbraco 9, but not from v10 and onwards with zero changes to the code.
EDIT It seems that adding the
AzureB2CBackofficeExternalLoginProviderOptions@HalldorLyngmo example and outcommenting the “otherEmails” action, it seems to be working. Really strange. I will need to do some further testing but so far so good.I would argue to make a note in the documentation, because with the documentation as it is right now, it wont work.
Thank you for the workaround solution @HalldorLyngmo! I’ve been looking all morning for why I was getting the following error upon logging in with an unlinked Azure AD account.
I am in version 10.3.2 and was seeing the same errors:
The problem was indeed as you described the user principle name
foo.bar@contoso.com#EXT#@current-tenant.onmicrosoft.combreaking the auto-link.Your code-bit for fetching and using the
otherMailsproperty was exactly what fixed my problem.I’ve confirmed that this is a new issue in Umbraco 10.
There is some discussion on the forum.
EDIT: I realize you are using
AddMicrosoftAccountand notAddOpenIdConnectwhich I resorted to using. I’m not sure if you have access to the particular event I mention below. If not, maybe there’s a similar one you can use.I had the same issue when integrating Azure AD to our backoffice and after digging through the source code I realized a couple of claims were required but not documented. I had (have) a TODO on adding it to the documentation but haven’t done so yet. Perhaps this is what you need to do as well?
I added a method for ensuring all claims are present in the
OpenIdConnectOptions.Events.OnTokenValidatedevent in which I ensure there’s a culture claim available:I also needed to provide a security stamp claim, which I just set to a random GUID for my purposes:
Then I set the claims with an Umbraco extension:
claimsIdentity.AddRequiredClaims(...)Hope this helps