Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add WebAssemblyAuthenticationStateProvider. #19479

Merged
merged 10 commits into from
Apr 8, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ var abp = abp || {};

// FULL SCREEN /////////////////

abp.utils.toggleFullscreen = function() {
abp.utils.toggleFullscreen = function () {
var elem = document.documentElement;
if (!document.fullscreenElement && !document.mozFullScreenElement &&
!document.webkitFullscreenElement && !document.msFullscreenElement) {
Expand All @@ -152,7 +152,7 @@ var abp = abp || {};
}
}

abp.utils.requestFullscreen = function() {
abp.utils.requestFullscreen = function () {
var elem = document.documentElement;
if (!document.fullscreenElement && !document.mozFullScreenElement &&
!document.webkitFullscreenElement && !document.msFullscreenElement) {
Expand All @@ -168,7 +168,7 @@ var abp = abp || {};
}
}

abp.utils.exitFullscreen = function() {
abp.utils.exitFullscreen = function () {
if (!(!document.fullscreenElement && !document.mozFullScreenElement &&
!document.webkitFullscreenElement && !document.msFullscreenElement)) {
if (document.exitFullscreen) {
Expand Down Expand Up @@ -230,4 +230,19 @@ var abp = abp || {};
}, 250);
}
};

abp.utils.removeOidcUser = function () {
for (var i = 0; i < sessionStorage.length; i++) {
var key = sessionStorage.key(i);
if (key.startsWith('oidc.user:')) {
sessionStorage.removeItem(key);
}
}
for (var i = 0; i < localStorage.length; i++) {
var key = localStorage.key(i);
if (key.startsWith('oidc.user:')) {
localStorage.removeItem(key);
}
}
}
})();
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
using Volo.Abp;
using Volo.Abp.AspNetCore.Components.WebAssembly.WebApp;
using Volo.Abp.Http.Client.Authentication;
using Volo.Abp.Security.Claims;

namespace Microsoft.Extensions.DependencyInjection;

Expand All @@ -16,7 +15,6 @@ public static IServiceCollection AddBlazorWebAppServices([NotNull] this IService

services.AddSingleton<AuthenticationStateProvider, RemoteAuthenticationStateProvider>();
services.Replace(ServiceDescriptor.Transient<IAbpAccessTokenProvider, CookieBasedWebAssemblyAbpAccessTokenProvider>());
services.Replace(ServiceDescriptor.Transient<ICurrentPrincipalAccessor, RemoteCurrentPrincipalAccessor>());

return services;
}
Expand All @@ -27,7 +25,6 @@ public static IServiceCollection AddBlazorWebAppTieredServices([NotNull] this IS

services.AddScoped<AuthenticationStateProvider, RemoteAuthenticationStateProvider>();
services.Replace(ServiceDescriptor.Singleton<IAbpAccessTokenProvider, PersistentComponentStateAbpAccessTokenProvider>());
services.Replace(ServiceDescriptor.Transient<ICurrentPrincipalAccessor, RemoteCurrentPrincipalAccessor>());

return services;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
<PackageReference Include="Microsoft.AspNetCore.Components.Authorization" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" />
<PackageReference Include="Microsoft.AspNetCore.WebUtilities" />
<PackageReference Include="IdentityModel" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
using System;
using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Volo.Abp.AspNetCore.Components.Web;
Expand Down Expand Up @@ -47,6 +50,26 @@ public override void ConfigureServices(ServiceConfigurationContext context)
.AddProvider(new AbpExceptionHandlingLoggerProvider(context.Services));
}

public override void PostConfigureServices(ServiceConfigurationContext context)
{
var msAuthenticationStateProvider = context.Services.FirstOrDefault(x => x.ServiceType == typeof(AuthenticationStateProvider));
if (msAuthenticationStateProvider != null &&
msAuthenticationStateProvider.ImplementationType != null &&
msAuthenticationStateProvider.ImplementationType.IsGenericType &&
msAuthenticationStateProvider.ImplementationType.GetGenericTypeDefinition() == typeof(RemoteAuthenticationService<,,>))
{
var webAssemblyAuthenticationStateProviderType = typeof(WebAssemblyAuthenticationStateProvider<,,>).MakeGenericType(
msAuthenticationStateProvider.ImplementationType.GenericTypeArguments[0],
msAuthenticationStateProvider.ImplementationType.GenericTypeArguments[1],
msAuthenticationStateProvider.ImplementationType.GenericTypeArguments[2]);

context.Services.AddScoped(webAssemblyAuthenticationStateProviderType, webAssemblyAuthenticationStateProviderType);

context.Services.Remove(msAuthenticationStateProvider);
context.Services.AddScoped(typeof(AuthenticationStateProvider), sp => sp.GetRequiredService(webAssemblyAuthenticationStateProviderType));
}
}

public override void OnApplicationInitialization(ApplicationInitializationContext context)
{
AsyncHelper.RunSync(() => OnApplicationInitializationAsync(context));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
using System;
using System.Collections.Concurrent;
using System.Linq;
using System.Net.Http;
using System.Security.Claims;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using IdentityModel.Client;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Microsoft.JSInterop;

namespace Volo.Abp.AspNetCore.Components.WebAssembly;

/// <summary>
/// Blazor requests a new token each time it is initialized/refreshed.
/// This class is used to revoke a token that is no longer in use.
/// </summary>
public class WebAssemblyAuthenticationStateProvider<TRemoteAuthenticationState, TAccount, TProviderOptions> : RemoteAuthenticationService<TRemoteAuthenticationState, TAccount, TProviderOptions>
where TRemoteAuthenticationState : RemoteAuthenticationState
where TProviderOptions : new()
where TAccount : RemoteUserAccount
{
protected ILogger<RemoteAuthenticationService<TRemoteAuthenticationState, TAccount, TProviderOptions>> Logger { get; }
protected WebAssemblyCachedApplicationConfigurationClient WebAssemblyCachedApplicationConfigurationClient { get; }
protected IOptions<WebAssemblyAuthenticationStateProviderOptions> WebAssemblyAuthenticationStateProviderOptions { get; }
protected IHttpClientFactory HttpClientFactory { get; }

protected readonly static ConcurrentDictionary<string, string> AccessTokens = new ConcurrentDictionary<string, string>();

public WebAssemblyAuthenticationStateProvider(
IJSRuntime jsRuntime,
IOptionsSnapshot<RemoteAuthenticationOptions<TProviderOptions>> options,
NavigationManager navigation,
AccountClaimsPrincipalFactory<TAccount> accountClaimsPrincipalFactory,
ILogger<RemoteAuthenticationService<TRemoteAuthenticationState, TAccount, TProviderOptions>>? logger,
WebAssemblyCachedApplicationConfigurationClient webAssemblyCachedApplicationConfigurationClient,
IOptions<WebAssemblyAuthenticationStateProviderOptions> webAssemblyAuthenticationStateProviderOptions,
IHttpClientFactory httpClientFactory)
: base(jsRuntime, options, navigation, accountClaimsPrincipalFactory, logger)
{
Logger = logger ?? NullLogger<RemoteAuthenticationService<TRemoteAuthenticationState, TAccount, TProviderOptions>>.Instance;

WebAssemblyCachedApplicationConfigurationClient = webAssemblyCachedApplicationConfigurationClient;
WebAssemblyAuthenticationStateProviderOptions = webAssemblyAuthenticationStateProviderOptions;
HttpClientFactory = httpClientFactory;

AuthenticationStateChanged += async state =>
{
var user = await state;
if (user.User.Identity == null || !user.User.Identity.IsAuthenticated)
{
return;
}

var accessToken = await FindAccessTokenAsync();
if (!accessToken.IsNullOrWhiteSpace())
{
AccessTokens.TryAdd(accessToken, accessToken);
}

await TryRevokeOldAccessTokensAsync();
};
}

protected async override ValueTask<ClaimsPrincipal> GetAuthenticatedUser()
{
var accessToken = await FindAccessTokenAsync();
if (!accessToken.IsNullOrWhiteSpace())
{
AccessTokens.TryAdd(accessToken, accessToken);
}

await TryRevokeOldAccessTokensAsync();

return await base.GetAuthenticatedUser();
}

public async override Task<AuthenticationState> GetAuthenticationStateAsync()
{
var accessToken = await FindAccessTokenAsync();
if (!accessToken.IsNullOrWhiteSpace())
{
AccessTokens.TryAdd(accessToken, accessToken);
}

await TryRevokeOldAccessTokensAsync();

var state = await base.GetAuthenticationStateAsync();
var applicationConfigurationDto = await WebAssemblyCachedApplicationConfigurationClient.GetAsync();
if (state.User.Identity != null && state.User.Identity.IsAuthenticated && !applicationConfigurationDto.CurrentUser.IsAuthenticated)
{
await WebAssemblyCachedApplicationConfigurationClient.InitializeAsync();
}

return state;
}

protected virtual async Task<string?> FindAccessTokenAsync()
{
var result = await RequestAccessToken();
if (result.Status != AccessTokenResultStatus.Success)
{
return null;
}

result.TryGetToken(out var token);
return token?.Value;
}

protected virtual async Task TryRevokeOldAccessTokensAsync()
{
if (AccessTokens.Count <= 1)
{
return;
}

var oidcProviderOptions = Options.ProviderOptions?.As<OidcProviderOptions>();
var authority = oidcProviderOptions?.Authority;
var clientId = oidcProviderOptions?.ClientId;

if (authority.IsNullOrWhiteSpace() || clientId.IsNullOrWhiteSpace())
{
return;
}

var revoked = false;
var revokeAccessTokens = AccessTokens.Select(x => x.Value);
var currentAccessToken = await FindAccessTokenAsync();
foreach (var accessToken in revokeAccessTokens)
{
if (accessToken == currentAccessToken)
{
continue;
}

var httpClient = HttpClientFactory.CreateClient(nameof(WebAssemblyAuthenticationStateProvider<TRemoteAuthenticationState, TAccount, TProviderOptions>));
var result = await httpClient.RevokeTokenAsync(new TokenRevocationRequest
{
Address = authority.EnsureEndsWith('/') + WebAssemblyAuthenticationStateProviderOptions.Value.TokenRevocationUrl,
ClientId = clientId,
Token = accessToken,
});

if (!result.IsError)
{
AccessTokens.TryRemove(accessToken, out _);
revoked = true;
}
else
{
Logger.LogError(result.Raw);
}
}

if (revoked)
{
await WebAssemblyCachedApplicationConfigurationClient.InitializeAsync();
}
}
}

internal class OidcUser
{
[JsonPropertyName("access_token")]
public string? AccessToken { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Volo.Abp.AspNetCore.Components.WebAssembly;

public class WebAssemblyAuthenticationStateProviderOptions
{
public string TokenRevocationUrl { get; set; } = "connect/revocat";
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Threading.Tasks;
using Microsoft.JSInterop;
using Volo.Abp.AspNetCore.Components.Web.Security;
using Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations;
using Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations.ClientProxies;
Expand All @@ -20,18 +21,22 @@ public class WebAssemblyCachedApplicationConfigurationClient : ICachedApplicatio

protected ApplicationConfigurationChangedService ApplicationConfigurationChangedService { get; }

protected IJSRuntime JSRuntime { get; }

public WebAssemblyCachedApplicationConfigurationClient(
AbpApplicationConfigurationClientProxy applicationConfigurationClientProxy,
ApplicationConfigurationCache cache,
ICurrentTenantAccessor currentTenantAccessor,
AbpApplicationLocalizationClientProxy applicationLocalizationClientProxy,
ApplicationConfigurationChangedService applicationConfigurationChangedService)
ApplicationConfigurationChangedService applicationConfigurationChangedService,
IJSRuntime jsRuntime)
{
ApplicationConfigurationClientProxy = applicationConfigurationClientProxy;
Cache = cache;
CurrentTenantAccessor = currentTenantAccessor;
ApplicationLocalizationClientProxy = applicationLocalizationClientProxy;
ApplicationConfigurationChangedService = applicationConfigurationChangedService;
JSRuntime = jsRuntime;
}

public virtual async Task InitializeAsync()
Expand All @@ -53,6 +58,11 @@ public virtual async Task InitializeAsync()

Cache.Set(configurationDto);

if (!configurationDto.CurrentUser.IsAuthenticated)
{
await JSRuntime.InvokeVoidAsync("abp.utils.removeOidcUser");
}

ApplicationConfigurationChangedService.NotifyChanged();

CurrentTenantAccessor.Current = new BasicTenantInfo(
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@
using Volo.Abp.DependencyInjection;
using Volo.Abp.Security.Claims;

namespace Volo.Abp.AspNetCore.Components.WebAssembly.WebApp;
namespace Volo.Abp.AspNetCore.Components.WebAssembly;

public class RemoteCurrentPrincipalAccessor : CurrentPrincipalAccessorBase, ITransientDependency
public class WebAssemblyRemoteCurrentPrincipalAccessor : CurrentPrincipalAccessorBase, ITransientDependency
{
protected ApplicationConfigurationCache ApplicationConfigurationCache { get; }

public RemoteCurrentPrincipalAccessor(ApplicationConfigurationCache applicationConfigurationCache)
public WebAssemblyRemoteCurrentPrincipalAccessor(ApplicationConfigurationCache applicationConfigurationCache)
{
ApplicationConfigurationCache = applicationConfigurationCache;
}
Expand Down Expand Up @@ -75,6 +75,10 @@ protected override ClaimsPrincipal GetClaimsPrincipal()
{
claims.Add(new Claim(AbpClaimTypes.PhoneNumberVerified, applicationConfiguration.CurrentUser.PhoneNumberVerified.ToString()));
}
if (applicationConfiguration.CurrentUser.SessionId != null)
{
claims.Add(new Claim(AbpClaimTypes.SessionId, applicationConfiguration.CurrentUser.SessionId));
}

if (!applicationConfiguration.CurrentUser.Roles.IsNullOrEmpty())
{
Expand All @@ -84,6 +88,6 @@ protected override ClaimsPrincipal GetClaimsPrincipal()
}
}

return new ClaimsPrincipal(new ClaimsIdentity(claims, authenticationType: nameof(RemoteCurrentPrincipalAccessor)));
return new ClaimsPrincipal(new ClaimsIdentity(claims, authenticationType: nameof(WebAssemblyRemoteCurrentPrincipalAccessor)));
}
}
Loading
Loading