Pisum.Bff.Blazor.Client 1.11.0

Pisum.Bff.Blazor.Client

NuGet License

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:

  1. User clicks login button
  2. Navigates to /bff/login endpoint
  3. BFF redirects to identity provider
  4. User authenticates at IdP
  5. Returns to BFF with authorization code
  6. BFF creates session and sets httpOnly cookie
  7. 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

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

  1. Always use forceLoad when navigating to login/logout endpoints
  2. Handle 401 responses by redirecting to login
  3. Use AuthorizeView for conditional rendering based on authentication
  4. Implement error boundaries for API call failures
  5. Cache user information when appropriate
  6. Test authentication flows thoroughly
  7. 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/user endpoint 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: true in 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.

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