aspnetcore icon indicating copy to clipboard operation
aspnetcore copied to clipboard

SignalR negotiate fails from Blazor Server with windows authentication

Open bmoteria opened this issue 5 years ago • 35 comments

Describe the bug

I have a Blazor Server app that uses Windows Authentication. It requires SignalR hub connection to update partial UI when a user click a button. I have followed Use ASP.NET Core SignalR with Blazor WebAssembly documentation as well read #22767 and tried following SignalR cross-origin negotiation for authentication. The following code works when debugging locally on IIS Express using Visual Studio 2019 Enterprise but it returns 401 Unauthorized exception while starting hub connection when it's deployed to IIS, especially when it has subdomain binding (http://subdomain.mydomain.org). It only works when navigating via server name and port number "http://servername:60006". Please note that the signalr hub are hosted along with Blazor server app so there is no cross origin.

NavMenu.razor

@using Microsoft.AspNetCore.SignalR.Client
@inject NavigationManager NavigationManager
@implements IDisposable

<div class="top-row pl-4 navbar navbar-dark">
    <a class="navbar-brand" href="">BlazorSignalRDemo</a>
    <button class="navbar-toggler" @onclick="ToggleNavMenu">
        <span class="navbar-toggler-icon"></span>
    </button>
</div>

<div class="@NavMenuCssClass" @onclick="ToggleNavMenu">
    <ul class="nav flex-column">
        <li class="nav-item px-3">
            <NavLink class="nav-link" href="" Match="NavLinkMatch.All">
                <span class="oi oi-home" aria-hidden="true"></span> Home
            </NavLink>
        </li>
        <li class="nav-item px-3">
            <NavLink class="nav-link" href="counter">
                <span class="oi oi-plus" aria-hidden="true"></span> Counter (@counter)
            </NavLink>
        </li>
        <li class="nav-item px-3">
            <NavLink class="nav-link" href="fetchdata">
                <span class="oi oi-list-rich" aria-hidden="true"></span> Fetch data
            </NavLink>
        </li>
    </ul>
</div>

@code {
    private string hubErrorMsg;
    private HubConnection hubConnection;
    private int counter = 0;
    private bool collapseNavMenu = true;

    private string NavMenuCssClass => collapseNavMenu ? "collapse" : null;

    private void ToggleNavMenu()
    {
        collapseNavMenu = !collapseNavMenu;
    }

    protected override async Task OnInitializedAsync()
    {
        hubConnection = new HubConnectionBuilder()
            .WithUrl(NavigationManager.ToAbsoluteUri("/taskhub"), config =>
            {
                config.UseDefaultCredentials = true;
                config.HttpMessageHandlerFactory = innerHandler => new IncludeRequestCredentialsMessageHandler {
                     InnerHandler = innerHandler
                };
            })
            .WithAutomaticReconnect()
            .Build();

        hubConnection.On<int>("ReceiveTaskCount", (counter) =>
        {
            this.counter = counter;
            StateHasChanged();
        });

        try
        {
            await hubConnection.StartAsync();
        }
        catch (Exception ex)
        {
            hubErrorMsg = ex.Message;
        }
    }

    public void Dispose()
    {
        _ = hubConnection.DisposeAsync();
    }
}

IncludeRequestCredentialsMessageHandler.cs

using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;

namespace BlazorSignalRDemo
{
    public class IncludeRequestCredentialsMessageHandler : DelegatingHandler
    {
        public IncludeRequestCredentialsMessageHandler()
        {
            InnerHandler = new HttpClientHandler()
            {
                Credentials = CredentialCache.DefaultNetworkCredentials,
                UseDefaultCredentials = true,
                PreAuthenticate = true
            };
        }

        protected override Task<HttpResponseMessage> SendAsync(
            HttpRequestMessage request, CancellationToken cancellationToken)
        {
            // The following SetBrowserRequestCredentials(...) api is not available for Blazor Server Side.
            //request.SetBrowserRequestCredentials(BrowserRequestCredentials.Include);

            return base.SendAsync(request, cancellationToken);
        }
    }
}

To Reproduce

  • Deploy BlazorSignalRDemo_WinAuth.zip to IIS.
  • Add following bindings to IIS site: 1. http://subdomain.mydomain.org 2. http://servername:60006 image
  • Navigate to http://subdomain.mydomain.org. - It doesn't work! Please see exception details below.
  • Navigate to http://servername:60006. - It works!

Exceptions (if any)

System.Net.Http.HttpRequestException: Response status code does not indicate success: 401 (Unauthorized).
   at System.Net.Http.HttpResponseMessage.EnsureSuccessStatusCode()
   at Microsoft.AspNetCore.Http.Connections.Client.HttpConnection.NegotiateAsync(Uri url, HttpClient httpClient, ILogger logger, CancellationToken cancellationToken)
   at Microsoft.AspNetCore.Http.Connections.Client.HttpConnection.GetNegotiationResponseAsync(Uri uri, CancellationToken cancellationToken)
   at Microsoft.AspNetCore.Http.Connections.Client.HttpConnection.SelectAndStartTransport(TransferFormat transferFormat, CancellationToken cancellationToken)
   at Microsoft.AspNetCore.Http.Connections.Client.HttpConnection.StartAsyncCore(TransferFormat transferFormat, CancellationToken cancellationToken)
   at System.Threading.Tasks.ForceAsyncAwaiter.GetResult()
   at Microsoft.AspNetCore.Http.Connections.Client.HttpConnection.StartAsync(TransferFormat transferFormat, CancellationToken cancellationToken)
   at Microsoft.AspNetCore.Http.Connections.Client.HttpConnectionFactory.ConnectAsync(EndPoint endPoint, CancellationToken cancellationToken)
   at Microsoft.AspNetCore.Http.Connections.Client.HttpConnectionFactory.ConnectAsync(EndPoint endPoint, CancellationToken cancellationToken)
   at Microsoft.AspNetCore.SignalR.Client.HubConnection.StartAsyncCore(CancellationToken cancellationToken)
   at Microsoft.AspNetCore.SignalR.Client.HubConnection.StartAsyncInner(CancellationToken cancellationToken)
   at System.Threading.Tasks.ForceAsyncAwaiter.GetResult()
   at Microsoft.AspNetCore.SignalR.Client.HubConnection.StartAsync(CancellationToken cancellationToken)
   at BlazorSignalRDemo.Shared.NavbarItemsBase.OnInitializedAsync() in C:\<MY_PATH>\BlazorSignalRDemo\Shared\NavbarItems.razor.cs:line 38
   at Microsoft.AspNetCore.Components.ComponentBase.RunInitAndSetParametersAsync()
   at Microsoft.AspNetCore.Components.RenderTree.Renderer.GetErrorHandledTask(Task taskToHandle)
   at Microsoft.AspNetCore.Components.Rendering.HtmlRenderer.HandleException(Exception exception)
   at Microsoft.AspNetCore.Components.RenderTree.Renderer.GetErrorHandledTask(Task taskToHandle)
   at Microsoft.AspNetCore.Components.RenderTree.Renderer.ProcessAsynchronousWork()
   at Microsoft.AspNetCore.Components.RenderTree.Renderer.RenderRootComponentAsync(Int32 componentId, ParameterView initialParameters)
   at Microsoft.AspNetCore.Components.Rendering.HtmlRenderer.CreateInitialRenderAsync(Type componentType, ParameterView initialParameters)
   at Microsoft.AspNetCore.Components.Rendering.HtmlRenderer.RenderComponentAsync(Type componentType, ParameterView initialParameters)
   at Microsoft.AspNetCore.Components.Rendering.RendererSynchronizationContext.<>c__11`1.<<InvokeAsync>b__11_0>d.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at Microsoft.AspNetCore.Mvc.ViewFeatures.StaticComponentRenderer.PrerenderComponentAsync(ParameterView parameters, HttpContext httpContext, Type componentType)
   at Microsoft.AspNetCore.Mvc.ViewFeatures.ComponentRenderer.PrerenderedServerComponentAsync(HttpContext context, ServerComponentInvocationSequence invocationId, Type type, ParameterView parametersCollection)
   at Microsoft.AspNetCore.Mvc.ViewFeatures.ComponentRenderer.RenderComponentAsync(ViewContext viewContext, Type componentType, RenderMode renderMode, Object parameters)
   at Microsoft.AspNetCore.Mvc.TagHelpers.ComponentTagHelper.ProcessAsync(TagHelperContext context, TagHelperOutput output)
   at Microsoft.AspNetCore.Razor.Runtime.TagHelpers.TagHelperRunner.<RunAsync>g__Awaited|0_0(Task task, TagHelperExecutionContext executionContext, Int32 i, Int32 count)
   at BlazorSignalR.Pages.Pages__Host.<ExecuteAsync>b__17_1() in C:\<MY_PATH>\BlazorSignalR\Pages\_Host.cshtml:line 23
   at Microsoft.AspNetCore.Razor.Runtime.TagHelpers.TagHelperExecutionContext.SetOutputContentAsync()
   at BlazorSignalR.Pages.Pages__Host.ExecuteAsync() in C:\<MY_PATH>\BlazorSignalR\Pages\_Host.cshtml:line 5
   at Microsoft.AspNetCore.Mvc.Razor.RazorView.RenderPageCoreAsync(IRazorPage page, ViewContext context)
   at Microsoft.AspNetCore.Mvc.Razor.RazorView.RenderPageAsync(IRazorPage page, ViewContext context, Boolean invokeViewStarts)
   at Microsoft.AspNetCore.Mvc.Razor.RazorView.RenderAsync(ViewContext context)
   at Microsoft.AspNetCore.Mvc.ViewFeatures.ViewExecutor.ExecuteAsync(ViewContext viewContext, String contentType, Nullable`1 statusCode)
   at Microsoft.AspNetCore.Mvc.ViewFeatures.ViewExecutor.ExecuteAsync(ViewContext viewContext, String contentType, Nullable`1 statusCode)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeNextResultFilterAsync>g__Awaited|29_0[TFilter,TFilterAsync](ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Rethrow(ResultExecutedContextSealed context)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.ResultNext[TFilter,TFilterAsync](State& next, Scope& scope, Object& state, Boolean& isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeResultFilters>g__Awaited|27_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeNextResourceFilter>g__Awaited|24_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>g__Awaited|19_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Logged|17_1(ResourceInvoker invoker)
   at Microsoft.AspNetCore.Routing.EndpointMiddleware.<Invoke>g__AwaitRequestTask|6_0(Endpoint endpoint, Task requestTask, ILogger logger)
   at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)
   at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)
   at Microsoft.AspNetCore.Builder.Extensions.MapMiddleware.Invoke(HttpContext context)
   at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context)

Further technical details

  • ASP.NET Core version: 3.1.401
  • Include the output of dotnet --info
.NET Core SDK (reflecting any global.json):
 Version:   3.1.401
 Commit:    5b6f5e5005

Runtime Environment:
 OS Name:     Windows
 OS Version:  10.0.18363
 OS Platform: Windows
 RID:         win10-x64
 Base Path:   C:\Program Files\dotnet\sdk\3.1.401\

Host (useful for support):
  Version: 3.1.7
  Commit:  fcfdef8d6b

.NET Core SDKs installed:
  3.1.400 [C:\Program Files\dotnet\sdk]
  3.1.401 [C:\Program Files\dotnet\sdk]

.NET Core runtimes installed:
  Microsoft.AspNetCore.All 2.1.20 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.All]
  Microsoft.AspNetCore.All 2.1.21 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.All]
  Microsoft.AspNetCore.App 2.1.20 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 2.1.21 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 3.1.6 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 3.1.7 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.NETCore.App 2.1.20 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 2.1.21 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 3.1.6 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 3.1.7 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.WindowsDesktop.App 3.1.6 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]
  Microsoft.WindowsDesktop.App 3.1.7 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]

To install additional .NET Core runtimes or SDKs:
  https://aka.ms/dotnet-download
  • The IDE (VS / VS Code/ VS4Mac) you're running on, and it's version 1. VS Enterprise 2019 v16.8.0 Preview 1.0 2. VS Enterprise 2019 v16.7.1

Thanks, Bishan M.

bmoteria avatar Aug 18 '20 13:08 bmoteria

@bmoteria thanks for contacting us.

Are you trying to start a blazor server application cross-origin?

javiercn avatar Aug 18 '20 14:08 javiercn

@javiercn No. The blazor server app and signalr hub are hosted on the same site. Here is the sample project.

bmoteria avatar Aug 18 '20 15:08 bmoteria

Thanks for contacting us. We're moving this issue to the Next sprint planning milestone for future evaluation / consideration. We will evaluate the request when we are planning the work for the next milestone. To learn more about what to expect next and how this issue will be handled you can read more about our triage process here.

ghost avatar Aug 18 '20 16:08 ghost

@captainsafia Is there any workaround for this issue? We're writing an enterprise application and we need to determine if could proceed further or find an alternative path. Thanks! (cc @BrennanConroy)

bmoteria avatar Aug 24 '20 12:08 bmoteria

Hi @bmoteria -- not sure yet! We'll be investigating this issue soon and will post back about whether this a bug/known issue/fixable in your app/etc.

captainsafia avatar Aug 25 '20 02:08 captainsafia

We've moved this issue to the Backlog milestone. This means that it is not going to be worked on for the coming release. We will reassess the backlog following the current release and consider this item at that time. To learn more about our issue management process and to have better expectation regarding different types of issues you can read our Triage Process.

ghost avatar Oct 09 '20 17:10 ghost

This is a blocking issue for me too. The only way I got rid of the 401 was to allow Anonymous Authentication on IIS on a QA environment but that is not an option for Production.

jorgejmartins avatar Nov 10 '20 01:11 jorgejmartins

I also get a 401, but this is on Azure App Services using Azure AD. It works using the subdomain.azurewebsites.net domain but when I access the site from our custom configured domain I get a 401. I have added CORS to allow all origins but still no luck. Is there any workaround for this issue?

faarbaek avatar Nov 24 '20 08:11 faarbaek

I am facing this exact same issue, in an angularjs client. I can get passed it by using websockets as transport only, and then setting skipNegotiation to false. Then it works for obvious reasons.

raltrifork avatar Nov 30 '20 13:11 raltrifork

I am facing this exact same issue, in an angularjs client. I can get passed it by using websockets as transport only, and then setting skipNegotiation to false. Then it works for obvious reasons.

@raltrifork, thanks for the tip. Does this makes sense?

    _hubConnection = new HubConnectionBuilder()
    .WithUrl(_hubUrl, options =>
    {
        options.UseDefaultCredentials = true;
        options.SkipNegotiation = false;
        options.Transports = Microsoft.AspNetCore.Http.Connections.HttpTransportType.WebSockets;
    })
    .WithAutomaticReconnect()
    .Build();

Still no luck. Sticky 401! :-( I also tried some other options ... same result!

  • SkipNegotiation = true
  • UseDefaultCredentials = false; Credentials = new NetworkCredential("", "", "***********");

jorgejmartins avatar Nov 30 '20 14:11 jorgejmartins

We found a temporary workaround. Instead of using signalr, we now use Scoped service (for example TaskCountService.cs). The code inside task count service looks like so:

public event Action OnReloadTaskCount;

public void ReloadTaskCount()
{
    OnReloadTaskCount?.Invoke();
}

Subscribe to OnReloadTaskCount event inside navbar component (TopNavbar.razor):

public partial class TopNavbar
{
    [Inject]
    protected TaskService TaskService { get; set; }
    [Inject]
    protected TaskCountService TaskCountService { get; set; }

    protected overriede void OnInitialized()
    {
        TaskCountService.OnReloadTaskCount += ReloadTaskCount;
    }

    private void ReloadTaskCount()
    {
        InvokeAsync(() => {
            _assignedToMeCount = TaskService.GetAssignedToMeCount(_currentUserIdentityName);
            StateHasChanged();
        });
    }
}

Now that it's subscribed to an event, you can invoke ReloadTaskCount method of TaskCountService.cs from any razor pages/components:

*****Create.razor.cs*****
public partial class Create
{
    [Inject]
    protected TaskCountService TaskCountService { get; set; }
    // other code goes here...

    public async Task SubmitValidFormAsync()
    {
        // calling database repository.
        // and other stuff..
        TaskCountService.ReloadTaskCount();
    }
}

bmoteria avatar Nov 30 '20 14:11 bmoteria

Jesus! My mistake I meant setting SkipNegotiation to true, if this is set it should at least not be on the negotiation request the exception is thrown, since it is not performed.

raltrifork avatar Nov 30 '20 14:11 raltrifork

Just another follow up to say I found another way to make it work (temporarily, until it's solved by the SignalR/Core/Blazor team) and it's by moving the Hub to a separate project, publish it on a separate IIS application and with anonymous authentication enabled. Had to move also the WEB API Controller that interacts with the hub and, for the moment, it also has to remain with anonymous authentication. This way I've been able to remove the anonymous authentication from the main project. So far it's working fine, not graceful, just fine.

jorgejmartins avatar Jan 09 '21 02:01 jorgejmartins

I have the same problem. Are you going to fix this?

Amangeldi avatar Feb 16 '21 16:02 Amangeldi

I was able to resolve this issue by simply setting the UseDefaultCredentials configuration option to true:

this.hubConnection = new HubConnectionBuilder()
                     .WithUrl(
                              hubUrl,
                              config => config.UseDefaultCredentials = true)
                     .WithAutomaticReconnect()
                     .Build();

darrylluther avatar Mar 10 '21 16:03 darrylluther

My workaround for this issue is to catch the SignalR update with JavaScript and invoke a Blazor method.

JavaScript code:

const connection = new signalR.HubConnectionBuilder().withUrl("/myhub").build();

window.blazorSignalR = {
    init: function (dotnetHelper, bindTo, nameFunc) {
        connection.on(bindTo,
            function (someArg) {
                return dotnetHelper.invokeMethodAsync(nameFunc, someArg);
            });
    }
}

Component code:

private DotNetObjectReference<myComponent> dotNetRef;
private string EventName = "SomethingHappened";

protected override async Task OnInitializedAsync()
{
   dotNetRef = DotNetObjectReference.Create(this);
}

protected override async Task OnAfterRenderAsync(bool firstRender)
{
    await base.OnAfterRenderAsync(firstRender);

    if (firstRender)
    {
        await JSRuntime.InvokeVoidAsync("blazorSignalR.init", dotNetRef, EventName, "OnSomethingHappened");
    }
}

[JSInvokable("OnSomethingHappened")]
public async Task OnSomethingHappened(string data)
{
   // React to SomethingHappened
}

SignalR: await myHub.Clients.All.SendAsync("SomethingHappened", "data");

RikDriever avatar Mar 15 '21 20:03 RikDriever

I was able to resolve this issue by simply setting the UseDefaultCredentials configuration option to true:

this.hubConnection = new HubConnectionBuilder()
                     .WithUrl(
                              hubUrl,
                              config => config.UseDefaultCredentials = true)
                     .WithAutomaticReconnect()
                     .Build();

Thank you so much, that did the trick!

Addendum: No, only when running locally, but when published to IIS I get the error 401 (unauthorized) again.

Any updates on that?

Haraldi-Coding avatar Apr 09 '22 19:04 Haraldi-Coding

Any updates? just work local but no on real IIS

blindmeis avatar May 10 '22 13:05 blindmeis

We're also waiting for an update on this.

szczz avatar May 12 '22 19:05 szczz

Almost 2 years and this is still awaiting a response? @captainsafia Any updates?

darrylluther avatar May 12 '22 21:05 darrylluther

Any update on this ? We need this feature as most of our apps uses Windows Authentication and not able to use Blazor Server App with SignalR.

@darrylluther Thanks for the trick, works locally, but not working with IIS after deployment.

chinmay-trivedi avatar May 26 '22 22:05 chinmay-trivedi

Any update on this thread. This still does not work for me as i still get 401 Unauthorized when using Windows Authentication

Ackermannen avatar May 16 '23 14:05 Ackermannen

Same error here. Waiting for updates

deepdarkenergy avatar Jul 24 '23 04:07 deepdarkenergy

Ran into the same problem. Works fine locally, 401 when deployed to iis.

liamcannon avatar Sep 11 '23 13:09 liamcannon

To learn more about what this message means, what to expect next, and how this issue will be handled you can read our Triage Process document. We're moving this issue to the .NET 9 Planning milestone for future evaluation / consideration. Because it's not immediately obvious what is causing this behavior, we would like to keep this around to collect more feedback, which can later help us determine how to handle this. We will re-evaluate this issue, during our next planning meeting(s). If we later determine, that the issue has no community involvement, or it's very rare and low-impact issue, we will close it - so that the team can focus on more important and high impact work.

ghost avatar Dec 13 '23 17:12 ghost

After further discussion we believe this is not specific to Blazor by any means and is rather something that should be addressed on the SignalR side. @BrennanConroy moving to your area path.

mkArtakMSFT avatar Dec 13 '23 17:12 mkArtakMSFT

After further discussion

Can you share any details about this discussion...

BrennanConroy avatar Dec 13 '23 17:12 BrennanConroy

Update on our side, we managed to get it to work, but not through using SignalR, we had to revert to using long polling instead. When connecting using HubConnectionBuilder, configure the transport to use websockets, then longpolling. Do the same for MapHub in Program.cs. This is not good for production scenarios, but fine for us with <10 users.

Hope this helps anyone else.

Ackermannen avatar Dec 13 '23 17:12 Ackermannen

Are people in this thread trying to do impersonation? Or are you just trying to authenticate with the application's identity?

If you're trying to authenticate with the apps identity, setting UseDefaultCredentials as suggested in https://github.com/dotnet/aspnetcore/issues/25000#issuecomment-795685660 should work. I'm guessing all the thumbs up means this has worked for a bunch of people.

If you're trying to do impersonation, take a look at the impersonation docs at https://learn.microsoft.com/en-us/aspnet/core/security/authentication/windowsauth?view=aspnetcore-8.0&tabs=visual-studio#impersonation

halter73 avatar Dec 14 '23 00:12 halter73

@halter73

Only works locally. Not when the app is deployed to IIS.

szczz avatar Dec 14 '23 00:12 szczz