Reputation: 33
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.UseAntiforgery();
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
Upvotes: 0
Views: 110
Reputation: 11896
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();
......
}
Upvotes: 1