From cb4ddf92113c93eeab17a502cfd6d54f6418c239 Mon Sep 17 00:00:00 2001 From: maliming Date: Thu, 4 Apr 2024 14:38:17 +0800 Subject: [PATCH] Revoke unused access tokens from Blazor. --- ...p.AspNetCore.Components.WebAssembly.csproj | 1 + .../WebAssemblyAuthenticationStateProvider.cs | 118 +++++++++++++++--- ...emblyAuthenticationStateProviderOptions.cs | 6 + ...bAssemblyRemoteCurrentPrincipalAccessor.cs | 4 + .../CurrentUserDto.cs | 2 + .../AbpApplicationConfigurationAppService.cs | 3 +- 6 files changed, 119 insertions(+), 15 deletions(-) create mode 100644 framework/src/Volo.Abp.AspNetCore.Components.WebAssembly/Volo/Abp/AspNetCore/Components/WebAssembly/WebAssemblyAuthenticationStateProviderOptions.cs diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly/Volo.Abp.AspNetCore.Components.WebAssembly.csproj b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly/Volo.Abp.AspNetCore.Components.WebAssembly.csproj index 8d720a19ee..d9e5081421 100644 --- a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly/Volo.Abp.AspNetCore.Components.WebAssembly.csproj +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly/Volo.Abp.AspNetCore.Components.WebAssembly.csproj @@ -27,6 +27,7 @@ + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly/Volo/Abp/AspNetCore/Components/WebAssembly/WebAssemblyAuthenticationStateProvider.cs b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly/Volo/Abp/AspNetCore/Components/WebAssembly/WebAssemblyAuthenticationStateProvider.cs index dc6b3d3536..ee9fdcc523 100644 --- a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly/Volo/Abp/AspNetCore/Components/WebAssembly/WebAssemblyAuthenticationStateProvider.cs +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly/Volo/Abp/AspNetCore/Components/WebAssembly/WebAssemblyAuthenticationStateProvider.cs @@ -1,9 +1,15 @@ using System; +using System.Collections.Concurrent; +using System.Linq; +using System.Net.Http; +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; @@ -14,20 +20,12 @@ public class WebAssemblyAuthenticationStateProvider> Logger { get; } protected WebAssemblyCachedApplicationConfigurationClient WebAssemblyCachedApplicationConfigurationClient { get; } + protected IOptions WebAssemblyAuthenticationStateProviderOptions { get; } + protected IHttpClientFactory HttpClientFactory { get; } - [Obsolete] - public WebAssemblyAuthenticationStateProvider( - IJSRuntime jsRuntime, - IOptionsSnapshot> options, - NavigationManager navigation, - AccountClaimsPrincipalFactory accountClaimsPrincipalFactory, - ILogger> logger, - WebAssemblyCachedApplicationConfigurationClient webAssemblyCachedApplicationConfigurationClient) - : base(jsRuntime, options, navigation, accountClaimsPrincipalFactory) - { - WebAssemblyCachedApplicationConfigurationClient = webAssemblyCachedApplicationConfigurationClient; - } + protected readonly static ConcurrentDictionary AccessTokens = new ConcurrentDictionary(); public WebAssemblyAuthenticationStateProvider( IJSRuntime jsRuntime, @@ -35,11 +33,33 @@ public class WebAssemblyAuthenticationStateProvider accountClaimsPrincipalFactory, ILogger>? logger, - ILogger> logger1, - WebAssemblyCachedApplicationConfigurationClient webAssemblyCachedApplicationConfigurationClient) + WebAssemblyCachedApplicationConfigurationClient webAssemblyCachedApplicationConfigurationClient, + IOptions webAssemblyAuthenticationStateProviderOptions, + IHttpClientFactory httpClientFactory) : base(jsRuntime, options, navigation, accountClaimsPrincipalFactory, logger) { + Logger = logger ?? NullLogger>.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(); + }; } public async override Task GetAuthenticationStateAsync() @@ -51,6 +71,76 @@ public class WebAssemblyAuthenticationStateProvider 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(); + var authority = oidcProviderOptions?.Authority; + var clientId = oidcProviderOptions?.ClientId; + + if (authority.IsNullOrWhiteSpace() || clientId.IsNullOrWhiteSpace()) + { + return; + } + + 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)); + var result = await httpClient.RevokeTokenAsync(new TokenRevocationRequest + { + Address = authority.EnsureEndsWith('/') + WebAssemblyAuthenticationStateProviderOptions.Value.TokenRevocationUrl, + ClientId = clientId, + Token = accessToken, + }); + + if (!result.IsError) + { + AccessTokens.TryRemove(accessToken, out _); + } + else + { + Logger.LogError(result.Raw); + } + } + } +} + +internal class OidcUser +{ + [JsonPropertyName("access_token")] + public string? AccessToken { get; set; } } diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly/Volo/Abp/AspNetCore/Components/WebAssembly/WebAssemblyAuthenticationStateProviderOptions.cs b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly/Volo/Abp/AspNetCore/Components/WebAssembly/WebAssemblyAuthenticationStateProviderOptions.cs new file mode 100644 index 0000000000..c8d904d872 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly/Volo/Abp/AspNetCore/Components/WebAssembly/WebAssemblyAuthenticationStateProviderOptions.cs @@ -0,0 +1,6 @@ +namespace Volo.Abp.AspNetCore.Components.WebAssembly; + +public class WebAssemblyAuthenticationStateProviderOptions +{ + public string TokenRevocationUrl { get; set; } = "connect/revocat"; +} diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly/Volo/Abp/AspNetCore/Components/WebAssembly/WebAssemblyRemoteCurrentPrincipalAccessor.cs b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly/Volo/Abp/AspNetCore/Components/WebAssembly/WebAssemblyRemoteCurrentPrincipalAccessor.cs index e96e315144..b94dc89966 100644 --- a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly/Volo/Abp/AspNetCore/Components/WebAssembly/WebAssemblyRemoteCurrentPrincipalAccessor.cs +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly/Volo/Abp/AspNetCore/Components/WebAssembly/WebAssemblyRemoteCurrentPrincipalAccessor.cs @@ -75,6 +75,10 @@ public class WebAssemblyRemoteCurrentPrincipalAccessor : CurrentPrincipalAccesso { 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()) { diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/CurrentUserDto.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/CurrentUserDto.cs index 63a4b7a0e9..9870165e8c 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/CurrentUserDto.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/CurrentUserDto.cs @@ -34,4 +34,6 @@ public class CurrentUserDto public bool PhoneNumberVerified { get; set; } public string[] Roles { get; set; } = default!; + + public string? SessionId { get; set; } } diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/AbpApplicationConfigurationAppService.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/AbpApplicationConfigurationAppService.cs index 38d68aa96c..f00c4ac250 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/AbpApplicationConfigurationAppService.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/AbpApplicationConfigurationAppService.cs @@ -158,7 +158,8 @@ public class AbpApplicationConfigurationAppService : ApplicationService, IAbpApp EmailVerified = _currentUser.EmailVerified, PhoneNumber = _currentUser.PhoneNumber, PhoneNumberVerified = _currentUser.PhoneNumberVerified, - Roles = _currentUser.Roles + Roles = _currentUser.Roles, + SessionId = _currentUser.FindSessionId() }; }