Blazor server running on http behind NGINX reverse proxy

I've only been using Radzen for a couple of days, so new to me.

I made a demo app that uses SqlServer and Security. (btw the wizard for visual studio has a bug and specifies MySql instead of SqlServer in the Identity code, but Blazor Studio does it properly).

I removed the https from launchsettings.json.

I removed the line app.UseHttpsRedirection() from program.cs

Running on Ubuntu 22.04.

I run it from http://localhost:5000 and everything works beautifully.

But when I run it from https://localhost using NGINX as a reverse proxy, it does not proceed beyond the login screen.

The NGINX config is the following (originally from some microsoft documentation / instructions):

server {
listen 443 ssl;

ssl_certificate /etc/ssl/bundle.crt;
ssl_certificate_key /etc/ssl/something.com.key;

server_name   something.com;
location / {
	proxy_pass     	http://127.0.0.1:5000;
	proxy_http_version 1.1;
	proxy_set_header   Upgrade $http_upgrade;
	proxy_set_header   Connection $connection_upgrade;
	proxy_set_header   Host $host;
	proxy_cache_bypass $http_upgrade;
	proxy_set_header   X-Forwarded-For $proxy_add_x_forwarded_for;
	proxy_set_header   X-Forwarded-Proto $scheme;
}

This exact setup works as expected for other similar demo apps using microsoft fluent ui blazor and also blazorise, so if you could point me in an particular direction for troubleshooting, it would be greatly appreciated!

This could also be helpful:

I tried the suggestions (removing HttpPost, changing to HttpMethod.Get), with the same result.

Also I am redirecting all http requests to https in the nginx conf:

server {
listen 80 default_server;
server_name _;
return 301 https://$host$request_uri;
}

Doesn't help. Is it is a useful clue that after I type the credentials in and then go to http://localhost:5000 it is in fact authenticated and logged in? But the https page remains on the login screen with no feedback.

Thank you, I had used that before, but it has been updated in the past year. The only thing I got from this was to add the UseForwardedHeaders to the middleware first thing after builder.Build():

app.UseForwardedHeaders(new ForwardedHeadersOptions
{
ForwardedHeaders = Microsoft.AspNetCore.HttpOverrides.ForwardedHeaders.XForwardedFor |
Microsoft.AspNetCore.HttpOverrides.ForwardedHeaders.XForwardedProto
});

Still no cigar though. Clearly I must be missing something trivial, surely running a radzen blazor http server behind an NGINX reverse proxy must be super common? What stupid thing am I doing?

I have an identical fluent ui demo that uses the same identity sql server database, also running in http, and it works perfectly as expected, and it didn't have the forwarded headers line either. Thanks for any comments.

I think I know why. It is something I had issues with too. The request comes from "something.com" but in the AccountController.razor the _navigationManager.BaseUri will return "something.com" as the uri. The issue might be that you need to add to your local machine DNS file that the "something.com" resolves to the localhost:5000.

Looking at the post it seems it should work, but check out the breakpoints in the AccountController. For me it didn't through any error in the console and I could find it out by going through the AccountController.razor and the UserService calls.

This problem usually occurs when nginx does not redirect HTTP POST requests to HTTPS (the sercurity service makes a POST request to get the current user). Check the linked thread for a modification in the source which uses GET instead of POST. I any case you can add logging in the SecurityService to troubleshoot the deployment issue. There are probably messages logged in the output too (also you can check NGINX's logs for failed HTTP requests).

And to clarify a few points:

The UI framework is not relevant to the issue as it is entirely server-side.

There is no such thing as radzen blazor http server. The server is the ASP.NET Core built-in one.

That makes sense to me, this is probably it.

Yes, I did that, unfortunately it had no effect.

Thank you, I will do more logging and look more closely into this.

Well of course, but semantics aside my point is that the fluent ui wizard app with identity and authentication works with this nginx setup, the blazorise wizard demo app with added identity also works with this same nginx setup, but unfortunately the Radzen wizard app does not.... So something IS different.

They probably don't use POST requests ...

Here are a few stackoverflow posts about nginx losing POST during redirects:

Also here is a working nginx configuration from a Radzen Blazor application that uses security which we have in production for two years:

server {
  listen      [::]:80;
  listen      80;

  location / {
    return 301 https://$host:443$request_uri;
  }
}

Finally you can check what the actual URL being requested here is:

public async Task<ApplicationAuthenticationState> GetAuthenticationStateAsync()
{
     var uri =  new Uri($"{navigationManager.BaseUri}Account/CurrentUser");

     var response = await httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Post, uri));

     return await response.ReadAsync<ApplicationAuthenticationState>();
}

If it is http instead of https and nginx redirects aren't properly configured it would fail.

I did more research and successfully deployed a brand new app created with Radzen Blazor Studio behind nginx.

As I suspected the problem is HTTPS propagation. By default NGINX uses 301 redirects which loses the request method (POST gets converted to a GET). This in turn confuses the SecurityService that Radzen apps use.

The default Microsoft instructions say that this is needed in order to forward http headers when using nginx:

app.UseForwardedHeaders(new ForwardedHeadersOptions
{
    ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto
});

This however didn't work out of the box for me - Blazor still thought it was running over HTTP - @NavigationManager.BaseUri returned http. I scratched my head and found this: c# - .net Core X Forwarded Proto not working - Stack Overflow

It says that the forwarding middleware supports only proxies that work on 127.0.0.1 by default. The suggested solution from that thread worked as expected.

In short to support nginx deployment with security add this code right below var app = builder.Build(); in Program.cs

var app = builder.Build();

// Start -->
var forwardingOptions = new ForwardedHeadersOptions()
{
    ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto
};
forwardingOptions.KnownNetworks.Clear();
forwardingOptions.KnownProxies.Clear();

app.UseForwardedHeaders(forwardingOptions);

// <-- End

We can also avoid the forwarding middleware by modifying the code of the app:

  1. Open App.razor and replace <base href="@NavigationManager.BaseUri" /> with <base href="/" />
  2. Open AccountController.cs and delete [HttpPost] before CurrentUser

This option also works but I like the forwarding middleware more.

2 Likes

Thank you for all your assistance. Unfortunately I still have issues:

I put in the forwarding middleware exactly as you specified and also added your 301 redirect to the nginx conf exactly as you have it, in addition to the proxy pass section which is as I posted before, taken from the Microsoft instructions. Putting some breakpoints in GetAuthenticationStateAsync, I get what I think it expected behavior. The uri appears correct.

running locally, https:\localhost (so going through the nginx proxy pass), I get:
uri = https:\localhost/Account/CurrentUser and then a SSL Exception in HttpRequestMessage(HttpMethod.Post, uri)). The inner exception says incorrect certificate, makes sense, the certificate is for "example.c0m".

running remotely, example.c0m:
I get
uri = https:\example.c0m/Account/CurrentUser
And then it hangs in the httpClient.SendAsync call, eventually throwing a timeout exception.

I also tried your previous suggestion of replacing the HttpMethod.Post with Get, and removing the [HttpPost] before CurrentUser. Also tried replacing "@NavigationManager.BaseUri" with "/". Nothing works.

I hear what you're saying, that you have production code working for years with security running behind nginx, so I am at a loss. Running Ubuntu 24.04, nginx/1.18.0, nginx conf exactly as in the microsoft docs with the 301 redirect for http port 80 that you posted above.

Edit: It wouldn't let me post links so I put those goofy back slashes in. Sorry

Let's stick with the forwarding middleware. What is the value rendered for <base href="@NavigationManager.BaseUri" /> that you see when browsing with https://example.com? Does it have https or http? If it is still http then the middleware isn't working properly for some reason.

Did you try adding some form of logging? Adding Console.WriteLine in SecurityService and AccountController would show what code is executed.

In any case here is my test application running on our Ubuntu server: https://next.radzen.com

You can login with test@example.com / Te$tpa$$1

Here is the source.EmptyApp.zip (254.7 KB)

Is your certificate self-signed? This won't work for sure and you would have to disable certificate validation.

What is the value rendered for <base href="@NavigationManager.BaseUri" /> that you see when browsing with https://example.com? Does it have https or http?

I downloaded your test application and ran it on my ubuntu server.

Running your EmptyApp code, I put a breakpoint on line 98 in SecurityService.cs, and the value for BaseUri is "https://mydomain.com/". And then it gets hung up in the next line with the httpClient.SendAsync.

Is your certificate self-signed?

No.

What is output in the console? Is the CurrentUser method of AccountController executed? Any exception after SendAsync?

What is output in the console? Is the CurrentUser method of AccountController executed? Any exception after SendAsync?

info: System.Net.Http.HttpClient.EmptyApp.LogicalHandler[100]
Start processing HTTP request POST https://mydomain.com/Account/CurrentUser
info: System.Net.Http.HttpClient.EmptyApp.ClientHandler[100]
Sending HTTP request POST https://mydomain.com/Account/CurrentUser

No exception

The last thing I can suggest is to check what happens in the CurrentUser method. It seems to be properly requested and should return a successful response. You can add logging to see what happens. For example this:

[HttpPost]
public ApplicationAuthenticationState CurrentUser()
{
    try
    {
        Console.WriteLine("Entering CurrentUser");

        var result = new ApplicationAuthenticationState
        {
            IsAuthenticated = User.Identity.IsAuthenticated,
            Name = User.Identity.Name,
            Claims = User.Claims.Select(c => new ApplicationClaim { Type = c.Type, Value = c.Value })
        };

        Console.WriteLine(JsonSerializer.Serialize(result, new JsonSerializerOptions { WriteIndented = true }));

        return result;
    }
    catch (Exception ex)
    {
        Console.WriteLine($"Exception: {ex}");

        return null;
    }
}

The output should be something like this: