dotnet / aspnetcore

ASP.NET Core is a cross-platform .NET framework for building modern cloud-based web applications on Windows, Mac, or Linux.
https://asp.net
MIT License
35.37k stars 9.99k forks source link

Postback on blazor InteractiveServer doesn't work #53950

Closed Lesterdor closed 8 months ago

Lesterdor commented 8 months ago

Is there an existing issue for this?

Describe the bug

Hello community,

I have an interactive login page in the Blazor Server context (.NET 8).

I would like to authenticate with a cookie and I am facing the problem that the cookie cannot be set with the <EditForm> because the postback behavior is handled differently in Blazor.

I'm unsure if it's a bug, but I can't find any documentation on how to realize the intention:

Login.razor (html form) - works as expected and a cookie is set:

@page "/login"
@attribute [AllowAnonymous]
<h1>Login</h1>

 <form method="post" action="/Auth">
    <button type="submit">Login</button>
</form> 

Login.razor (EditForm) - No cookie is set

@page "/login"
@using System.ComponentModel.DataAnnotations
@attribute [AllowAnonymous]
@inject IHttpClientFactory ClientFactory;
<h1>Login</h1>

<EditForm Model="Model" FormName="Login" OnValidSubmit="Submit" method="post">
    <DataAnnotationsValidator/>
    <ValidationSummary/>
    <InputText @bind-Value="Model.Username"/>
    <ValidationMessage For="@(()=> Model.Username)"/>
    <InputText type="password" @bind-Value="Model.Password"/>
    <ValidationMessage For="@(() => Model.Password)" />
    <button type="submit">Login</button>
</EditForm>

@code {

    public class LoginModel
    {
        [Required]
        public string Username { get; set; }

        [Required]
        [DataType(DataType.Password)]
        public string Password { get; set; }
    }

    [SupplyParameterFromForm]
    private LoginModel Model { get; set; } = new();

    private async Task Submit()
    {
        var httpClient = ClientFactory.CreateClient();
        httpClient.BaseAddress = new Uri("https://localhost:7099");
        var response = await httpClient.PostAsync("/Auth", null).ConfigureAwait(false);
    }

}

Program.cs

    public class Program
    {
        public static void Main(string[] args)
        {
            var builder = WebApplication.CreateBuilder(args);
            builder.Services.AddRazorComponents().AddInteractiveServerComponents();
            builder.Services.AddControllers();
            builder.Services.AddHttpClient();
            builder.Services.AddCascadingAuthenticationState();
            builder.Services.AddAuthentication(IdentityConstants.ApplicationScheme).AddIdentityCookies();
            builder.Services.ConfigureApplicationCookie(cfg => {cfg.LoginPath = "/login";});

            var app = builder.Build();

            // Configure the HTTP request pipeline.
            if (!app.Environment.IsDevelopment())
            {
                app.UseExceptionHandler("/Error");
                // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
                app.UseHsts();
            }
            app.UseHttpsRedirection();
            app.UseStaticFiles();
            app.UseAntiforgery();
            app.MapRazorComponents<App>() .AddInteractiveServerRenderMode();
            app.MapControllers();
            app.MapPost("/Auth", async context =>
            {
                var identity = new ClaimsIdentity(new[]
                {
                    new Claim(ClaimTypes.Name, "Foo"),
                    new Claim(ClaimTypes.NameIdentifier, "Bar")
                }, "cookie_example");
                await context.SignInAsync(IdentityConstants.ApplicationScheme, new ClaimsPrincipal(identity));
            });

            app.Run();
        }
    }

Routes.razor

<Router AppAssembly="typeof(Program).Assembly">
    <Found Context="routeData">
        <AuthorizeRouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)" />
        <FocusOnNavigate RouteData="routeData" Selector="h1" />
    </Found>
</Router>

App.razor

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <base href="/" />
    <link rel="stylesheet" href="app.css" />
    <HeadOutlet @rendermode="InteractiveServer" />
</head>

<body>
    <Routes @rendermode="InteractiveServer" />
    <script src="_framework/blazor.web.js"></script>
</body>

</html>

_Imports.razor

@using System.Net.Http
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using static Microsoft.AspNetCore.Components.Web.RenderMode
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.JSInterop
@using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Components.Authorization
@attribute [Authorize]

The Blazor Web App Template with the configuration: Authentication Type: Individual Accounts Interactive render mode: Server Interactivity location: Global

works because the render mode in App.razor is set to SSR in the case of "Account" pages.

Expected Behavior

No response

Steps To Reproduce

No response

Exceptions (if any)

No response

.NET Version

.NET 8

Anything else?

No response

javiercn commented 8 months ago

@Lesterdor thanks for contacting us.

This is by design. It's fundamentally impossible to have an interactive server component, that's why the auth pages on the templates set the render mode to static. We plan to provide a mechanism to force a component to render statically, but making HTTP semantics work over an interactive component is not something we'll ever do.

This is a dupe of https://github.com/dotnet/aspnetcore/issues/51046