最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

c# - Blazor interactive unauthorized .NET 8 - Stack Overflow

programmeradmin0浏览0评论

I am building a service that uses a mix of SSR and interactive server. The SSR portion is only used to login. Now after the login completes, any request to the backend fails as unauthorized, but if I print the HttpContext on the Blazor page (interactive server), the user is logged in.

Any idea about what is going on?

EDIT CODE:

To make the starting example: create a folder and once inside it run dotnet new mudblazor --interactivity Server --auth Individual

REPLACE: program.cs

using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using MudBlazor.Services;
using test2.Components;
using test2.Components.Account;
using test2.Data;

var builder = WebApplication.CreateBuilder(args);

// Add MudBlazor services
builder.Services.AddMudServices();

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

builder.Services.AddCascadingAuthenticationState();
builder.Services.AddScoped<IdentityUserAccessor>();
builder.Services.AddScoped<IdentityRedirectManager>();
builder.Services.AddScoped<AuthenticationStateProvider, IdentityRevalidatingAuthenticationStateProvider>();

builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.Configuration["ApiUrl"] ?? "http://localhost:5009/") });

builder.Services.AddAuthentication(options =>
    {
        options.DefaultScheme = IdentityConstants.ApplicationScheme;
        options.DefaultSignInScheme = IdentityConstants.ExternalScheme;
    })
    .AddIdentityCookies();

var connectionString = builder.Configuration.GetConnectionString("DefaultConnection") ?? throw new InvalidOperationException("Connection string 'DefaultConnection' not found.");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlite(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddControllers();

builder.Services.AddIdentityCore<ApplicationUser>(options =>
    {
        options.SignIn.RequireConfirmedAccount = false;
        options.Password.RequireDigit = false;
        options.Password.RequireLowercase = false;
        options.Password.RequireNonAlphanumeric = false;
        options.Password.RequireUppercase = false;
        options.Password.RequiredLength = 1;
    })
    .AddEntityFrameworkStores<ApplicationDbContext>()
    .AddSignInManager()
    .AddDefaultTokenProviders();

builder.Services.AddSingleton<IEmailSender<ApplicationUser>, IdentityNoOpEmailSender>();

// Add this after the authentication configuration
builder.Services.AddAuthorization();

var app = builder.Build();

// Configure the HTTP request pipeline.

    app.UseMigrationsEndPoint();



app.UseStaticFiles();
app.UseAntifery();
app.UseAuthentication();
app.UseAuthorization();

app.MapRazorComponents<App>()
    .AddInteractiveServerRenderMode();
    
// Add additional endpoints required by the Identity /Account Razor components.
app.MapAdditionalIdentityEndpoints();

app.MapControllers();

app.Run();

NavMenu.razor:

@implements IDisposable

@inject NavigationManager NavigationManager

<MudNavMenu>
    <MudNavLink Href="" Match="NavLinkMatch.All" Icon="@Icons.Material.Filled.Home">Home</MudNavLink>
    <MudNavLink Href="auth" Match="NavLinkMatch.Prefix" Icon="@Icons.Material.Filled.Lock">Auth Required</MudNavLink>
    <AuthorizeView>
        <Authorized>
            <MudNavLink Href="test" Match="NavLinkMatch.Prefix" Icon="@Icons.Material.Filled.Person">test</MudNavLink>
            <MudNavLink Href="auth-debug" Match="NavLinkMatch.Prefix" Icon="@Icons.Material.Filled.List">debug</MudNavLink>
        </Authorized>
        <NotAuthorized>
            <MudNavLink Href="Account/Register" Match="NavLinkMatch.Prefix" Icon="@Icons.Material.Filled.Person">Register</MudNavLink>
            <MudNavLink Href="Account/Login" Match="NavLinkMatch.Prefix" Icon="@Icons.Material.Filled.Password">Login</MudNavLink>
        </NotAuthorized>
    </AuthorizeView>
</MudNavMenu>


@code {
    private string? currentUrl;

    protected override void OnInitialized()
    {
        currentUrl = NavigationManager.ToBaseRelativePath(NavigationManager.Uri);
        NavigationManager.LocationChanged += OnLocationChanged;
    }

    private void OnLocationChanged(object? sender, LocationChangedEventArgs e)
    {
        currentUrl = NavigationManager.ToBaseRelativePath(e.Location);
        StateHasChanged();
    }

    public void Dispose()
    {
        NavigationManager.LocationChanged -= OnLocationChanged;
    }
}

create a Testcontroller.cs at the same level as Program.cs:

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

[ApiController]
[Route("api/")]
public class TestController : ControllerBase
{
    [HttpGet]
    [Authorize]
    [Route("test")]
    public IActionResult Get()
    {
        return Ok("Hello from the API!");
    }
}

create 2 new files, in the pages folder: testpage.razor:

    @page "/test"
    @inject HttpClient Http
    @inject ISnackbar Snackbar
    
    <MudText Typo="Typo.h4" Class="mb-4">API Test</MudText>
    
    <MudButton Color="Color.Primary" 
               Variant="Variant.Filled" 
               OnClick="GetTestData">
        Get Test Data
    </MudButton>
    
    @if (!string.IsNullOrEmpty(_apiResponse))
    {
        <MudPaper Class="pa-4 mt-4">
            <MudText>@_apiResponse</MudText>
        </MudPaper>
    }
    
    @code {
        private string _apiResponse = string.Empty;
    
        private async Task GetTestData()
        {
            try
            {
                _apiResponse = await Http.GetStringAsync("api/test");
                Snackbar.Add("Data retrieved successfully!", Severity.Success);
            }
            catch (Exception ex)
            {
                Snackbar.Add("Error fetching data: " + ex.Message, Severity.Error);
            }
        }
}

Debug.razor:

@page "/auth-debug"
@using System.Security.Claims
@using Microsoft.AspNetCore.Http
@inject IHttpContextAccessor HttpContextAccessor

<MudContainer MaxWidth="MaxWidth.Large" Class="mt-4">
    <MudCard>
        <MudCardHeader>
            <MudText Typo="Typo.h5">Authentication Debug Information</MudText>
        </MudCardHeader>
        <MudCardContent>
            <MudGrid>
                <MudItem xs="12">
                    <MudPaper Class="pa-4" Elevation="0">
                        <MudText Typo="Typo.subtitle1" Class="mb-2"><b>Basic Information</b></MudText>
                        <MudList Dense="true" T="string">
                            <MudListItem T="string">
                                <MudText>Is Authenticated: <b>@IsAuthenticated</b></MudText>
                            </MudListItem>
                            <MudListItem T="string">
                                <MudText>Auth Type: <b>@AuthType</b></MudText>
                            </MudListItem>
                            <MudListItem T="string">
                                <MudText>Is Admin: <b>@IsInAdminRole</b></MudText>
                            </MudListItem>
                        </MudList>
                    </MudPaper>
                </MudItem>

                <MudItem xs="12">
                    <MudPaper Class="pa-4" Elevation="0">
                        <MudText Typo="Typo.subtitle1" Class="mb-2"><b>Authorization Header</b></MudText>
                        <MudTextField Value="@AuthHeader" Label="Auth Header" ReadOnly="true" Lines="2" />
                    </MudPaper>
                </MudItem>

                <MudItem xs="12">
                    <MudPaper Class="pa-4" Elevation="0">
                        <MudText Typo="Typo.subtitle1" Class="mb-2"><b>Roles</b></MudText>
                        @if (Roles.Any())
                        {
                            <MudList Dense="true" T="string">
                                @foreach (var role in Roles)
                                {
                                    <MudListItem Icon="@Icons.Material.Filled.Group" T="string">@role</MudListItem>
                                }
                            </MudList>
                        }
                        else
                        {
                            <MudAlert Severity="Severity.Info">No roles found</MudAlert>
                        }
                    </MudPaper>
                </MudItem>

                <MudItem xs="12">
                    <MudExpansionPanels>
                        <MudExpansionPanel Text="Claims">
                            @if (Claims.Any())
                            {
                                <MudTable Items="Claims" Dense="true" Hover="true">
                                    <HeaderContent>
                                        <MudTh>Type</MudTh>
                                        <MudTh>Value</MudTh>
                                    </HeaderContent>
                                    <RowTemplate>
                                        <MudTd>@context.Type</MudTd>
                                        <MudTd>@context.Value</MudTd>
                                    </RowTemplate>
                                </MudTable>
                            }
                            else
                            {
                                <MudAlert Severity="Severity.Info">No claims found</MudAlert>
                            }
                        </MudExpansionPanel>
                    </MudExpansionPanels>
                </MudItem>
            </MudGrid>
        </MudCardContent>
        <MudCardActions>
            <MudButton Color="Color.Primary" OnClick="RefreshData">Refresh Data</MudButton>
        </MudCardActions>
    </MudCard>
</MudContainer>

@code {
    private bool IsAuthenticated => HttpContextAccessor.HttpContext?.User?.Identity?.IsAuthenticated ?? false;
    private string AuthType => HttpContextAccessor.HttpContext?.User?.Identity?.AuthenticationType ?? "None";
    private string AuthHeader => HttpContextAccessor.HttpContext?.Request.Headers.Authorization.ToString() ?? "None";
    private bool IsInAdminRole => HttpContextAccessor.HttpContext?.User?.IsInRole("Administrator") ?? false;
    private IEnumerable<string> Roles => HttpContextAccessor.HttpContext?.User?.Claims
        .Where(c => c.Type == ClaimTypes.Role)
        .Select(c => c.Value) ?? Array.Empty<string>();
    private IEnumerable<ClaimInfo> Claims => HttpContextAccessor.HttpContext?.User?.Claims
        .Select(c => new ClaimInfo { Type = c.Type, Value = c.Value }) ?? Array.Empty<ClaimInfo>();

    private void RefreshData()
    {
        StateHasChanged();
    }

    private class ClaimInfo
    {
        public string? Type { get; set; }
        public string? Value { get; set; }
    }
}

The request on the testpage fails, and instead returns the text of the login page instead of the test text

I am building a service that uses a mix of SSR and interactive server. The SSR portion is only used to login. Now after the login completes, any request to the backend fails as unauthorized, but if I print the HttpContext on the Blazor page (interactive server), the user is logged in.

Any idea about what is going on?

EDIT CODE:

To make the starting example: create a folder and once inside it run dotnet new mudblazor --interactivity Server --auth Individual

REPLACE: program.cs

using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using MudBlazor.Services;
using test2.Components;
using test2.Components.Account;
using test2.Data;

var builder = WebApplication.CreateBuilder(args);

// Add MudBlazor services
builder.Services.AddMudServices();

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

builder.Services.AddCascadingAuthenticationState();
builder.Services.AddScoped<IdentityUserAccessor>();
builder.Services.AddScoped<IdentityRedirectManager>();
builder.Services.AddScoped<AuthenticationStateProvider, IdentityRevalidatingAuthenticationStateProvider>();

builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.Configuration["ApiUrl"] ?? "http://localhost:5009/") });

builder.Services.AddAuthentication(options =>
    {
        options.DefaultScheme = IdentityConstants.ApplicationScheme;
        options.DefaultSignInScheme = IdentityConstants.ExternalScheme;
    })
    .AddIdentityCookies();

var connectionString = builder.Configuration.GetConnectionString("DefaultConnection") ?? throw new InvalidOperationException("Connection string 'DefaultConnection' not found.");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlite(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddControllers();

builder.Services.AddIdentityCore<ApplicationUser>(options =>
    {
        options.SignIn.RequireConfirmedAccount = false;
        options.Password.RequireDigit = false;
        options.Password.RequireLowercase = false;
        options.Password.RequireNonAlphanumeric = false;
        options.Password.RequireUppercase = false;
        options.Password.RequiredLength = 1;
    })
    .AddEntityFrameworkStores<ApplicationDbContext>()
    .AddSignInManager()
    .AddDefaultTokenProviders();

builder.Services.AddSingleton<IEmailSender<ApplicationUser>, IdentityNoOpEmailSender>();

// Add this after the authentication configuration
builder.Services.AddAuthorization();

var app = builder.Build();

// Configure the HTTP request pipeline.

    app.UseMigrationsEndPoint();



app.UseStaticFiles();
app.UseAntifery();
app.UseAuthentication();
app.UseAuthorization();

app.MapRazorComponents<App>()
    .AddInteractiveServerRenderMode();
    
// Add additional endpoints required by the Identity /Account Razor components.
app.MapAdditionalIdentityEndpoints();

app.MapControllers();

app.Run();

NavMenu.razor:

@implements IDisposable

@inject NavigationManager NavigationManager

<MudNavMenu>
    <MudNavLink Href="" Match="NavLinkMatch.All" Icon="@Icons.Material.Filled.Home">Home</MudNavLink>
    <MudNavLink Href="auth" Match="NavLinkMatch.Prefix" Icon="@Icons.Material.Filled.Lock">Auth Required</MudNavLink>
    <AuthorizeView>
        <Authorized>
            <MudNavLink Href="test" Match="NavLinkMatch.Prefix" Icon="@Icons.Material.Filled.Person">test</MudNavLink>
            <MudNavLink Href="auth-debug" Match="NavLinkMatch.Prefix" Icon="@Icons.Material.Filled.List">debug</MudNavLink>
        </Authorized>
        <NotAuthorized>
            <MudNavLink Href="Account/Register" Match="NavLinkMatch.Prefix" Icon="@Icons.Material.Filled.Person">Register</MudNavLink>
            <MudNavLink Href="Account/Login" Match="NavLinkMatch.Prefix" Icon="@Icons.Material.Filled.Password">Login</MudNavLink>
        </NotAuthorized>
    </AuthorizeView>
</MudNavMenu>


@code {
    private string? currentUrl;

    protected override void OnInitialized()
    {
        currentUrl = NavigationManager.ToBaseRelativePath(NavigationManager.Uri);
        NavigationManager.LocationChanged += OnLocationChanged;
    }

    private void OnLocationChanged(object? sender, LocationChangedEventArgs e)
    {
        currentUrl = NavigationManager.ToBaseRelativePath(e.Location);
        StateHasChanged();
    }

    public void Dispose()
    {
        NavigationManager.LocationChanged -= OnLocationChanged;
    }
}

create a Testcontroller.cs at the same level as Program.cs:

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

[ApiController]
[Route("api/")]
public class TestController : ControllerBase
{
    [HttpGet]
    [Authorize]
    [Route("test")]
    public IActionResult Get()
    {
        return Ok("Hello from the API!");
    }
}

create 2 new files, in the pages folder: testpage.razor:

    @page "/test"
    @inject HttpClient Http
    @inject ISnackbar Snackbar
    
    <MudText Typo="Typo.h4" Class="mb-4">API Test</MudText>
    
    <MudButton Color="Color.Primary" 
               Variant="Variant.Filled" 
               OnClick="GetTestData">
        Get Test Data
    </MudButton>
    
    @if (!string.IsNullOrEmpty(_apiResponse))
    {
        <MudPaper Class="pa-4 mt-4">
            <MudText>@_apiResponse</MudText>
        </MudPaper>
    }
    
    @code {
        private string _apiResponse = string.Empty;
    
        private async Task GetTestData()
        {
            try
            {
                _apiResponse = await Http.GetStringAsync("api/test");
                Snackbar.Add("Data retrieved successfully!", Severity.Success);
            }
            catch (Exception ex)
            {
                Snackbar.Add("Error fetching data: " + ex.Message, Severity.Error);
            }
        }
}

Debug.razor:

@page "/auth-debug"
@using System.Security.Claims
@using Microsoft.AspNetCore.Http
@inject IHttpContextAccessor HttpContextAccessor

<MudContainer MaxWidth="MaxWidth.Large" Class="mt-4">
    <MudCard>
        <MudCardHeader>
            <MudText Typo="Typo.h5">Authentication Debug Information</MudText>
        </MudCardHeader>
        <MudCardContent>
            <MudGrid>
                <MudItem xs="12">
                    <MudPaper Class="pa-4" Elevation="0">
                        <MudText Typo="Typo.subtitle1" Class="mb-2"><b>Basic Information</b></MudText>
                        <MudList Dense="true" T="string">
                            <MudListItem T="string">
                                <MudText>Is Authenticated: <b>@IsAuthenticated</b></MudText>
                            </MudListItem>
                            <MudListItem T="string">
                                <MudText>Auth Type: <b>@AuthType</b></MudText>
                            </MudListItem>
                            <MudListItem T="string">
                                <MudText>Is Admin: <b>@IsInAdminRole</b></MudText>
                            </MudListItem>
                        </MudList>
                    </MudPaper>
                </MudItem>

                <MudItem xs="12">
                    <MudPaper Class="pa-4" Elevation="0">
                        <MudText Typo="Typo.subtitle1" Class="mb-2"><b>Authorization Header</b></MudText>
                        <MudTextField Value="@AuthHeader" Label="Auth Header" ReadOnly="true" Lines="2" />
                    </MudPaper>
                </MudItem>

                <MudItem xs="12">
                    <MudPaper Class="pa-4" Elevation="0">
                        <MudText Typo="Typo.subtitle1" Class="mb-2"><b>Roles</b></MudText>
                        @if (Roles.Any())
                        {
                            <MudList Dense="true" T="string">
                                @foreach (var role in Roles)
                                {
                                    <MudListItem Icon="@Icons.Material.Filled.Group" T="string">@role</MudListItem>
                                }
                            </MudList>
                        }
                        else
                        {
                            <MudAlert Severity="Severity.Info">No roles found</MudAlert>
                        }
                    </MudPaper>
                </MudItem>

                <MudItem xs="12">
                    <MudExpansionPanels>
                        <MudExpansionPanel Text="Claims">
                            @if (Claims.Any())
                            {
                                <MudTable Items="Claims" Dense="true" Hover="true">
                                    <HeaderContent>
                                        <MudTh>Type</MudTh>
                                        <MudTh>Value</MudTh>
                                    </HeaderContent>
                                    <RowTemplate>
                                        <MudTd>@context.Type</MudTd>
                                        <MudTd>@context.Value</MudTd>
                                    </RowTemplate>
                                </MudTable>
                            }
                            else
                            {
                                <MudAlert Severity="Severity.Info">No claims found</MudAlert>
                            }
                        </MudExpansionPanel>
                    </MudExpansionPanels>
                </MudItem>
            </MudGrid>
        </MudCardContent>
        <MudCardActions>
            <MudButton Color="Color.Primary" OnClick="RefreshData">Refresh Data</MudButton>
        </MudCardActions>
    </MudCard>
</MudContainer>

@code {
    private bool IsAuthenticated => HttpContextAccessor.HttpContext?.User?.Identity?.IsAuthenticated ?? false;
    private string AuthType => HttpContextAccessor.HttpContext?.User?.Identity?.AuthenticationType ?? "None";
    private string AuthHeader => HttpContextAccessor.HttpContext?.Request.Headers.Authorization.ToString() ?? "None";
    private bool IsInAdminRole => HttpContextAccessor.HttpContext?.User?.IsInRole("Administrator") ?? false;
    private IEnumerable<string> Roles => HttpContextAccessor.HttpContext?.User?.Claims
        .Where(c => c.Type == ClaimTypes.Role)
        .Select(c => c.Value) ?? Array.Empty<string>();
    private IEnumerable<ClaimInfo> Claims => HttpContextAccessor.HttpContext?.User?.Claims
        .Select(c => new ClaimInfo { Type = c.Type, Value = c.Value }) ?? Array.Empty<ClaimInfo>();

    private void RefreshData()
    {
        StateHasChanged();
    }

    private class ClaimInfo
    {
        public string? Type { get; set; }
        public string? Value { get; set; }
    }
}

The request on the testpage fails, and instead returns the text of the login page instead of the test text

Share Improve this question edited Feb 5 at 21:56 silajim asked Feb 4 at 3:15 silajimsilajim 331 silver badge8 bronze badges 5
  • Hi,@silajim could you share a minimal example that could reproduce the issue? – Ruikai Feng Commented Feb 5 at 2:09
  • I probably could, but it would be several files, not 1 or 2. but more like 7-8 I think – silajim Commented Feb 5 at 2:53
  • You could also tell us what you've done based on a default template,so that we could reproduce – Ruikai Feng Commented Feb 5 at 7:42
  • You need a AuthenticationStateProvider to synchronize the authorization token obtained in the backend through the SSR page to send it to the wasm client – Bisjob Commented Feb 5 at 16:58
  • I added the code, I think I did not miss anything – silajim Commented Feb 5 at 21:56
Add a comment  | 

1 Answer 1

Reset to default 1

The request on the testpage fails, and instead returns the text of the login page instead of the test text

Identity is based on CookieAuthentication,when you send request with httpclient,the cookie required is not contained

 _apiResponse = await Http.GetStringAsync("api/test");

You could try configure Identity Cookie:

builder.Services.ConfigureApplicationCookie(op => op.Cookie.HttpOnly = false);

add the js codes in app.razor to read the cookie:

<script>
            window.ReadCookie = {
            ReadCookie: function (cname) {
            var name = cname + "=";
        var decodedCookie = decodeURIComponent(document.cookie);
        var ca = decodedCookie.split(';');
        for (var i = 0; i < ca.length; i++) {
                var c = ca[i];
        while (c.charAt(0) == ' ') {
            c = c.substring(1);
                }
        if (c.indexOf(name) == 0) {
                    return c.substring(name.length, c.length);
                }
            }
        return "";
        }
    }
</script>

modify codes in test component:

@inject IJSRuntime JsRuntime
.....



private async Task GetTestData()
 {
     try
     {
         var authCookie = await JsRuntime.InvokeAsync<string>("ReadCookie.ReadCookie", ".AspNetCore.Identity.Application");
        
         Http.DefaultRequestHeaders.Add("Cookie", String.Format(".AspNetCore.Identity.Application={0}", authCookie));
         _apiResponse = await Http.GetStringAsync("api/test");
         Snackbar.Add("Data retrieved successfully!", Severity.Success);
     }
     catch (Exception ex)
     {
         Snackbar.Add("Error fetching data: " + ex.Message, Severity.Error);
     }
 }

Now Authenticate succeeded:

could this work without JS only C#?

There's no C# method for you to read cookie directly,but you could save the cookie (db, long life time service....)during static server rendering and read it when you click the Button

    [CascadingParameter]
    
    public HttpContext? httpContext{ get; set; }
    
    
protected override void OnInitialized()
{


    base.OnInitialized();
    if (httpContext != null)
    {
        var authCookie = httpContext.Request.Cookies[".AspNetCore.Identity.Application"];
        
        authCookieContainer.CookieSet(authCookie??"");
       

        
    }
}



private async Task GetTestData()
{
    ......
    var authCookie = authCookieContainer.CookieGet();
    ......
}
发布评论

评论列表(0)

  1. 暂无评论