Base solution for your next web application
Open Closed

Cannot provision Microsoft account without Last Name #10637


User avatar
0
hra created

Prerequisites

  • AspNetZero 10

We have enabled Microsoft authentication, and have had users report that they get an error while attempting to associate their Microsoft account with our product.

We tracked it back to the fact that some of the users do not have a "Last Name" set in their Microsoft accounts.

This is the error in AspNetZero. You can clearly see that AspNetZero demands a last name, which is inherently incompatible with Microsoft accounts.

RROR 2021-10-19 03:01:25,045 [6    ] Mvc.ExceptionHandling.AbpExceptionFilter - An error occurred while updating the entries. See the inner exception for details.
Microsoft.EntityFrameworkCore.DbUpdateException: An error occurred while updating the entries. See the inner exception for details.
 ---> Microsoft.Data.SqlClient.SqlException (0x80131904): Cannot insert the value NULL into column 'Surname', table 'cloudrisk-staging.dbo.AbpUsers'; column does not allow nulls. INSERT fails.
The statement has been terminated.
   at Microsoft.Data.SqlClient.SqlCommand.<>c.<ExecuteDbDataReaderAsync>b__169_0(Task`1 result)
   at System.Threading.Tasks.ContinuationResultTaskFromResultTask`2.InnerInvoke()
   at System.Threading.Tasks.Task.<>c.<.cctor>b__277_0(Object obj)
   at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state)
--- End of stack trace from previous location ---
   at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state)
   at System.Threading.Tasks.Task.ExecuteWithThreadLocal(Task& currentTaskSlot, Thread threadPoolThread)
--- End of stack trace from previous location ---
   at Microsoft.EntityFrameworkCore.Storage.RelationalCommand.ExecuteReaderAsync(RelationalCommandParameterObject parameterObject, CancellationToken cancellationToken)
   at Microsoft.EntityFrameworkCore.Storage.RelationalCommand.ExecuteReaderAsync(RelationalCommandParameterObject parameterObject, CancellationToken cancellationToken)
   at Microsoft.EntityFrameworkCore.Update.ReaderModificationCommandBatch.ExecuteAsync(IRelationalConnection connection, CancellationToken cancellationToken)
ClientConnectionId:a272394f-be75-4a1d-ab5b-4f6838ec0bdd
Error Number:515,State:2,Class:16
ClientConnectionId before routing:e3fa7b20-81ee-4edd-9aa2-c99dfb447e78
Routing Destination:b8b26ffad02d.tr1.australiacentral1-a.worker.database.windows.net,11020
   --- End of inner exception stack trace ---
   at Microsoft.EntityFrameworkCore.Update.ReaderModificationCommandBatch.ExecuteAsync(IRelationalConnection connection, CancellationToken cancellationToken)
   at Microsoft.EntityFrameworkCore.Update.Internal.BatchExecutor.ExecuteAsync(IEnumerable`1 commandBatches, IRelationalConnection connection, CancellationToken cancellationToken)
   at Microsoft.EntityFrameworkCore.Update.Internal.BatchExecutor.ExecuteAsync(IEnumerable`1 commandBatches, IRelationalConnection connection, CancellationToken cancellationToken)
   at Microsoft.EntityFrameworkCore.Update.Internal.BatchExecutor.ExecuteAsync(IEnumerable`1 commandBatches, IRelationalConnection connection, CancellationToken cancellationToken)
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.SaveChangesAsync(IList`1 entriesToSave, CancellationToken cancellationToken)
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.SaveChangesAsync(DbContext _, Boolean acceptAllChangesOnSuccess, CancellationToken cancellationToken)
   at Microsoft.EntityFrameworkCore.SqlServer.Storage.Internal.SqlServerExecutionStrategy.ExecuteAsync[TState,TResult](TState state, Func`4 operation, Func`4 verifySucceeded, CancellationToken cancellationToken)
   at Microsoft.EntityFrameworkCore.DbContext.SaveChangesAsync(Boolean acceptAllChangesOnSuccess, CancellationToken cancellationToken)
   at Microsoft.EntityFrameworkCore.DbContext.SaveChangesAsync(Boolean acceptAllChangesOnSuccess, CancellationToken cancellationToken)
   at Abp.EntityFrameworkCore.AbpDbContext.SaveChangesAsync(CancellationToken cancellationToken)
   at Abp.Zero.EntityFrameworkCore.AbpZeroCommonDbContext`3.SaveChangesAsync(CancellationToken cancellationToken)
   at Abp.EntityFrameworkCore.Uow.EfCoreUnitOfWork.SaveChangesInDbContextAsync(DbContext dbContext)
   at Abp.EntityFrameworkCore.Uow.EfCoreUnitOfWork.SaveChangesAsync()
   at Abp.Authorization.Users.AbpUserStore`2.CreateAsync(TUser user, CancellationToken cancellationToken)
   at Abp.Domain.Uow.UnitOfWorkInterceptor.InternalInterceptAsynchronous[TResult](IInvocation invocation)
   at Microsoft.AspNetCore.Identity.UserManager`1.CreateAsync(TUser user)
   at Abp.Authorization.Users.AbpUserManager`2.CreateAsync(TUser user)
   at Abp.Domain.Uow.UnitOfWorkInterceptor.InternalInterceptAsynchronous[TResult](IInvocation invocation)
   at Microsoft.AspNetCore.Identity.UserManager`1.CreateAsync(TUser user, String password)
   at Abp.Domain.Uow.UnitOfWorkInterceptor.InternalInterceptAsynchronous[TResult](IInvocation invocation)
   at HRA.Portal.Authorization.Users.UserRegistrationManager.RegisterAsync(String name, String surname, String emailAddress, String userName, String plainPassword, Boolean isEmailConfirmed, String emailActivationLink) in /home/vsts/work/1/s/aspnet-core/src/HRA.Portal.Core/Authorization/Users/UserRegistrationManager.cs:line 87
   at HRA.Portal.Web.Controllers.TokenAuthController.RegisterExternalUserAsync(ExternalAuthUserInfo externalLoginInfo) in /home/vsts/work/1/s/aspnet-core/src/HRA.Portal.Web.Core/Controllers/TokenAuthController.cs:line 572
   at HRA.Portal.Web.Controllers.TokenAuthController.ExternalAuthenticate(ExternalAuthenticateModel model) in /home/vsts/work/1/s/aspnet-core/src/HRA.Portal.Web.Core/Controllers/TokenAuthController.cs:line 490
   at lambda_method2047(Closure , Object )
   at Microsoft.AspNetCore.Mvc.Infrastructure.ActionMethodExecutor.AwaitableObjectResultExecutor.Execute(IActionResultTypeMapper mapper, ObjectMethodExecutor executor, Object controller, Object[] arguments)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.&lt;InvokeActionMethodAsync&gt;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>g__Awaited|13_0(ControllerActionInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeNextExceptionFilterAsync>g__Awaited|25_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)

3 Answer(s)
  • User Avatar
    0
    ismcagdas created
    Support Team

    Hi @hra

    I have created an issue about this, see https://github.com/aspnetzero/aspnet-zero-core/issues/4064.

    As a workaround, you can use the source code of MicrosoftAuthProviderApi as shown below and handle surname being null.

    using System;
    using System.Collections.Generic;
    using System.Net.Http;
    using System.Net.Http.Headers;
    using System.Threading.Tasks;
    using Microsoft.AspNetCore.Authentication.MicrosoftAccount;
    using Newtonsoft.Json.Linq;
    
    namespace Abp.AspNetZeroCore.Web.Authentication.External.Microsoft
    {
        public class MicrosoftAuthProviderApi : ExternalAuthProviderApiBase
        {
            public const string Name = "Microsoft";
    
            public async override Task<ExternalAuthUserInfo> GetUserInfo(string accessCode)
            {
                /* TODO: Microsoft login could not be tested because of a problem on Angular2 application.
                 * see login.service.ts in Angular2 application.
                 * This is not a problem for MVC application since it uses server side login.
                 */
    
                using (var client = new HttpClient())
                {
                    client.DefaultRequestHeaders.UserAgent.ParseAdd("Microsoft ASP.NET Core OAuth middleware");
                    client.DefaultRequestHeaders.Accept.ParseAdd("application/json");
                    client.Timeout = TimeSpan.FromSeconds(30);
                    client.MaxResponseContentBufferSize = 1024 * 1024 * 10; // 10 MB
    
                    var request = new HttpRequestMessage(HttpMethod.Get, MicrosoftAccountDefaults.UserInformationEndpoint);
                    request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessCode);
    
                    var response = await client.SendAsync(request);
    
                    response.EnsureSuccessStatusCode();
    
                    var payload = JObject.Parse(await response.Content.ReadAsStringAsync());
    
                    var result = new ExternalAuthUserInfo
                    {
                        Name = MicrosoftAccountHelper.GetDisplayName(payload),
                        EmailAddress = MicrosoftAccountHelper.GetEmail(payload),
                        Surname = MicrosoftAccountHelper.GetSurname(payload),
                        Provider = Name,
                        ProviderKey = MicrosoftAccountHelper.GetId(payload)
                    };
    
                    FillClaimsFromJObject(result, payload);
                    return result;
                }
            }
        }
    }
    
  • User Avatar
    0
    hra created

    A supplement to @ismcagdas's workaround - he's also supplied some additional code - thanks:

    protected virtual void FillClaimsFromJObject(ExternalAuthUserInfo userInfo, JObject payload) { var dict = new Dictionary<string, string>(); foreach (var item in payload) { dict.Add(item.Key,item.Value?.ToString()); } userInfo.Claims = dict; }

    You can use the class MicrosoftAuthProviderApi in the TokenAuthController's (for angular project) ExternalAuthenticate method. Just check for (input.AuthProvider == "Microsoft") and if it is true, create a new instance of MicrosoftAuthProviderApi and get the user info from your custom class.

    If you are using the MVC version of AspNet Zero, you can do it in AccountController.

  • User Avatar
    0
    ismcagdas created
    Support Team

    Hi @hra

    Thanks, but it seems like this doesn't work as well because the setter for userInfo.Claims is private. We will try to release a patch version for related NuGet packages soon.