Pisum.Bff.Blazor.Client 1.11.0
Pisum.Bff.Blazor.Client
Blazor WebAssembly client components for the Pisum Backend for Frontend (BFF) security framework.
Overview
Pisum.Bff.Blazor.Client provides client-side components and services for Blazor WebAssembly applications that integrate with the Pisum BFF pattern. This package handles authentication state, user information retrieval, and secure API calls through the BFF server.
Key Features
- ✅ Authentication State Management - Track user authentication status
- ✅ User Information - Retrieve and display user claims
- ✅ CSRF Token Handling - Automatic anti-forgery token management
- ✅ Secure HTTP Client - Pre-configured HttpClient with BFF integration
- ✅ Login/Logout Actions - Simple navigation-based authentication flows
- ✅ Blazor Components - Ready-to-use UI components for authentication
Installation
From Pisum NuGet Server
dotnet add package Pisum.Bff.Blazor.Client --source https://nuget.pisum.synology.me/v3/index.json
dotnet add package Pisum.Bff.Shared --source https://nuget.pisum.synology.me/v3/index.json
Or via Package Manager:
Install-Package Pisum.Bff.Blazor.Client -Source https://nuget.pisum.synology.me/v3/index.json
Install-Package Pisum.Bff.Shared -Source https://nuget.pisum.synology.me/v3/index.json
Configure NuGet Source
Add the Pisum NuGet server to your NuGet configuration:
dotnet nuget add source https://nuget.pisum.synology.me/v3/index.json --name Pisum
Then install normally:
dotnet add package Pisum.Bff.Blazor.Client
dotnet add package Pisum.Bff.Shared
Quick Start
1. Configure Services (Program.cs)
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using Pisum.Bff.Blazor.Client;
var builder = WebAssemblyHostBuilder.CreateDefault(args);
// Add BFF client services
builder.Services.AddBffBlazorClient(builder.HostEnvironment.BaseAddress);
await builder.Build().RunAsync();
2. Use Authentication State
@page "/secure"
@using Pisum.Bff.Shared
@inject HttpClient Http
<AuthorizeView>
<Authorized>
<h3>Welcome, @context.User.Identity?.Name!</h3>
<h4>Your Claims:</h4>
<ul>
@foreach (var claim in context.User.Claims)
{
<li>@claim.Type: @claim.Value</li>
}
</ul>
</Authorized>
<NotAuthorized>
<p>You need to log in.</p>
<a href="/bff/login?returnUrl=@NavigationManager.Uri">Log In</a>
</NotAuthorized>
</AuthorizeView>
3. Make Authenticated API Calls
@page "/data"
@inject HttpClient Http
<button @onclick="FetchData">Load Data</button>
@if (data != null)
{
<ul>
@foreach (var item in data)
{
<li>@item</li>
}
</ul>
}
@code {
private List<string>? data;
private async Task FetchData()
{
// CSRF token is automatically added by the configured HttpClient
data = await Http.GetFromJsonAsync<List<string>>("/api/data");
}
}
Core Components
BFF Authentication State Provider
Manages authentication state by querying the BFF /user endpoint:
// Automatically registered with AddBffBlazorClient()
public class BffAuthenticationStateProvider : AuthenticationStateProvider
{
// Queries /bff/user endpoint
// Converts ClaimsPrincipalLite to ClaimsPrincipal
// Updates authentication state
}
Features:
- Automatic state refresh
- User claims mapping
- Anonymous user handling
HTTP Client Configuration
Pre-configured HttpClient with CSRF token support:
// Automatically configured
builder.Services.AddBffBlazorClient(baseAddress);
// Adds:
// - Base address configuration
// - CSRF header handler
// - JSON serialization options
Usage:
@inject HttpClient Http
// GET requests (no CSRF token needed)
var data = await Http.GetFromJsonAsync<MyData>("/api/data");
// POST requests (CSRF token automatically added)
var response = await Http.PostAsJsonAsync("/api/data", newData);
// PUT requests (CSRF token automatically added)
await Http.PutAsJsonAsync("/api/data/123", updatedData);
// DELETE requests (CSRF token automatically added)
await Http.DeleteAsync("/api/data/123");
CSRF Token Handler
Automatically adds anti-forgery tokens to state-changing requests:
// Configured automatically
// Adds X-CSRF-TOKEN header to POST, PUT, DELETE, PATCH requests
// Token value: DB8994E6-7736-4FDD-8BB7-03A1423F2AC4 (default)
Custom CSRF Configuration:
// If BFF server uses custom header name
builder.Services.AddBffBlazorClient(baseAddress, options =>
{
options.CsrfHeaderName = "X-CUSTOM-CSRF";
options.CsrfHeaderValue = "custom-token-value";
});
Authentication Flows
Login Flow
@page "/login"
@inject NavigationManager Navigation
<h3>Login</h3>
<button @onclick="Login">Log In</button>
@code {
private void Login()
{
var returnUrl = Navigation.Uri;
Navigation.NavigateTo($"/bff/login?returnUrl={returnUrl}", forceLoad: true);
}
}
How it works:
- User clicks login button
- Navigates to
/bff/loginendpoint - BFF redirects to identity provider
- User authenticates at IdP
- Returns to BFF with authorization code
- BFF creates session and sets httpOnly cookie
- Redirects back to
returnUrl
Logout Flow
@page "/logout"
@inject HttpClient Http
@inject NavigationManager Navigation
<button @onclick="Logout">Log Out</button>
@code {
private async Task Logout()
{
// Call BFF logout endpoint
await Http.PostAsync("/bff/logout", null);
// Redirect to home page
Navigation.NavigateTo("/", forceLoad: true);
}
}
Check Authentication Status
@page "/"
@inject AuthenticationStateProvider AuthStateProvider
<AuthorizeView>
<Authorized>
<p>Logged in as: @context.User.Identity?.Name</p>
<button @onclick="Logout">Logout</button>
</Authorized>
<NotAuthorized>
<button @onclick="Login">Login</button>
</NotAuthorized>
</AuthorizeView>
@code {
private void Login()
{
Navigation.NavigateTo("/bff/login", forceLoad: true);
}
private async Task Logout()
{
await Http.PostAsync("/bff/logout", null);
Navigation.NavigateTo("/", forceLoad: true);
}
}
Advanced Scenarios
Custom User Service
public interface IBffUserService
{
Task<ClaimsPrincipalLite?> GetUserAsync();
Task<bool> IsAuthenticatedAsync();
}
public class BffUserService : IBffUserService
{
private readonly HttpClient _http;
public BffUserService(HttpClient http)
{
_http = http;
}
public async Task<ClaimsPrincipalLite?> GetUserAsync()
{
try
{
return await _http.GetFromJsonAsync<ClaimsPrincipalLite>("/bff/user");
}
catch
{
return null;
}
}
public async Task<bool> IsAuthenticatedAsync()
{
var user = await GetUserAsync();
return user?.Claims?.Any() == true;
}
}
Role-Based Authorization
<AuthorizeView Roles="Admin">
<Authorized>
<AdminPanel />
</Authorized>
<NotAuthorized>
<p>You need admin privileges.</p>
</NotAuthorized>
</AuthorizeView>
Policy-Based Authorization
<AuthorizeView Policy="CanEditContent">
<Authorized>
<EditButton />
</Authorized>
</AuthorizeView>
Accessing User Claims
@inject AuthenticationStateProvider AuthStateProvider
@code {
private async Task LoadUserInfo()
{
var authState = await AuthStateProvider.GetAuthenticationStateAsync();
var user = authState.User;
if (user.Identity?.IsAuthenticated == true)
{
var userId = user.FindFirst("sub")?.Value;
var email = user.FindFirst("email")?.Value;
var roles = user.FindAll("role").Select(c => c.Value);
}
}
}
Error Handling
@code {
private async Task MakeApiCall()
{
try
{
var data = await Http.GetFromJsonAsync<MyData>("/api/data");
}
catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.Unauthorized)
{
// Session expired or not authenticated
Navigation.NavigateTo("/bff/login", forceLoad: true);
}
catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.Forbidden)
{
// User doesn't have required permissions
errorMessage = "Access denied";
}
catch (Exception ex)
{
// General error handling
errorMessage = $"Error: {ex.Message}";
}
}
}
Integration with MudBlazor
@using MudBlazor
<MudAppBar>
<MudText Typo="Typo.h6">My App</MudText>
<MudSpacer />
<AuthorizeView>
<Authorized>
<MudMenu Icon="@Icons.Material.Filled.Person" Label="@context.User.Identity?.Name">
<MudMenuItem OnClick="@Logout">Logout</MudMenuItem>
</MudMenu>
</Authorized>
<NotAuthorized>
<MudButton Color="Color.Primary" OnClick="@Login">Login</MudButton>
</NotAuthorized>
</AuthorizeView>
</MudAppBar>
Configuration Options
BFF Client Options
public class BffBlazorClientOptions
{
// BFF endpoints
public string UserEndpoint { get; set; } = "/bff/user";
public string LoginEndpoint { get; set; } = "/bff/login";
public string LogoutEndpoint { get; set; } = "/bff/logout";
// CSRF configuration
public string CsrfHeaderName { get; set; } = "X-CSRF-TOKEN";
public string CsrfHeaderValue { get; set; } = "DB8994E6-7736-4FDD-8BB7-03A1423F2AC4";
// Authentication state polling (optional)
public TimeSpan? AuthenticationPollingInterval { get; set; } = null;
}
Usage:
builder.Services.AddBffBlazorClient(baseAddress, options =>
{
options.UserEndpoint = "/custom/user";
options.CsrfHeaderName = "X-CUSTOM-CSRF";
options.AuthenticationPollingInterval = TimeSpan.FromMinutes(5);
});
Target Frameworks
- .NET 8.0
- .NET 9.0
Dependencies
- Microsoft.AspNetCore.Components.WebAssembly - Blazor WebAssembly
- Microsoft.AspNetCore.Components.WebAssembly.Authentication - Authentication components
- Microsoft.Extensions.Http - HTTP client factory
- System.Text.Json - JSON serialization
- Pisum.Bff.Shared (1.0.0) - Shared models
Related Packages
- Pisum.Bff - Core BFF server-side library
- Pisum.Bff.Shared - Shared models and utilities
- Pisum.Bff.Yarp - YARP integration
Examples
See the samples directory for complete examples:
- Demo.Bff.Blazor.Client - Complete Blazor WebAssembly client
- Demo.Bff.Blazor.Server - BFF server hosting the Blazor client
Best Practices
- Always use forceLoad when navigating to login/logout endpoints
- Handle 401 responses by redirecting to login
- Use AuthorizeView for conditional rendering based on authentication
- Implement error boundaries for API call failures
- Cache user information when appropriate
- Test authentication flows thoroughly
- Use HTTPS in production
Security Considerations
- No tokens stored in browser storage
- CSRF tokens automatically managed
- Authentication state synchronized with server
- HttpOnly cookies prevent XSS attacks
- All API calls go through BFF for token injection
Troubleshooting
Common Issues
Problem: AuthorizeView always shows NotAuthorized
- Solution: Ensure BFF server is running and accessible
- Solution: Check that
/bff/userendpoint returns user data - Solution: Verify cookies are being sent (check SameSite settings)
Problem: API calls return 401
- Solution: User session may have expired - redirect to login
- Solution: Check that CSRF token header matches server configuration
Problem: Login redirect not working
- Solution: Use
forceLoad: truein NavigateTo - Solution: Verify BFF login endpoint is correctly configured
Contributing
This is part of the Pisum BFF framework. For contributions and issues, please refer to the main repository.
License
Copyright © 2025 pisum.net
Support
For questions and support, please open an issue in the main repository.
No packages depend on Pisum.Bff.Blazor.Client.
.NET 8.0
- Pisum.Bff.Shared (>= 1.11.0)
- Microsoft.AspNetCore.Components.WebAssembly (>= 8.0.21)
- Microsoft.AspNetCore.Components.WebAssembly.Authentication (>= 8.0.21)
- Microsoft.Extensions.Http (>= 8.0.1)
- System.Text.Json (>= 8.0.6)
.NET 9.0
- Pisum.Bff.Shared (>= 1.11.0)
- Microsoft.AspNetCore.Components.WebAssembly (>= 9.0.10)
- Microsoft.AspNetCore.Components.WebAssembly.Authentication (>= 9.0.10)
- Microsoft.Extensions.Http (>= 9.0.10)
- System.Text.Json (>= 9.0.10)
| Version | Downloads | Last updated |
|---|---|---|
| 1.11.0 | 5 | 10/21/2025 |
| 1.10.0 | 27 | 10/09/2025 |
| 1.9.0 | 21 | 10/09/2025 |
| 1.8.1-preview | 5 | 10/09/2025 |