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.41k stars 10k forks source link

[Blazor]AuthenticationState inside component return Authenticated as true after SignOut #57368

Closed mghammer40 closed 2 months ago

mghammer40 commented 2 months ago

Is there an existing issue for this?

Describe the bug

Hello, I created simple blazor project with cookie authentication to visualize the problem. Inside I have only four pages Home, Start, Login and Logout. Additionally I modified MainLayout to show login and logout links.

MainLayout.razor
<AuthorizeView>
    <Authorized>
        <a href="/logout">logout</a>
    </Authorized>
    <NotAuthorized>
        <a href="/login">Login</a>
    </NotAuthorized>
</AuthorizeView>
@Body

Simple home page

Home.razor
@page "/"

<PageTitle>Home</PageTitle>
<div class="main-content">
    <NavLink class="welcome-card" href="/Start">
        <div class="welcome-card-text">Start</div>
        <div class="welcome-card-back"></div>
    </NavLink>
</div>

Start page with EditForm and AuthorizeView

Start.razor
<div class="row">
    <EditForm Model="simple" FormName="loginForm" Context="editContext">
        <div class="col-lg-4 offset-lg-1 pt-2 pb-2">
            <InputText @bind-Value="simple.Name" class=" form-control" autocomplete="off">Name</InputText>
            <div class=" mb-3 d-grid gap-2">
                <AuthorizeView Context="authContext">
                <NotAuthorized>
                    <button @onclick="(() => goToLogin(editContext))">
                        Login 
                    </button>
                </NotAuthorized>
                <Authorized>
                    <button @onclick="(() => authButton(authContext))">
                        check 
                    </button>
                </Authorized>
                </AuthorizeView>
            </div>
        </div>
    </EditForm>
</div>

@code {
    private EditContext? editContext;
    public Simple simple { get; set; } = new();

    protected override async Task OnInitializedAsync()
    {
        var authState = AuthenticationStateProvider.GetAuthenticationStateAsync();
        await base.OnInitializedAsync();
    }
    public async Task goToLogin(EditContext editContext)
    {
        nav.NavigateTo("/login");
    }
    public async Task authButton(AuthenticationState authenticationState)
    {
        var authState = AuthenticationStateProvider.GetAuthenticationStateAsync();
    }
}

Login with simple cookie authentication

Login.razor
<div class="row">
    <EditForm Model="model" OnValidSubmit="Authenticate" FormName="LoginFrom">
        <div class="col-lg-4 offset-lg-1 pt-2 pb-2">
            <ObjectGraphDataAnnotationsValidator />

            <div class=" mb-3">
                <label>User</label>
                <InputText @bind-Value="model.UserName" class=" form-control" autocomplete="off">User</InputText>
                <ValidationMessage For="() => model.UserName" />

            </div>
            <div class=" mb-3">
                <label>Password</label>
                <InputText @bind-Value="model.Password" class=" form-control" autocomplete="off">Password </InputText>
                <ValidationMessage For="() => model.Password" />

            </div>
            <div class=" mb-3 d-grid gap-2">
                <button>
                    Login
                </button>
            </div>
        </div>
    </EditForm>
</div>
@code {
    [CascadingParameter]
    public HttpContext? httpContext { get; set; }
    [SupplyParameterFromForm]
    public LoginVModel model { get; set; } = new();
    public async Task Authenticate()
    {
        try
        {

            if (string.IsNullOrWhiteSpace(model.UserName) || string.IsNullOrWhiteSpace(model.Password))
            {
                return;
            }
            UserAccount? userAccount = UserAccount.userAccounts.Where(x => x.Name == model.UserName).FirstOrDefault(); //skokContext.userAccounts.Where(x => x.Name == model.UserName).FirstOrDefault();
            if (userAccount is null || userAccount.Pass != model.Password)
            {
                return;
            }
            var claims = new List<Claim>{
                new Claim(ClaimTypes.Name, model.UserName)
            };
            var userAccountPolicy = UserAccountPolicy.userAccountPolicy.Where(x => x.Id == userAccount.Id && x.Enabled).ToList<UserAccountPolicy>();
            foreach (UserAccountPolicy accountPolicy in userAccountPolicy)
            {
                claims.Add(new Claim(accountPolicy.UserPolicy, "true"));
            }

            ClaimsIdentity identity = new(claims, CookieAuthenticationDefaults.AuthenticationScheme);
            ClaimsPrincipal principal = new ClaimsPrincipal(identity);

            await httpContext?.SignInAsync(principal);
            nav.NavigateTo("/start");
        }
        catch (Exception ex)
        {
            throw;
        }
    }

}

Logout

Logout.razor
div class="row">
    <div class="col-12">
        <div class="card">
            <div class="card-body flex-column">
                <div class="text-center mt-2">
                    <span >Logout</span>
                </div>
            </div>
        </div>
    </div>
</div>
@code {
    [CascadingParameter]
    public HttpContext? httpContext { get; set; }

    protected override async Task OnInitializedAsync()
    {
        await base.OnInitializedAsync();
        if(httpContext.User.Identity.IsAuthenticated)
        {
            await httpContext.SignOutAsync();
            nav.NavigateTo("/");
        }
    }
}

Program.cs

     public class Program
   {
       public static void Main(string[] args)
       {
           var builder = WebApplication.CreateBuilder(args);

           // Add services to the container.
           builder.Services.AddRazorComponents()
               .AddInteractiveServerComponents();

           builder.Services.AddAuthorization(configure =>
           {
               foreach (var userPolicy in UserPolicy.GetPolices())
               {
                   configure.AddPolicy(userPolicy, cfg => cfg.RequireClaim(userPolicy, "true"));
               }
           });
           builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie(options =>
           {
               options.Cookie.Name = "auth";
               options.Cookie.MaxAge = TimeSpan.FromMinutes(120);
               options.AccessDeniedPath = "/";

           });
           builder.Services.AddCascadingAuthenticationState();

           builder.Services.AddMvc();

           var app = builder.Build();

           // Configure the HTTP request pipeline.
           if (!app.Environment.IsDevelopment())
           {
               app.UseExceptionHandler("/Error", createScopeForErrors: true);
               // 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.MapRazorComponents<App>()
               .AddInteractiveServerRenderMode()
                .DisableAntiforgery();
           ;

           app.Run();
       }
   }

First I login using login page and I am redirected to start page. Then I click on logout from start page. I am redirected to home page. Next I need to click fast on start page link and I am redirected to start page. Now authentication on page is split in two. Mainlayout showing correct login button as it is NotAuthorized, but Start.razor component is showing as authorized and is returning AuthenticationState as authorized. This dont occur when I wait few seconds after logout and clicking start and page is rendering corectly as NotAuthorized. I'm new to Blazor and I'm wonder if this is intentional or not.

Expected Behavior

AuthorizeView inside component should refresh when accessed?

Steps To Reproduce

  1. Login
  2. Click logout
  3. Click start
  4. The page is divided into two parts with Authorized and NotAuthorized view at the same time

Exceptions (if any)

No response

.NET Version

8.0.7

Anything else?

No response

javiercn commented 2 months ago

@mghammer40 thanks for contacting us.

Have you implemented a RevalidatingAuthenticationStateProvider? See https://learn.microsoft.com/en-us/aspnet/core/blazor/security/server/?view=aspnetcore-8.0&tabs=visual-studio#manage-authentication-state-in-blazor-web-apps and https://learn.microsoft.com/en-us/aspnet/core/blazor/security/server/?view=aspnetcore-8.0&tabs=visual-studio#additional-security-abstractions for details.

dotnet-policy-service[bot] commented 2 months ago

Hi @mghammer40. We have added the "Needs: Author Feedback" label to this issue, which indicates that we have an open question for you before we can take further action. This issue will be closed automatically in 7 days if we do not hear back from you by then - please feel free to re-open it if you come back to this issue after that time.

dotnet-policy-service[bot] commented 2 months ago

This issue has been automatically marked as stale because it has been marked as requiring author feedback but has not had any activity for 4 days. It will be closed if no further activity occurs within 3 days of this comment. If it is closed, feel free to comment when you are able to provide the additional information and we will re-investigate.

See our Issue Management Policies for more information.