Skip to content
This repository has been archived by the owner on Nov 19, 2024. It is now read-only.

Add accessor for current principal #99

Merged
merged 5 commits into from
May 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 4 additions & 8 deletions samples/BlazorServer/HostingExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
using BlazorServer.Plumbing;
using BlazorServer.Services;
using Duende.AccessTokenManagement.OpenIdConnect;
using Serilog;

namespace BlazorServer;
Expand Down Expand Up @@ -50,27 +49,24 @@ public static WebApplication ConfigureServices(this WebApplicationBuilder builde
});

// adds access token management
builder.Services.AddOpenIdConnectAccessTokenManagement();
builder.Services.AddOpenIdConnectAccessTokenManagement()
.AddBlazorServerAccessTokenManagement<ServerSideTokenStore>();

// register events to customize authentication handlers
builder.Services.AddTransient<CookieEvents>();
builder.Services.AddTransient<OidcEvents>();

// not allowed to programmatically use HttpContext in Blazor Server.
// that's why tokens cannot be managed in the login session
builder.Services.AddSingleton<IUserTokenStore, ServerSideTokenStore>();

// registers HTTP client that uses the managed user access token
builder.Services.AddTransient<RemoteApiService>();
builder.Services.AddHttpClient<RemoteApiService>(client =>
builder.Services.AddUserAccessTokenHttpClient("demoApiClient", configureClient: client =>
{
client.BaseAddress = new Uri("https://demo.duendesoftware.com/api/");
});

builder.Services.AddControllersWithViews();
builder.Services.AddRazorPages();
builder.Services.AddServerSideBlazor();

builder.Services.AddSingleton<WeatherForecastService>();

return builder.Build();
Expand Down
2 changes: 1 addition & 1 deletion samples/BlazorServer/Plumbing/ServerSideTokenStore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ namespace BlazorServer.Plumbing;

/// <summary>
/// Simplified implementation of a server-side token store.
/// Probably want somehting more robust IRL
/// Probably want something more robust IRL
/// </summary>
public class ServerSideTokenStore : IUserTokenStore
{
Expand Down
28 changes: 4 additions & 24 deletions samples/BlazorServer/Services/RemoteApiService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,45 +2,25 @@
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.

using System.Text.Json;
using Duende.AccessTokenManagement.OpenIdConnect;
using IdentityModel.Client;
using Microsoft.AspNetCore.Components.Authorization;

namespace BlazorServer.Services;

public class RemoteApiService
{
private readonly HttpClient _client;
private readonly AuthenticationStateProvider _authenticationStateProvider;
private readonly IUserTokenManagementService _tokenManagementService;

public RemoteApiService(
HttpClient client,
AuthenticationStateProvider authenticationStateProvider,
IUserTokenManagementService tokenManagementService)
IHttpClientFactory factory)
{
_client = client;
_authenticationStateProvider = authenticationStateProvider;
_tokenManagementService = tokenManagementService;
_client = factory.CreateClient("demoApiClient");
}

private record Claim(string type, object value);

public async Task<string> GetData()
{
var request = new HttpRequestMessage(HttpMethod.Get, "test");
var response = await SendRequestAsync(request);

var json = JsonSerializer.Deserialize<IEnumerable<Claim>>(await response.Content.ReadAsStringAsync());
var response = await _client.GetStringAsync("test");
var json = JsonSerializer.Deserialize<IEnumerable<Claim>>(response);
return JsonSerializer.Serialize(json, new JsonSerializerOptions { WriteIndented = true });
}

private async Task<HttpResponseMessage> SendRequestAsync(HttpRequestMessage request)
{
var state = await _authenticationStateProvider.GetAuthenticationStateAsync();
var token = await _tokenManagementService.GetAccessTokenAsync(state.User);

request.SetToken(token.AccessTokenType!, token.AccessToken!);
return await _client.SendAsync(request);
}
}
5 changes: 4 additions & 1 deletion samples/BlazorServer/Shared/RedirectToLogin.razor
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
@inject NavigationManager Navigation

@code {
protected override void OnInitialized()
// Using the async method prevents NavigationExceptions, even though this method is synchronous
#pragma warning disable CS1998
protected override async Task OnInitializedAsync()
{
var returnUrl = Uri.EscapeDataString("/" + Navigation.ToBaseRelativePath(Navigation.Uri));
Navigation.NavigateTo($"account/login?returnUrl={returnUrl}", forceLoad: true);
}
#pragma warning restore CS1998
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.

using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using System.Security.Claims;
using System.Threading.Tasks;

namespace Duende.AccessTokenManagement.OpenIdConnect;

/// <summary>
/// Accesses the current user from blazor server.
/// </summary>
/// <remarks>
/// ctor
/// </remarks>
public class BlazorServerUserAccessor(
// We use the CircuitServicesAccessor to resolve the
// AuthenticationStateProvider, rather than injecting it. Injecting the
// state provider directly doesn't work here, because this service might be
// called in a non-blazor DI scope.
CircuitServicesAccessor circuitServicesAccessor,
IHttpContextAccessor? httpContextAccessor,
ILogger<BlazorServerUserAccessor> logger) : IUserAccessor
{

/// <inheritdoc/>
public async Task<ClaimsPrincipal> GetCurrentUserAsync()
{
var authStateProvider = circuitServicesAccessor.Services?
.GetService<AuthenticationStateProvider>();
// If we are in blazor server (streaming over a circuit), this provider will be non-null
if (authStateProvider != null)
{
var authState = await authStateProvider.GetAuthenticationStateAsync();
return authState.User;
}
// Otherwise, we should be in an SSR scenario, and the httpContext should be available
else if(httpContextAccessor?.HttpContext != null)
{
return httpContextAccessor.HttpContext.User;
}
// If we are in neither blazor server or SSR, something weird is going on.
else
{
logger.LogWarning("Neither an authentication state provider or http context are available to obtain the current principal.");
return new ClaimsPrincipal();
}
}

}


Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.

using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.Server.Circuits;
using Microsoft.Extensions.DependencyInjection;

namespace Duende.AccessTokenManagement.OpenIdConnect;

// This code is from the blazor documentation:
// https://learn.microsoft.com/en-us/aspnet/core/blazor/fundamentals/dependency-injection?view=aspnetcore-8.0#access-server-side-blazor-services-from-a-different-di-scope

/// <summary>
/// Provides access to scoped blazor services from non-blazor DI scopes, such as
/// scopes created using IHttpClientFactory.
/// </summary>
public class CircuitServicesAccessor
{
static readonly AsyncLocal<IServiceProvider> blazorServices = new();

internal IServiceProvider? Services
{
get => blazorServices.Value;
set => blazorServices.Value = value!;
}
}

internal class ServicesAccessorCircuitHandler : CircuitHandler
{
readonly IServiceProvider services;
readonly CircuitServicesAccessor circuitServicesAccessor;

internal ServicesAccessorCircuitHandler(IServiceProvider services,
CircuitServicesAccessor servicesAccessor)
{
this.services = services;
this.circuitServicesAccessor = servicesAccessor;
}

public override Func<CircuitInboundActivityContext, Task> CreateInboundActivityHandler(
Func<CircuitInboundActivityContext, Task> next)
{
return async context =>
{
circuitServicesAccessor.Services = services;
await next(context);
circuitServicesAccessor.Services = null;
};
}
}

internal static class CircuitServicesServiceCollectionExtensions
{
public static IServiceCollection AddCircuitServicesAccessor(
this IServiceCollection services)
{
services.AddScoped<CircuitServicesAccessor>();
services.AddScoped<CircuitHandler, ServicesAccessorCircuitHandler>();

return services;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.

using Microsoft.AspNetCore.Http;
using System.Security.Claims;
using System.Threading.Tasks;

namespace Duende.AccessTokenManagement.OpenIdConnect;

/// <summary>
/// Accesses the current principal based on the HttpContext.User.
/// </summary>
public class HttpContextUserAccessor : IUserAccessor
{
private readonly IHttpContextAccessor _httpContextAccessor;

/// <summary>
/// ctor
/// </summary>
public HttpContextUserAccessor(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}

/// <inheritdoc/>
public Task<ClaimsPrincipal> GetCurrentUserAsync()
{
return Task.FromResult(_httpContextAccessor.HttpContext?.User ?? new ClaimsPrincipal());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.

using System.Security.Claims;
using System.Threading.Tasks;

namespace Duende.AccessTokenManagement.OpenIdConnect;

/// <summary>
/// Service that retrieves the current principal.
/// </summary>
public interface IUserAccessor
{
/// <summary>
/// Gets the current user.
/// </summary>
Task<ClaimsPrincipal> GetCurrentUserAsync();
josephdecock marked this conversation as resolved.
Show resolved Hide resolved
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
using System.Net.Http;
using Duende.AccessTokenManagement;
using Duende.AccessTokenManagement.OpenIdConnect;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Logging;
Expand Down Expand Up @@ -35,13 +34,38 @@ public static IServiceCollection AddOpenIdConnectAccessTokenManagement(this ISer

services.TryAddTransient<IUserTokenManagementService, UserAccessAccessTokenManagementService>();
services.TryAddTransient<IOpenIdConnectConfigurationService, OpenIdConnectConfigurationService>();
// scoped since it will be caching per-request authentication results
services.TryAddScoped<IUserTokenStore, AuthenticationSessionUserAccessTokenStore>();
services.TryAddSingleton<IUserTokenRequestSynchronization, UserTokenRequestSynchronization>();
services.TryAddTransient<IUserTokenEndpointService, UserTokenEndpointService>();

services.ConfigureOptions<ConfigureOpenIdConnectOptions>();

// By default, we assume that we are in a traditional web application
// where we can use the http context. The services below depend on http
// context, and we register different ones in blazor

services.TryAddScoped<IUserAccessor, HttpContextUserAccessor>();
// scoped since it will be caching per-request authentication results
services.TryAddScoped<IUserTokenStore, AuthenticationSessionUserAccessTokenStore>();

return services;
}

/// <summary>
/// Adds implementations of services that enable access token management in
/// Blazor Server.
/// </summary>
/// <typeparam name="TTokenStore">An IUserTokenStore implementation. Blazor
/// Server requires an IUserTokenStore because the default token store
/// relies on cookies, which are not present when streaming updates over a
/// blazor circuit. </typeparam>
public static IServiceCollection AddBlazorServerAccessTokenManagement<TTokenStore>(this IServiceCollection services)
where TTokenStore : class, IUserTokenStore
{
services.AddSingleton<IUserTokenStore, TTokenStore>();
services.AddScoped<IUserAccessor, BlazorServerUserAccessor>();
services.AddCircuitServicesAccessor();
services.AddHttpContextAccessor(); // For SSR

return services;
}

Expand Down Expand Up @@ -143,10 +167,12 @@ public static IHttpClientBuilder AddUserAccessTokenHandler(
{
var dpopService = provider.GetRequiredService<IDPoPProofService>();
var dpopNonceStore = provider.GetRequiredService<IDPoPNonceStore>();
var contextAccessor = provider.GetRequiredService<IHttpContextAccessor>();
var userTokenManagement = provider.GetRequiredService<IUserTokenManagementService>();
var logger = provider.GetRequiredService<ILogger<OpenIdConnectClientAccessTokenHandler>>();
var principalAccessor = provider.GetRequiredService<IUserAccessor>();

return new OpenIdConnectUserAccessTokenHandler(dpopService, dpopNonceStore, contextAccessor, logger, parameters);
return new OpenIdConnectUserAccessTokenHandler(
dpopService, dpopNonceStore, principalAccessor, userTokenManagement, logger, parameters);
});
}

Expand Down
Loading
Loading