Reputation: 4330
I have created a Blazor 8 application by choosing the render mode "Auto (Server and WebAssembly)" with "Individual Accounts" as authentication. The application is running fine with various roles such as "Administrators" and "Users".
There is also an API controller in the Blazor server project, which is also decorated with an Authorize attribute. Blazor client application is calling the API with HttpClient. Without the Authorize attribute on the API controller, the Blazor client application can fetch data but when I use the Authorize attribute, it can't call the API. How does the same authentication scheme work with the API controller as well? How to make the API callable from the Blazor client application successfully?
Program.cs in server project as below:
using BlazorAppAuthenticationDemo.Client.Services;
using BlazorAppAuthenticationDemo.Components;
using BlazorAppAuthenticationDemo.Components.Account;
using BlazorAppAuthenticationDemo.Data;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddScoped<TemplateService>();
builder.Services.AddControllers();
builder.Services.AddRazorComponents()
.AddInteractiveServerComponents()
.AddInteractiveWebAssemblyComponents();
builder.Services.AddCascadingAuthenticationState();
builder.Services.AddScoped<IdentityUserAccessor>();
builder.Services.AddScoped<IdentityRedirectManager>();
builder.Services.AddScoped<AuthenticationStateProvider, PersistingRevalidatingAuthenticationStateProvider>();
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.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddIdentityCore<ApplicationUser>(options => options.SignIn.RequireConfirmedAccount = true)
.AddRoles<IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddSignInManager()
.AddDefaultTokenProviders();
builder.Services.AddSingleton<IEmailSender<ApplicationUser>, IdentityNoOpEmailSender>();
builder.Services.AddScoped(http => new HttpClient
{
BaseAddress = new Uri(builder.Configuration.GetSection("BaseAddress").Value!)
});
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseWebAssemblyDebugging();
app.UseMigrationsEndPoint();
}
else
{
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.UseAntiforgery();
app.MapControllers();
app.MapRazorComponents<App>()
.AddInteractiveServerRenderMode()
.AddInteractiveWebAssemblyRenderMode()
.AddAdditionalAssemblies(typeof(BlazorAppAuthenticationDemo.Client._Imports).Assembly);
// Add additional endpoints required by the Identity /Account Razor components.
app.MapAdditionalIdentityEndpoints();
app.Run();
Program.cs in client project as below:
using BlazorAppAuthenticationDemo.Client;
using BlazorAppAuthenticationDemo.Client.Services;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.Services.AddAuthorizationCore();
builder.Services.AddCascadingAuthenticationState();
builder.Services.AddSingleton<AuthenticationStateProvider, PersistentAuthenticationStateProvider>();
builder.Services.AddScoped<TemplateService>();
builder.Services.AddScoped(http => new HttpClient
{
BaseAddress = new Uri(builder.HostEnvironment.BaseAddress),
});
await builder.Build().RunAsync();
TemplateService.cs to consume the API:
using BlazorAppAuthenticationDemo.Shared.Models;
using System.Net.Http.Json;
namespace BlazorAppAuthenticationDemo.Client.Services;
public class TemplateService(HttpClient httpClient)
{
private readonly HttpClient _httpClient = httpClient;
public async Task<List<TemplateDTO>?> All()
{
var templates = await _httpClient.GetAsync($"api/Templates/all");
List<TemplateDTO>? data = null;
if (templates.IsSuccessStatusCode)
{
data = await templates.Content.ReadFromJsonAsync<List<TemplateDTO>?>();
}
return data;
}
}
TemplatesController.cs which has one endpoint:
using BlazorAppAuthenticationDemo.Shared.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace BlazorAppAuthenticationDemo.Controllers;
[Route("api/[controller]")]
[ApiController]
public class TemplatesController : ControllerBase
{
private readonly List<TemplateDTO> _templates = [
new TemplateDTO {Id =1, Name = "Test 1", Content = "This is first template." },
new TemplateDTO { Id =2, Name = "Test 2", Content = "This is second template."},
new TemplateDTO { Id =3, Name = "Test 3", Content = "This is third template."}
];
[HttpGet("all")]
[Authorize(Roles = "Administrators")]
public async Task<ActionResult<List<TemplateDTO>>> All()
{
await Task.CompletedTask;
return Ok(_templates);
}
}
Upvotes: 0
Views: 227
Reputation: 1657
UPDATE
Sorry for misunderstanding the question, your issue looks to be accessing the authorized controller in Blazor. In my test local, the service fails at var templates = await _httpClient.GetAsync($"api/templates/all");
and returns the html instead of json, leads to the error and the controller will not be hit.
Based on my research it is a quite frequently asked question. Here is a doc you could refer to Blazor Server cant access controller with [Authorize] attribute.
In my understanding, the cookie is stored in browser thus you could reach the endpoint if directly get to the url. However in your project backend it is not saved, so the authorization fails and you will get Account/Login?ReturnUrl.
In a word, it is still needed to help pass the authorize part in a authorized controller endpoint in blazor web app.
Register the controller in your server program.cs to ensure the route can be recognized correctly.
...
builder.Services.AddControllers();
var app = builder.Build();
...
app.MapControllers();
app.Run();
Upvotes: 0