Skip to content

Commit

Permalink
Merge pull request #99 from DuendeSoftware/joe/principal-accessor
Browse files Browse the repository at this point in the history
Add accessor for current principal
  • Loading branch information
josephdecock committed May 1, 2024
2 parents 9c49699 + 5657485 commit c80fe2b
Show file tree
Hide file tree
Showing 10 changed files with 223 additions and 47 deletions.
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();
}
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 IServiceCollection AddOpenIdConnectAccessTokenManagement(this ISer
{
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

0 comments on commit c80fe2b

Please sign in to comment.