aspnetcore icon indicating copy to clipboard operation
aspnetcore copied to clipboard

Blazor 8 reverse proxy signin-oidc not working.

Open silentdevnull opened this issue 1 year ago • 10 comments

Is there an existing issue for this?

  • [X] I have searched the existing issues

Describe the bug

I'm trying to get blazor 8 signin to work behind my nginx reverse proxy. Everything works but the signin. It comes back with 404 not found. Everything works fine on local host and if I make the application work on the root of the domain it works to.

I have added all of the Forwarded Headers from the documentation to my programs.cs and my application configuration in nginx.

I have updated the call back path in appsettings.json

I tried adding a page options route and that didn't help.

builder.Services.AddRazorPages()
    .AddRazorPagesOptions(options =>
    {
        options.Conventions.AddPageRoute("/app1/signin-oidc", "/signin-oidc");
    });

My sign uses Azure Ad.

Here is basic layout of my domain setup.

https://apps.example.com (nginx reverse proxy)
https://apps.exmaple.com/apps1/ (first blazor application)
https://apps.example.com/apps2/ (second blazor application)

If I set the location for app1 to work on / then everything works fine.

I'm unsure if I need to do something different to make the page route or if I'm missing a configuration altogether.

I love many of the new changes in Blazor 8, but so much of it has been very frustrating to the point that I'm learning it all over again.

Expected Behavior

blazor application accepts /app1/signin-oidc and not give a 404.

Steps To Reproduce

No response

Exceptions (if any)

No response

.NET Version

8.0..1

Anything else?

No response

silentdevnull avatar Feb 15 '24 15:02 silentdevnull

Are you using UsePathBase? Can you show us your full Program.cs?

MackinnonBuck avatar Feb 15 '24 17:02 MackinnonBuck

Are you using UsePathBase? Can you show us your full Program.cs?

Thank you for the reply

No I'm not using path base.

Program.cs


var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorComponents()
    .AddInteractiveServerComponents();

builder.Services.AddMicrosoftIdentityWebAppAuthentication(builder.Configuration);
builder.Services.AddHttpContextAccessor();
builder.Services.AddControllersWithViews(options =>
{
    var policy = new AuthorizationPolicyBuilder()
        .RequireAuthenticatedUser()
        .Build();
    options.Filters.Add(new AuthorizeFilter(policy));
}).AddMicrosoftIdentityUI();

builder.Services.AddRazorComponents()
    .AddInteractiveServerComponents();

//builder.Services.AddRazorPages();
builder.Services.AddRazorPages()
    .AddRazorPagesOptions(options =>
    {
        options.Conventions.AddPageRoute("/app1/signin-oidc", "/signin-oidc");
    });

builder.Services.Configure<ForwardedHeadersOptions>(options =>
{
    options.ForwardedHeaders =
        ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
    
});

builder.Services.AddServerSideBlazor()
    .AddMicrosoftIdentityConsentHandler();
builder.Services.AddCascadingAuthenticationState();

builder.Services.AddQuickGridEntityFrameworkAdapter();


builder.Services.AddHttpContextAccessor();

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error", createScopeForErrors: true);
    app.UseHsts();
}


app.UseForwardedHeaders();
app.UseAuthentication();
app.UseAuthorization();
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseAntiforgery();
app.MapControllers();
app.MapRazorComponents<App>()
    .AddInteractiveServerRenderMode();

app.Run();

silentdevnull avatar Feb 15 '24 18:02 silentdevnull

@silentdevnull thanks for the additional details.

If you are hosting the page in a "virtual directory" (not on the root of the domain) you need to call UseRouting explicitly and call UsePathBase("<<prefix>>") before UseRouting.

UsePathBase will consume the prefix, and UseRouting will run the routing logic after use path base has been called (otherwise it would run before use path base and result in a 404).

All this should happen before UseAuthentication (at the beginning of the pipeline)

javiercn avatar Feb 19 '24 13:02 javiercn

@silentdevnull thanks for the additional details.

If you are hosting the page in a "virtual directory" (not on the root of the domain) you need to call UseRouting explicitly and call UsePathBase("<<prefix>>") before UseRouting.

UsePathBase will consume the prefix, and UseRouting will run the routing logic after use path base has been called (otherwise it would run before use path base and result in a 404).

All this should happen before UseAuthentication (at the beginning of the pipeline)

Thank you for the informaiton.

I will add it right under the builder build line.

It should look a little something like this?

app.UsePathBase("/app1/");

Would it make a different that it running as a docker container, and the reverse proxy is in another container?

silentdevnull avatar Feb 19 '24 15:02 silentdevnull

@silentdevnull no, it won't matter.

What matters is that when the request is arriving to Kestrel is for /app1/signin-oidc and the route you are mapping is /signin-oidc (which is why it works when you run it from /).

app.UsePathBase("/app1");
app.UseRouting();

Will make it so that the request is adjusted to remove the "/app1/" prefix from the Path (and in turn set it on the base path) so that then routing gets to see "/signin-oidc" and matches your page.

javiercn avatar Feb 20 '24 13:02 javiercn

I'm still getting a 404 error on the return.

I have tried it the following two ways.

app.UsePathBase("/app1");
app.UsePathBase("/app1/");

I have tried changing the Callbackpath in the appsettings.json as well.

"CallbackPath": "/app1/signin-oidc"
"CallbackPath": "/signin-oidc"

Here is my current configuration file incase I have something out of order.

program.cs

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorComponents()
    .AddInteractiveServerComponents();

builder.Services.AddMicrosoftIdentityWebAppAuthentication(builder.Configuration);
builder.Services.AddHttpContextAccessor();
builder.Services.AddControllersWithViews(options =>
{
    var policy = new AuthorizationPolicyBuilder()
        .RequireAuthenticatedUser()
        .Build();
    options.Filters.Add(new AuthorizeFilter(policy));
}).AddMicrosoftIdentityUI();

builder.Services.AddRazorComponents()
    .AddInteractiveServerComponents();

//builder.Services.AddRazorPages();
builder.Services.AddRazorPages()
    .AddRazorPagesOptions(options =>
    {
        options.Conventions.AddPageRoute("/app1/signin-oidc", "/signin-oidc");
    });

builder.Services.Configure<ForwardedHeadersOptions>(options =>
{
    options.ForwardedHeaders =
        ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
    
});

builder.Services.AddServerSideBlazor()
    .AddMicrosoftIdentityConsentHandler();
builder.Services.AddCascadingAuthenticationState();

builder.Services.AddQuickGridEntityFrameworkAdapter();



builder.Services.AddDbContextFactory<ApplicationContext>(
    options =>
        options.UseSqlServer(connectionString));

builder.Services.AddHttpContextAccessor();

var app = builder.Build();



if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error", createScopeForErrors: true);
    app.UseHsts();
}

//app.UsePathBase("/app1");
app.UsePathBase("/app1/");
app.UseRouting();
app.UseForwardedHeaders();
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseAntiforgery();
app.MapControllers();
app.UseAuthentication();
app.UseAuthorization();
app.MapRazorComponents<App>()
    .AddInteractiveServerRenderMode();
app.Run();

appsettings.json

{
  "AzureAd": {
    "Instance": "https://login.microsoftonline.com/",
    "TenantId": " ",
    "ClientId": " ",
    "ClientSecret": " ",
    "Domain": " ",
    "CallbackPath": "/signin-oidc"
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "Databases": {
    "Application": {
      "SERVERNAME": "",
      "DATABASENAME": "",
      "USERNAME": "",
      "PASSWORD": ""
    }
  }
}

Reverse proxy configuration


server {
        server_name apps.example.com;
        access_log /var/log/nginx/apps-access.log;
        error_log /var/log/nginx/apps-error.log;
        listen       443 ssl;

        proxy_buffers 8 2m;
        proxy_buffer_size 12m;
        proxy_busy_buffers_size 12m;
        client_body_buffer_size 100m;
        client_max_body_size 100m;
        large_client_header_buffers 8 64k;


        location /app1/ {
                proxy_http_version 1.1;
                proxy_pass https://roster:5001/;
                proxy_redirect off;
                proxy_buffering off;
                proxy_ssl_protocols TLSv1.2;
                proxy_set_header HOST $host;
                proxy_set_header X-Real-IP $remote_addr;
                proxy_headers_hash_max_size 512;
                proxy_headers_hash_bucket_size 128;
                proxy_ssl_verify off;
                proxy_set_header X-Forwarded-Proto $scheme;
                proxy_set_header X-Forwarded-Host $server_name;
                proxy_set_header Upgrade $http_upgrade;
                proxy_set_header Connection "upgrade";
                proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
                proxy_set_header X-Forwarded-Proto $scheme;
                fastcgi_buffers 16 16k;
                fastcgi_buffer_size 32k;
                sub_filter 'action="/'  'action="/app1/';
                sub_filter 'href="/'  'href="/app1/';
                sub_filter 'src="/'  'src="/app1/';
                sub_filter_once off;
        }
} 

Thank you,

silentdevnull avatar Feb 21 '24 21:02 silentdevnull

Maybe this is related: https://github.com/dotnet/aspnetcore/issues/51929#issuecomment-1959110621

audacity76 avatar Feb 22 '24 10:02 audacity76

Maybe this is related: #51929 (comment)

Sure looks close to the same type of issue I'm having.

My project was like a 2 or 3 day project that now becoming 3 week project for something that was going to be simple.

silentdevnull avatar Feb 22 '24 21:02 silentdevnull

I have the similar issue with google auth behind reverse proxy on kubernetes.

When I'm running locally through HTTPS

  • No problem, I can successfully authenticate with google using app id and secret from console.cloud.google.com for my app.
  • I can log in, log out, no problems at all

When I'm running behind reverse proxy (nginx ingress, kubernetes)

  • I am hosting my app on the cluster over http on the pod.
  • To call the google endpoint so it thinks I am calling from https from my domain, I had to add
builder.Services.Configure<ForwardedHeadersOptions>(options =>
{
    options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
    options.KnownNetworks.Clear();
    options.KnownProxies.Clear();
});

and

app.UseForwardedHeaders();

that enabled me to trigger google's sign-in form on googles side. (without it, I had error about invalid url not being on my Authorized redirect URIs and I couldn't even reach google login site)

Of course I have set up the ingress nginx configuration to handle headers properly: My config map fragment

apiVersion: v1
kind: ConfigMap
metadata:
  name: my-app-forward-headers
  namespace: my-namespace
data:
  Host: "$http_host"
  X-Forwarded-For: "$proxy_add_x_forwarded_for"
  X-Forwarded-Proto: "$scheme"
  X-Real-IP: "$remote_addr"

My ingress config fragment

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: my-app-ingress
  namespace: my-namespace
  annotations:
    kubernetes.io/ingress.class: "nginx"
    cert-manager.io/cluster-issuer: letsencrypt-cluster-issuer
    nginx.ingress.kubernetes.io/proxy-set-headers: "my-namespace/my-app-forward-headers"
    nginx.ingress.kubernetes.io/proxy-buffer-size: 8k

In theory that should do it all. But for some reason, after google redirects back to my app's /signin-google callback, I get HTTP ERROR 500

      Executed endpoint 'HTTP: POST /Account/PerformExternalLogin'
info: Microsoft.AspNetCore.Hosting.Diagnostics[2]
      Request finished HTTP/1.1 POST https://<--my-domain-->/Account/PerformExternalLogin - 302 0 - 74.2793ms
info: Microsoft.AspNetCore.Hosting.Diagnostics[1]
      Request starting HTTP/1.1 GET http://<--my-domain-->/signin-google?state=CfDJ8HT<--rest-of-the-code-->LYYVZDg&code=4%2F0AeaYSH<--rest-of-the-code-->xT4Pw&scope=email+profile+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.email+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.profile+openid&authuser=0&prompt=none - - -
info: Microsoft.AspNetCore.Authentication.Google.GoogleHandler[4]
      Error from RemoteAuthentication: OAuth token endpoint failure: redirect_uri_mismatch;Description=Bad Request.
fail: Microsoft.AspNetCore.Server.Kestrel[13]
      Connection id "0HN1L5FOPQUD6", Request id "0HN1L5FOPQUD6:00000008": An unhandled exception was thrown by the application.
      Microsoft.AspNetCore.Authentication.AuthenticationFailureException: An error was encountered while handling the remote login.
       ---> Microsoft.AspNetCore.Authentication.AuthenticationFailureException: OAuth token endpoint failure: redirect_uri_mismatch;Description=Bad Request
         --- End of inner exception stack trace ---
         at Microsoft.AspNetCore.Authentication.RemoteAuthenticationHandler`1.HandleRequestAsync()
         at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)
         at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpProtocol.ProcessRequests[TContext](IHttpApplication`1 application)

However

I figured out that wen I add the

app.UseAuthentication();
app.UseAuthorization();

which do not appear by default in blazor template with authentication enabled, the problem goes away and I am able to successfully login.

BUT

Then, when I try to log out with a form, I get antiforgery tokken error (400, bad request). Having app.UseAuthentication(); app.UseAuthorization(); added also breaks localhost on https.

Microsoft.AspNetCore.Http.BadHttpRequestException: Invalid anti-forgery token found when reading parameter "string returnUrl" from the request body as form.
 ---> Microsoft.AspNetCore.Antiforgery.AntiforgeryValidationException: The provided antiforgery token was meant for a different claims-based user than the current user.
   at Microsoft.AspNetCore.Antiforgery.DefaultAntiforgery.ValidateTokens(HttpContext httpContext, AntiforgeryTokenSet antiforgeryTokenSet)

My logout form url for reference:

<AuthorizeView>
    <Authorized>
        <div class="px-3 pb-2">@context.User.Identity?.Name</div>
        <form action="Account/Logout" method="post">
            <AntiforgeryToken/>
            <input type="hidden" name="ReturnUrl" value="@_currentUrl"/>
            <button type="submit" class="nav-link">
                <span class="bi bi-arrow-bar-left-nav-menu" aria-hidden="true"></span> Logout
            </button>
        </form>
    </Authorized>
</AuthorizeView>

I think there might be some scheme issue (not passing https instead of http scheme somewhere down the line). And adding app.UseAuthentication(); app.UseAuthorization(); making it work for google auth but making it unable to handle anti forgery tokens melts my brain.

There should be some tutorial in docs how to setup a 3rd party auth behind reverse proxy, because in my opinion, it's impossible to do for this moment.

tososm avatar Feb 24 '24 12:02 tososm

I managed to overcome this issue by hosting my app on https with my self-signed certificate internally on the pod. So nginx ingress uses Letsencrypt for the real certificate, and I've generated my own self-signed certificate just for the sake of running my blazor app in the pod on https too. This solves the strange behavior when trying to log in through 3rd party oidc.

I've removed the:

app.UseAuthentication();
app.UseAuthorization();

I've created self signed certificate with openssl:

openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365 -nodes -subj '/CN=myapp.internal'

If you are on windows and want to install openssl, use chocolatey choco install openssl Now convert the certificate to pfx format:

openssl pkcs12 -export -out cert.pfx -inkey key.pem -in cert.pem

Now add cert.pfx to your blazor application. (You will need to set a password for it when you run the command above, so you will have it)

builder.WebHost.ConfigureKestrel(options =>
{
    options.ListenAnyIP(4431, listenOptions =>
    {
        listenOptions.UseHttps("cert.pfx", "your_password_you_used_for_generating_pfx");
    });
});

Now your app runs on your self-signed certificate through https. One thing to make all of this work is to tell your nginx ingress to talk to your service through https.

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: your-ingress
  namespace: your-namespace
  annotations:
    kubernetes.io/ingress.class: "nginx"
    cert-manager.io/cluster-issuer: letsencrypt-cluster-issuer
    nginx.ingress.kubernetes.io/proxy-set-headers: "your-namespace/your-config-forward-headers"
    nginx.ingress.kubernetes.io/proxy-buffer-size: 8k
    nginx.ingress.kubernetes.io/backend-protocol: "HTTPS"

The most important thing is to add the HTTPS annotation: nginx.ingress.kubernetes.io/backend-protocol: "HTTPS" The fact that your pod's self-signed certificate is not signed by any CA, does not matter.

There might be a bug in Blazor that does not pass the https scheme from the reverse proxy properly, when running on a pod without ssl.

sikora507 avatar Feb 24 '24 18:02 sikora507

We see a lot of code in the comments, but to investigate this further we will need a full repro project hosted on GitHub.

halter73 avatar Feb 27 '24 17:02 halter73

options.Conventions.AddPageRoute("/app1/signin-oidc", "/signin-oidc");

This seems suspect though. /signin-oidc is supposed to be handled by the authentication middleware, please try to remove any extraneous code that you can.

The other thing that we forgot to mention earlier is that app.UsePathBase("/app1") needs to before not just app.UseRouting() which should also be before app.UseAuthentication().

halter73 avatar Feb 27 '24 17:02 halter73

options.Conventions.AddPageRoute("/app1/signin-oidc", "/signin-oidc");

This seems suspect though. /signin-oidc is supposed to be handled by the authentication middleware, please try to remove any extraneous code that you can.

The other thing that we forgot to mention earlier is that app.UsePathBase("/app1") needs to before not just app.UseRouting() which should also be before app.UseAuthentication().

I removed it and it didn't change the issue. I will create a repo to share within the next few days.

silentdevnull avatar Mar 01 '24 10:03 silentdevnull

options.Conventions.AddPageRoute("/app1/signin-oidc", "/signin-oidc");

This seems suspect though. /signin-oidc is supposed to be handled by the authentication middleware, please try to remove any extraneous code that you can.

The other thing that we forgot to mention earlier is that app.UsePathBase("/app1") needs to before not just app.UseRouting() which should also be before app.UseAuthentication().

Did you happen to find a way forward with this issue? I am currently running into the same type of problem.

completej avatar Apr 30 '24 19:04 completej

Same problem, I've spent a week looking into this...

Everything works fine in localhost and breaks behind a proxy using http.

I'm using Keycloak.

Browser Server

  • Nginx -- MyAppDocker -- KeycloakDocker

Open a new incognito tab. Enter the https://mydomain.com/. Everything works fine, the blazor is using websokets Go to a page with [Authorize] like https://mydomain.com/private I'm redirected to login, then redirected to the /singin-oidc Then I get my cookie but websokets stop working and Blazor swiches to longpooling Somwhere in the logs Antiforgery token could not be validated.

The antiforgery token could not be decrypted. The key was not found in the key ring.

The site works well if I dont go to protected pages but switches to longpooling. Also this only affects the /_blazor request for some reason.

So please, reopen this issue

vgallegob avatar Jul 12 '24 07:07 vgallegob