DuendeSoftware / Samples

Samples for our Products
Other
225 stars 269 forks source link

Update Blazor Samples For .NET 8 #142

Open josephdecock opened 1 year ago

b1tzer0 commented 11 months ago

I second this, I have been looking all over the web trying to find an example of how to properly implement SSR with Auto Mode (SignalR and WASM). I have tried various configurations with BFF and Server Side but have not had much luck. If there is a working example out there that would be greatly that we could repost or if one could be added to this repo, that would be great help in trying to solve my problem.

lnaie commented 9 months ago

It needs a BlazorSSR example with Asp.Net Core Identity, so to have the default user management solution handled.

leastprivilege commented 9 months ago

What we need is a Blazor Web App sample that uses BFF for coordinating authentication and API calls that works for all render modes.

hugh-maaskant commented 9 months ago

If possible, an example (or explanation somewhere) of how to use it with SignalR from a Blazor WASM page/component would be very welcome.

lnaie commented 9 months ago

What we need is a Blazor Web App sample that uses BFF for coordinating authentication and API calls that works for all render modes.

One sample that covers all rendering modes is ideal (https://learn.microsoft.com/en-us/aspnet/core/blazor/components/render-modes?view=aspnetcore-8.0). I was thinking that it would help, for now, to target the Interactive WebAssembly, which renders on the server, as well on the client.

Liandrel commented 3 months ago

Is there any approximate date when we can expect the release of the updated samples?

b1tzer0 commented 3 months ago

@Liandrel , I managed to get this working with an app I am working on. While I can't show source, I can point you to the same location where I found my starting point. One caveat is that while it does work; it is not integrated into Duende at this time. https://github.com/josephdecock/InteractiveBlazorAuth/tree/main

Hope this helps

ThomasBarnekow commented 2 months ago

@b1tzer0, did you get "all of it" working based on that InteractiveBlazorAuth example? Based on your post, I also tried it out and run into issues, some of which are mentioned further above. For example:

@josephdecock, I would essentially like to set up a new ASP.NET 8.0 Blazor Web App with Interactive WebAssembly (avoiding SignalR) plus a Web API and then add IdentityServer-based authentication and authorization. While your InteractiveBlazorAuth example looks like a good starting point (which does more than what I need because it demonstrates render modes that I likely don't need), it has the issues mentioned above. So, unfortunately, there is currently no adequate, working example that demonstrates the mere basics.

b1tzer0 commented 2 months ago

@ThomasBarnekow, I can confirm the /bff/user appears to work in both WASM and Blazor Server (SignalR) image

Here you can see the only websocket is the browser refresh: image

Logout is hit and miss, while in debug it does in fact take me to the logout page, but if I am running outside of debug mode the page just refreshes. Looking at it, I feel like I messed that up myself, when I go directly to the URL it works with no problem.

This is the code I am using for my bff authentication state provider.

public class BffAuthenticationStateProvider : AuthenticationStateProvider
{
    private static readonly TimeSpan UserCacheRefreshInterval
    = TimeSpan.FromSeconds(60);

    private readonly HttpClient _client;
    private readonly ILogger<BffAuthenticationStateProvider> _logger;

    private DateTimeOffset _userLastCheck
        = DateTimeOffset.FromUnixTimeSeconds(0);
    private ClaimsPrincipal _cachedUser
        = new ClaimsPrincipal(new ClaimsIdentity());

    public BffAuthenticationStateProvider(
        HttpClient client,
        ILogger<BffAuthenticationStateProvider> logger)
    {
        _client = client;
        _logger = logger;
    }

    public override async Task<AuthenticationState> GetAuthenticationStateAsync()
    {
        var user = await GetUser();
        var state = new AuthenticationState(user);

        // checks periodically for a session state change and fires event
        // this causes a round trip to the server
        // adjust the period accordingly if that feature is needed
        if (user.Identity.IsAuthenticated)
        {
            _logger.LogInformation("starting background check..");
            Timer? timer = null;

            timer = new Timer(async _ =>
            {
                var currentUser = await GetUser(false);
                if (currentUser.Identity.IsAuthenticated == false)
                {
                    _logger.LogInformation("user logged out");
                    NotifyAuthenticationStateChanged(Task.FromResult(new AuthenticationState(currentUser)));
                    await timer.DisposeAsync();
                }
            }, null, 1000, 5000);
        }

        return state;
    }

    private async ValueTask<ClaimsPrincipal> GetUser(bool useCache = true)
    {
        var now = DateTimeOffset.Now;
        if (useCache && now < _userLastCheck + UserCacheRefreshInterval)
        {
            _logger.LogDebug("Taking user from cache");
            return _cachedUser;
        }

        _logger.LogDebug("Fetching user");
        _cachedUser = await FetchUser();
        _userLastCheck = now;

        return _cachedUser;
    }

    record ClaimRecord(string Type, object Value);

    private async Task<ClaimsPrincipal> FetchUser()
    {
        try
        {
            _logger.LogInformation("Fetching user information.");
            var response = await _client.GetAsync("bff/user?slide=false");

            if (response.StatusCode == HttpStatusCode.OK)
            {
                var claims = await response.Content.ReadFromJsonAsync<List<ClaimRecord>>();

                var identity = new ClaimsIdentity(
                    nameof(BffAuthenticationStateProvider),
                    "name",
                    "role");

                foreach (var claim in claims)
                {
                    identity.AddClaim(new Claim(claim.Type, claim.Value.ToString()));
                }

                return new ClaimsPrincipal(identity);
            }
        }
        catch (Exception ex)
        {
            _logger.LogWarning(ex, "Fetching user failed.");
        }

        return new ClaimsPrincipal(new ClaimsIdentity());
    }
}

Here is where I wire it up in my .Client startup file

builder.Services.AddAuthorizationCore();
builder.Services.AddCascadingAuthenticationState();
builder.Services.AddScoped<AuthenticationStateProvider, BffAuthenticationStateProvider>();

// HTTP client configuration
builder.Services.AddTransient<AntiforgeryHandler>();

builder.Services.AddHttpClient("HostUri", client => client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress))
    .AddHttpMessageHandler<AntiforgeryHandler>();