Open Closed

Extension property in Identity Users not working #8631


User avatar
0
markusbergkvist created

Check the docs before asking a question: https://abp.io/docs/latest Check the samples to see the basic tasks: https://abp.io/docs/latest/samples The exact solution to your question may have been answered before, and please first use the search on the homepage. Provide us with the following info: 🧐 Hint: If you are using the ABP Studio, you can see all the information about your solution from the configuration window, which opens when you right-click on the solution and click on the Solution Configuration button.

  • ABP Framework version: v9.0.2
  • UI Type: Blazor Server
  • Database System: EF Core SQL Server
  • Tiered (for MVC) or Auth Server Separated (for Angular): no
  • Exception message and full stack trace: [13:30:09 ERR] Unhandled exception in circuit 'sA6sWJvu5si7aktFPlaAkqY76suTEm7JZ1k8lLRDScE'. System.Text.Json.JsonReaderException: '<' is an invalid start of a value. LineNumber: 2 | BytePositionInLine: 0. at System.Text.Json.ThrowHelper.ThrowJsonReaderException(Utf8JsonReader& json, ExceptionResource resource, Byte nextByte, ReadOnlySpan1 bytes) at System.Text.Json.Utf8JsonReader.ConsumeValue(Byte marker) at System.Text.Json.Utf8JsonReader.ReadFirstToken(Byte first) at System.Text.Json.Utf8JsonReader.ReadSingleSegment() at System.Text.Json.JsonDocument.Parse(ReadOnlySpan1 utf8JsonSpan, JsonReaderOptions readerOptions, MetadataDb& database, StackRowStack& stack) at System.Text.Json.JsonDocument.Parse(ReadOnlyMemory1 utf8Json, JsonReaderOptions readerOptions, Byte[] extraRentedArrayPoolBytes, PooledByteBufferWriter extraPooledByteBufferWriter) at System.Text.Json.JsonDocument.Parse(ReadOnlyMemory1 json, JsonDocumentOptions options) at Volo.Abp.BlazoriseUI.Components.ObjectExtending.LookupExtensionProperty2.GetLookupItemsAsync(String filter) at Volo.Abp.BlazoriseUI.Components.ObjectExtending.LookupExtensionProperty2.SearchFilterChangedAsync(String filter) at Volo.Abp.BlazoriseUI.Components.ObjectExtending.LookupExtensionProperty`2.OnAfterRenderAsync(Boolean firstRender) at Microsoft.AspNetCore.Components.RenderTree.Renderer.GetErrorHandledTask(Task taskToHandle, ComponentState owningComponentState)
  • Steps to reproduce the issue: Add property to identityuser like in the documentation:
    ObjectExtensionManager.Instance.Modules()
    .ConfigureIdentity(identity =&gt;
    {
    identity.ConfigureUser(user =>
    {
    user.AddOrUpdateProperty\&lt;int?&gt;(
    "SupplierId",
    property =>
    {
    property.UI.Lookup.Url = "/api/app/suppliers";
    property.UI.Lookup.DisplayPropertyName = "companyName";
    }
    );
    });
    });

Create MyBlazorServerLookupApiRequestService.cs as mentioned in https://abp.io/support/questions/6195/Extension-property-in-Identity-Users-not-working-in-Azure When trying to access a modal which depends on the endpoint, you will get an null exception since HttpContext is null, remove the adding of headers according to this issue: https://abp.io/support/questions/8205/Extension-property-in-Identity-Users-not-working-in-Azure-6195 You will now get the loginpage as a result, instead of the json. How should we handle this to get access to the endpoint?

MyBlazorServerLookupApiRequestService.cs:

using System;
using System.Globalization;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using Abp;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Volo.Abp.AspNetCore.Components.Web.Extensibility;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Http.Client;
using Volo.Abp.Http.Client.Authentication;
using Volo.Abp.MultiTenancy;

namespace LansenOnline.Blazor.Services;

[Dependency(ReplaceServices = true)]
public class MyBlazorServerLookupApiRequestService : ILookupApiRequestService, ITransientDependency
{
    public IHttpClientFactory HttpClientFactory { get; }
    public IRemoteServiceHttpClientAuthenticator HttpClientAuthenticator { get; }
    public IRemoteServiceConfigurationProvider RemoteServiceConfigurationProvider { get; }
    public ICurrentTenant CurrentTenant { get; }
    public IHttpContextAccessor HttpContextAccessor { get; }
    public NavigationManager NavigationManager { get; }

    public ILogger&lt;MyBlazorServerLookupApiRequestService&gt; Logger { get; }

    public MyBlazorServerLookupApiRequestService(IHttpClientFactory httpClientFactory,
        IRemoteServiceHttpClientAuthenticator httpClientAuthenticator,
        ICurrentTenant currentTenant,
        IHttpContextAccessor httpContextAccessor,
        NavigationManager navigationManager,
        IRemoteServiceConfigurationProvider remoteServiceConfigurationProvider,
        ILogger&lt;MyBlazorServerLookupApiRequestService&gt; logger)
    {
        HttpClientFactory = httpClientFactory;
        HttpClientAuthenticator = httpClientAuthenticator;
        CurrentTenant = currentTenant;
        HttpContextAccessor = httpContextAccessor;
        NavigationManager = navigationManager;
        RemoteServiceConfigurationProvider = remoteServiceConfigurationProvider;
        Logger = logger;
    }

    public async Task&lt;string&gt; SendAsync(string url)
    {
        var client = HttpClientFactory.CreateClient();
        var requestMessage = new HttpRequestMessage(HttpMethod.Get, url);
        var uri = new Uri(url, UriKind.RelativeOrAbsolute);
        if (!uri.IsAbsoluteUri)
        {
            var remoteServiceConfig = await RemoteServiceConfigurationProvider.GetConfigurationOrDefaultOrNullAsync("Default");
            if (remoteServiceConfig != null)
            {
                // Blazor tiered mode
                var baseUrl = remoteServiceConfig.BaseUrl;
                client.BaseAddress = new Uri(baseUrl);
                AddHeaders(requestMessage);
                await HttpClientAuthenticator.Authenticate(new RemoteServiceHttpClientAuthenticateContext(client, requestMessage, new RemoteServiceConfiguration(baseUrl), string.Empty));
            }
            else
            {
                // Blazor server  mode
                client.BaseAddress = new Uri(NavigationManager.BaseUri);
                // foreach (var header in HttpContextAccessor.HttpContext!.Request.Headers)
                // {
                //     requestMessage.Headers.TryAddWithoutValidation(header.Key, header.Value.ToArray());
                // }
            }
        }

        var response = await client.SendAsync(requestMessage);

        var responseString = await response.Content.ReadAsStringAsync();

        Logger.LogInformation("Response: {0}", responseString);

        return responseString;
    }

    protected virtual void AddHeaders(HttpRequestMessage requestMessage)
    {
        if (CurrentTenant.Id.HasValue)
        {
            requestMessage.Headers.Add(TenantResolverConsts.DefaultTenantKey, CurrentTenant.Id.Value.ToString());
        }

        var currentCulture = CultureInfo.CurrentUICulture.Name ?? CultureInfo.CurrentCulture.Name;
        if (!currentCulture.IsNullOrEmpty())
        {
            requestMessage.Headers.AcceptLanguage.Add(new(currentCulture));
        }
    }
}

15 Answer(s)
  • User Avatar
    0
    liangshiwei created
    Support Team Fullstack Developer

    Hi

    Could you share a test project with me? I will check it. thanks. shiwei.liang@volosoft.com

  • User Avatar
    0
    markusbergkvist created

    Hello, what do you mean by a test project?

  • User Avatar
    0
    liangshiwei created
    Support Team Fullstack Developer

    Hi,

    Create a new project to reproduce the problem.

  • User Avatar
    0
    markusbergkvist created

    We are experiencing the same issue described in tickets #6195 and #8205, but the problem occurs in a Blazor Server setup running locally, not in Azure.

    Issue Description:

    We have extended the IdentityUser entity using the documented approach with an additional property called "SupplierId". This property is configured as a lookup using the following code:

    ObjectExtensionManager.Instance.Modules()
        .ConfigureIdentity(identity =>
        {
            identity.ConfigureUser(user =>
            {
                user.AddOrUpdateProperty<int?>(
                    "SupplierId",
                    property =>
                    {
                        property.UI.Lookup.Url = "/api/app/suppliers";
                        property.UI.Lookup.DisplayPropertyName = "companyName";
                    }
                );
            });
        });
    

    The Problem:

    When attempting to open a modal that depends on the "SupplierId" lookup, we encounter the following error:

    System.Text.Json.JsonReaderException: '<' is an invalid start of a value.
    

    Upon further investigation, we determined that this error is due to the response not being JSON, but instead an HTML login page (401 Unauthorized) caused by missing authentication headers during the API call.

    Why It's Happening:

    • Blazor Server SignalR Behavior: In Blazor Server, the initial HTTP context (HttpContext) is only available during the initial page load and becomes null during subsequent SignalR WebSocket communications.
    • Token Missing: The HttpContextAccessor is null when the API call is made, causing the token and authentication headers to be excluded from the request.
    • Unauthorized Request: Because no token is passed, the /api/app/suppliers endpoint returns a 401 Unauthorized, leading to the parsing error.

    Steps We've Taken:

    1. Implemented a custom MyBlazorServerLookupApiRequestService to try manually adding headers.
    2. Attempted to access headers via HttpContextAccessor but it was null in Blazor Server.
    3. Removed the header propagation and the error changed to the login page being returned instead of the JSON data.

    Request:

    Could you clarify how to properly handle token propagation and ensure the IdentityUser extension properties work correctly in a Blazor Server environment? Specifically:

    • How can we ensure authentication headers are included with requests made from a Blazor Server component using the lookup API?
    • Is there a preferred way to handle this without relying on HttpContextAccessor?

    This issue seems to be directly related to how SignalR handles persistent connections in Blazor Server, and a clear resolution would be beneficial for others facing similar challenges.

    Thank you for your assistance!


  • User Avatar
    0
    liangshiwei created
    Support Team Fullstack Developer

    Hi,

    Is there a preferred way to handle this without relying on HttpContextAccessor?

    It's best practice to get the current user token from the current http context

    I can't reproduce the problem.

    My steps:

    • Create a new project abp new Testapp -u blazor-server
    • Add extra property
    ObjectExtensionManager.Instance.Modules()
        .ConfigureIdentity(identity =>
        {
            identity.ConfigureUser(user =>
            {
                user.AddOrUpdateProperty<int?>(
                    "SupplierId",
                    property =>
                    {
                        property.UI.Lookup.Url = "/api/app/suppliers";
                        property.UI.Lookup.DisplayPropertyName = "companyName";
                    }
                );
            });
        });
    
    • Add API endpoint
    [Authorize]
    [Route("api/app/suppliers")]
    public class SuppliersController : AbpController
    {
        [Route("")]
        public ListResultDto<TestDto>  GetAsync()
        {
            return new ListResultDto<TestDto>(
                new[]
                {
                    new TestDto
                    {
                        Id = 1,
                        CompanyName = "Apple"
                    },
                    new TestDto
                    {
                        Id = 2,
                        CompanyName = "Microsoft"
                    }
                }
            );
        }
    }
    
    public class TestDto
    {
        public string CompanyName { get; set; }
        public int Id { get; set; }
    }
    

  • User Avatar
    0
    markusbergkvist created

    Hello, It seems that the issue stems from that we are using azure signalr in our Blazor Server app. When not setting up that in the Blazor module we get no httpclient null exception. We have found two possible related issues in the abp support:

    https://abp.io/support/questions/2054/Using-Azure-SignalR-backplane-for-abp-Blazor-app https://abp.io/support/questions/4993/Issue-Implementing-Azure's-Managed-SignalR-service-with-an-ABP-Blazor-Server-application

    We have tried using both solutions/filters when configuring azure signalr and neither solves the issue that the httpclient beeing null when trying to set the headers in the lookup api request call. Can you please check if there is any issues in how we are implementing azure signalr in the module?

        public override void ConfigureServices(ServiceConfigurationContext context)
        {
            var hostingEnvironment = context.Services.GetHostingEnvironment();
            var configuration = context.Services.GetConfiguration();
    
            Configure<AbpSystemTextJsonSerializerOptions>(options => { options.JsonSerializerOptions.ReferenceHandler = ReferenceHandler.IgnoreCycles; });
    
            // Add services to the container.
            context.Services.AddRazorComponents()
                .AddInteractiveServerComponents();
    
            if (!configuration.GetValue<bool>("App:DisablePII"))
            {
                Microsoft.IdentityModel.Logging.IdentityModelEventSource.ShowPII = true;
                Microsoft.IdentityModel.Logging.IdentityModelEventSource.LogCompleteSecurityArtifact = true;
            }
    
            if (!configuration.GetValue<bool>("AuthServer:RequireHttpsMetadata"))
            {
                Configure<OpenIddictServerAspNetCoreOptions>(options => { options.DisableTransportSecurityRequirement = true; });
    
                Configure<ForwardedHeadersOptions>(options => { options.ForwardedHeaders = ForwardedHeaders.XForwardedProto; });
            }
    
            ConfigureAuthentication(context);
            ConfigureUrls(configuration);
            ConfigureBundles();
            ConfigureImpersonation(context, configuration);
            ConfigureCookieConsent(context);
            ConfigureAutoMapper();
            ConfigureVirtualFileSystem(hostingEnvironment);
            ConfigureSwaggerServices(context.Services);
            ConfigureAutoApiControllers();
            ConfigureBlazorise(context);
            ConfigureRouter(context);
            ConfigureMenu(context);
            ConfigureTheme();
            ConfigureLocalization();
            ConfigureHangfire(context, configuration);
            ConfigureSignalR(context);
    
            Configure<AbpBackgroundJobWorkerOptions>(options => { options.DefaultTimeout = 3600; });
        }
    
        private void ConfigureSignalR(ServiceConfigurationContext context)
        {
            context.Services.AddSignalR(options =>
            {
                options.AddFilter<AbpSignalRFilter>();
            }).AddAzureSignalR(options =>
            {
                options.ConnectionString = context.Services.GetConfiguration().GetConnectionString("AzureSignalR");
                options.ServerStickyMode = Microsoft.Azure.SignalR.ServerStickyMode.Required;
            });
    
            // context.Services.AddSignalR(options =>
            // {
            //     options.AddFilter<AbpMultiTenantHubFilter>();
            // }).AddAzureSignalR();
            
            context.Services.AddResponseCompression(opts =>
            {
                opts.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(
                    ["application/octet-stream"]);
            });
        }
    

    We have tried setting up signalr with the filters from both previous mentioned issues and with no filter at all. Thanks!

  • User Avatar
    0
    markusbergkvist created

    Follow up, we have tried setting up azure signalr in the same way in the test project and we get the same issue there, we have tested with the filters mentioned before and without them, not solving the issue.

  • User Avatar
    0
    liangshiwei created
    Support Team Fullstack Developer

    Did you enable websocket on azure

  • User Avatar
    0
    markusbergkvist created

    We have not deployed to Azure yet, so this issue is occurring in our local development environment. Therefore, Azure-specific settings like enabling WebSockets should not be related to the problem.

  • User Avatar
    0
    liangshiwei created
    Support Team Fullstack Developer

    Hi,

    Hi https://github.com/abpframework/abp/blob/dev/framework/src/Volo.Abp.AspNetCore.SignalR/Volo/Abp/AspNetCore/SignalR/AbpAspNetCoreSignalRModule.cs#L41

    I guess your Signalr configuration code overrides the ABP code.

    you can try to remove your signalr code and:

    PreConfigure<ISignalRServerBuilder>(builder =>
    {
        builder.AddAzureSignalR(options =>
        {
            options.ConnectionString = context.Services.GetConfiguration().GetConnectionString("AzureSignalR");
            options.ServerStickyMode = Microsoft.Azure.SignalR.ServerStickyMode.Required;
        });
    });
    

    or

    context.Services.AddSignalR(options =>
    {
        options.DisableImplicitFromServicesParameters = true;
        options.AddFilter<AbpHubContextAccessorHubFilter>();
        options.AddFilter<AbpAuthenticationHubFilter>();
        options.AddFilter<AbpAuditHubFilter>();
    }).AddAzureSignalR(options =>
    {
        options.ConnectionString = context.Services.GetConfiguration().GetConnectionString("AzureSignalR");
        options.ServerStickyMode = Microsoft.Azure.SignalR.ServerStickyMode.Required;
    });
    
    
  • User Avatar
    0
    markusbergkvist created

    Hi,

    Thank you for your suggestions. We have tried both configurations you provided:

    1. Using PreConfigure<ISignalRServerBuilder>:

      PreConfigure<ISignalRServerBuilder>(builder =>
      {
          builder.AddAzureSignalR(options =>
          {
              options.ConnectionString = context.Services.GetConfiguration().GetConnectionString("AzureSignalR");
              options.ServerStickyMode = Microsoft.Azure.SignalR.ServerStickyMode.Required;
          });
      });
      
    2. Using context.Services.AddSignalR with filters:

      context.Services.AddSignalR(options =>
      {
          options.DisableImplicitFromServicesParameters = true;
          options.AddFilter<AbpHubContextAccessorHubFilter>();
          options.AddFilter<AbpAuthenticationHubFilter>();
          options.AddFilter<AbpAuditHubFilter>();
      }).AddAzureSignalR(options =>
      {
          options.ConnectionString = context.Services.GetConfiguration().GetConnectionString("AzureSignalR");
          options.ServerStickyMode = Microsoft.Azure.SignalR.ServerStickyMode.Required;
      });
      

    Unfortunately, neither of these resolved the issue of HttpContext being null when Azure SignalR is configured.


    We have found two related issues with Azure SignalR disrupting the httpcontext:

    Since Abp is relying on httpcontext in the BlazorServerLookupApiRequestService we either need an alternate solution to setting the headers or possibly a way to configure azure signalr so that we still have access to the httpcontext if that still is the issue.

    Thank you for your assistance

  • User Avatar
    0
    liangshiwei created
    Support Team Fullstack Developer

    Hi,

    Okay, seems like a problem, i will check it

  • User Avatar
    0
    markusbergkvist created

    Ok, thank you, let us know about the progress when possible, our project progress is on hold while we cant solve this.

    Thanks again

  • User Avatar
    0
    liangshiwei created
    Support Team Fullstack Developer

    Hi,

    I didn't find a standard way to solve this. you can see even Microsoft's examples are using httpContextAccessor.

    https://github.com/dotnet/blazor-samples/blob/main/9.0/BlazorWebAppOidcBff/BlazorWebAppOidc/ServerWeatherForecaster.cs#L11

    We need to wait for the Azure SignalR team to provide a better explanation.

    https://github.com/dotnet/AspNetCore.Docs/issues/13508

    you can try this as a temporary solution:

    public class AzureSignalRCookieService : ISingletonDependency
    {
        public string Cookies { get; set; }
    }
    
    [Dependency(ReplaceServices = true)]
    public class MyBlazorServerLookupApiRequestService : ILookupApiRequestService, ITransientDependency
    {
        public IHttpClientFactory HttpClientFactory { get; }
        public IRemoteServiceHttpClientAuthenticator HttpClientAuthenticator { get; }
        public IRemoteServiceConfigurationProvider RemoteServiceConfigurationProvider { get; }
        public ICurrentTenant CurrentTenant { get; }
        public IHttpContextAccessor HttpContextAccessor { get; }
        public NavigationManager NavigationManager { get; }
        
        public AzureSignalRCookieService SignalRCookieService { get; }
        
        public MyBlazorServerLookupApiRequestService(IHttpClientFactory httpClientFactory,
            IRemoteServiceHttpClientAuthenticator httpClientAuthenticator,
            ICurrentTenant currentTenant,
            IHttpContextAccessor httpContextAccessor,
            NavigationManager navigationManager,
            IRemoteServiceConfigurationProvider remoteServiceConfigurationProvider,
            AzureSignalRCookieService signalRCookieService)
        {
            HttpClientFactory = httpClientFactory;
            HttpClientAuthenticator = httpClientAuthenticator;
            CurrentTenant = currentTenant;
            HttpContextAccessor = httpContextAccessor;
            NavigationManager = navigationManager;
            RemoteServiceConfigurationProvider = remoteServiceConfigurationProvider;
            SignalRCookieService = signalRCookieService;
        }
    
        public async Task<string> SendAsync(string url)
        {
            var client = HttpClientFactory.CreateClient(nameof(BlazorServerLookupApiRequestService));
            var requestMessage = new HttpRequestMessage(HttpMethod.Get, url);
    
            var uri = new Uri(url, UriKind.RelativeOrAbsolute);
            if (!uri.IsAbsoluteUri)
            {
                var remoteServiceConfig = await RemoteServiceConfigurationProvider.GetConfigurationOrDefaultOrNullAsync("Default");
                if (remoteServiceConfig != null)
                {
                    // Blazor tiered mode
                    var baseUrl = remoteServiceConfig.BaseUrl;
                    client.BaseAddress = new Uri(baseUrl);
                    AddHeaders(requestMessage);
                    await HttpClientAuthenticator.Authenticate(new RemoteServiceHttpClientAuthenticateContext(client, requestMessage, new RemoteServiceConfiguration(baseUrl), string.Empty));
                }
                else
                {
                    // Blazor server  mode
                    client.BaseAddress = new Uri(NavigationManager.BaseUri);
                    var request = HttpContextAccessor.HttpContext?.Request;
                    if (request != null)
                    {
                        foreach (var header in HttpContextAccessor.HttpContext!.Request.Headers)
                        {
                            requestMessage.Headers.TryAddWithoutValidation(header.Key, header.Value.ToArray());
                        }
                    }
                    else
                    {
                        requestMessage.Headers.TryAddWithoutValidation("Cookie", SignalRCookieService.Cookies);
                    }
                }
            }
    
            var response = await client.SendAsync(requestMessage);
            return await response.Content.ReadAsStringAsync();
        }
    
        protected virtual void AddHeaders(HttpRequestMessage requestMessage)
        {
            if (CurrentTenant.Id.HasValue)
            {
                requestMessage.Headers.Add(TenantResolverConsts.DefaultTenantKey, CurrentTenant.Id.Value.ToString());
            }
    
            var currentCulture = CultureInfo.CurrentUICulture.Name ?? CultureInfo.CurrentCulture.Name;
            if (!currentCulture.IsNullOrEmpty())
            {
                requestMessage.Headers.AcceptLanguage.Add(new(currentCulture));
            }
        }
    }
    
    app.Use((httpContext, next) =>
    {
        httpContext.RequestServices.GetRequiredService<AzureSignalRCookieService>().Cookies = httpContext.Request.Cookies.Select(x => $"{x.Key}={x.Value}").JoinAsString("; ");
        return next();
    });
    
  • User Avatar
    0
    markusbergkvist created

    Hello, The temporary solution you suggested seems to work, thanks for your help!

Made with ❤️ on ABP v9.2.0-preview. Updated on January 16, 2025, 11:47