Blazor 8 reverse proxy signin-oidc not working.
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
Are you using UsePathBase? Can you show us your full Program.cs?
Are you using
UsePathBase? Can you show us your fullProgram.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 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)
@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
UseRoutingexplicitly and callUsePathBase("<<prefix>>")beforeUseRouting.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 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.
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,
Maybe this is related: https://github.com/dotnet/aspnetcore/issues/51929#issuecomment-1959110621
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.
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.
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.
We see a lot of code in the comments, but to investigate this further we will need a full repro project hosted on GitHub.
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().
options.Conventions.AddPageRoute("/app1/signin-oidc", "/signin-oidc");This seems suspect though.
/signin-oidcis 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 justapp.UseRouting()which should also be beforeapp.UseAuthentication().
I removed it and it didn't change the issue. I will create a repo to share within the next few days.
options.Conventions.AddPageRoute("/app1/signin-oidc", "/signin-oidc");This seems suspect though.
/signin-oidcis 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 justapp.UseRouting()which should also be beforeapp.UseAuthentication().
Did you happen to find a way forward with this issue? I am currently running into the same type of problem.
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