Hello, we are using a Blazor Maui Hybrid app as a mobile application that targets android and iOS.
We have our backend using the microservices template, with an auth server.
We use the oidc code flow to login and get our permissions.
We have two menu items hidden behind permissions :
//FileManagement
context.Menu.AddItem(
new ApplicationMenuItem(
ProjectMenus.FileManagement,
l["Menu:FileManagement"],
ProjectRoutes.FileManagement,
icon: "fa fa-folder-open",
order: 5
).RequirePermissions(FileManagementPermissions.FileDescriptor.Default)
We have found no other solution than a Task.Delay after the TriggerUserChanged() in our project CodeFlowExternalAuthService :
public class ProjectCodeFlowExternalAuthService : ExternalAuthServiceBase
{
private readonly OidcClient _oidcClient;
private readonly IStorage _storage;
public ProjectCodeFlowExternalAuthService (
IAccessTokenStore accessTokenStore,
IStorage storage,
OidcClient oidcClient)
: base(accessTokenStore)
{
_oidcClient = oidcClient;
_storage = storage;
}
public async override Task<LoginResult> LoginAsync(LoginInput input)
{
var loginResult = await _oidcClient.LoginAsync(new LoginRequest());
if (loginResult.IsError)
{
return LoginResult.Failed(loginResult.Error, loginResult.ErrorDescription);
}
await SetTokenCacheAsync(loginResult.AccessToken, loginResult.RefreshToken);
var currentUser = new ClaimsPrincipal(new ClaimsIdentity(new JwtSecurityTokenHandler().ReadJwtToken(loginResult.AccessToken).Claims, AuthenticationType));
TriggerUserChanged(currentUser);
// Our hack that needs to be more than 500 when we're deployed
await Task.Delay(500);
return LoginResult.Success();
}
public async override Task SignOutAsync()
{
var logoutResult = await _oidcClient.LogoutAsync();
await ClearTokenCacheAsync();
var currentUser = new ClaimsPrincipal(new ClaimsIdentity());
TriggerUserChanged(currentUser);
// Our hack that needs to be more than 500 when we're deployed
await Task.Delay(500);
}
public async Task<string> GetAccessTokenAsync()
{
var token = await _storage.GetAsync(OidcConsts.AccessTokenKeyName);
if (!token.IsNullOrEmpty())
{
var jwtToken = new JwtSecurityTokenHandler().ReadJwtToken(token);
if (jwtToken.ValidTo <= DateTime.UtcNow)
{
var newToken = await TryRefreshTokenAsync();
if (!newToken.IsNullOrEmpty())
{
return newToken;
}
}
}
return token;
}
public async Task<string> TryRefreshTokenAsync()
{
var refreshToken = await _storage.GetAsync(OidcConsts.RefreshTokenKeyName);
if (!refreshToken.IsNullOrEmpty())
{
var refreshResult = await _oidcClient.RefreshTokenAsync(refreshToken);
if (!refreshResult.IsError)
{
await SetTokenCacheAsync(refreshResult.AccessToken, refreshResult.RefreshToken);
return refreshResult.AccessToken;
}
}
return string.Empty;
}
private async Task SetTokenCacheAsync(string accessToken, string refreshToken)
{
await AccessTokenStore.SetAccessTokenAsync(accessToken);
await _storage.SetAsync(OidcConsts.AccessTokenKeyName, accessToken);
await _storage.SetAsync(OidcConsts.RefreshTokenKeyName, refreshToken);
}
private async Task ClearTokenCacheAsync()
{
await AccessTokenStore.SetAccessTokenAsync(null);
await _storage.RemoveAsync(OidcConsts.AccessTokenKeyName);
await _storage.RemoveAsync(OidcConsts.RefreshTokenKeyName);
}
}
I'm sure the framework offers a better alternative to propagate the permissions change to the menus using the AuthState. I can't see how..
This issue happens on the first login only (without the Delay), if we close and open the app, the menus are shown.
So the permissions are propagating but not fast enough or the menus aren't notified of the change..
Thank you in advance for your answer!
2 Answer(s)
-
0
Hi,
Since it's a still a Blazor application, you can implement your own
AuthenticationStateProvider
and notify authentication system from single point like something similar:[Volo.Abp.DependencyInjection.Dependency(ReplaceServices = true)] [ExposeServices(typeof(AuthenticationStateProvider))] public class CustomAuthenticationStateProvider : AuthenticationStateProvider, ISingletonDependency { private ClaimsPrincipal _currentUser = new ClaimsPrincipal(new ClaimsIdentity()); public void NotifyUserChanged(ClaimsPrincipal user) { _currentUser = user; NotifyAuthenticationStateChanged(GetAuthenticationStateAsync()); } public override Task GetAuthenticationStateAsync() { return Task.FromResult(new AuthenticationState(_currentUser)); } }
You can call
NotifyUserChanged
by injecting singletonCustomAuthenticationStateProvider
service to notify all the existing components about the state is changed.We also faced a similar problem during the investigation, we'll try to find a solution on framework level, but for now this can be a workaround
-
0
Thank you for your answer, I implemented the solution and am still facing the same issue :
using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using MyProject.MauiBlazor.Consts; using MyProject.MauiBlazor.Storage; using IdentityModel.OidcClient; using Microsoft.AspNetCore.Components.Authorization; using Volo.Abp.Account.Pro.Public.MauiBlazor.OAuth; using LoginResult = Volo.Abp.Account.Pro.Public.MauiBlazor.OAuth.LoginResult; namespace MyProject.MauiBlazor.OAuth; public class MyProjectCodeFlowExternalAuthService : ExternalAuthServiceBase { private readonly OidcClient _oidcClient; private readonly IStorage _storage; private readonly AuthenticationStateProvider _authenticationStateProvider; public MyProjectCodeFlowExternalAuthService( IAccessTokenStore accessTokenStore, IStorage storage, OidcClient oidcClient, AuthenticationStateProvider authenticationStateProvider) : base(accessTokenStore) { _oidcClient = oidcClient; _storage = storage; _authenticationStateProvider = authenticationStateProvider; } public override async Task<LoginResult> LoginAsync(LoginInput input) { var loginResult = await _oidcClient.LoginAsync(new LoginRequest()); if (loginResult.IsError) { return LoginResult.Failed(loginResult.Error, loginResult.ErrorDescription); } await SetTokenCacheAsync(loginResult.AccessToken, loginResult.RefreshToken); var currentUser = new ClaimsPrincipal(new ClaimsIdentity( new JwtSecurityTokenHandler().ReadJwtToken(loginResult.AccessToken).Claims, authenticationType: AuthenticationType )); (_authenticationStateProvider as MyProjectAuthenticationStateProvider)?.NotifyUserChanged(currentUser); return LoginResult.Success(); } public override async Task SignOutAsync() { var logoutResult = await _oidcClient.LogoutAsync(); await ClearTokenCacheAsync(); var currentUser = new ClaimsPrincipal(new ClaimsIdentity()); (_authenticationStateProvider as MyProjectAuthenticationStateProvider)?.NotifyUserChanged(currentUser); } public async Task<string> GetAccessTokenAsync() { var token = await _storage.GetAsync(OidcConsts.AccessTokenKeyName); if (!token.IsNullOrEmpty()) { var jwtToken = new JwtSecurityTokenHandler().ReadJwtToken(token); if (jwtToken.ValidTo <= DateTime.UtcNow) { var newToken = await TryRefreshTokenAsync(); if (!newToken.IsNullOrEmpty()) { return newToken; } } } return token; } public async Task<string> TryRefreshTokenAsync() { var refreshToken = await _storage.GetAsync(OidcConsts.RefreshTokenKeyName); if (!refreshToken.IsNullOrEmpty()) { var refreshResult = await _oidcClient.RefreshTokenAsync(refreshToken); if (!refreshResult.IsError) { await SetTokenCacheAsync(refreshResult.AccessToken, refreshResult.RefreshToken); // Also update the authentication state when token is refreshed var currentUser = new ClaimsPrincipal(new ClaimsIdentity( new JwtSecurityTokenHandler().ReadJwtToken(refreshResult.AccessToken).Claims, authenticationType: AuthenticationType )); (_authenticationStateProvider as MyProjectAuthenticationStateProvider)?.NotifyUserChanged(currentUser); return refreshResult.AccessToken; } } return string.Empty; } private async Task SetTokenCacheAsync(string accessToken, string refreshToken) { await AccessTokenStore.SetAccessTokenAsync(accessToken); await _storage.SetAsync(OidcConsts.AccessTokenKeyName, accessToken); await _storage.SetAsync(OidcConsts.RefreshTokenKeyName, refreshToken); } private async Task ClearTokenCacheAsync() { await AccessTokenStore.SetAccessTokenAsync(null); await _storage.RemoveAsync(OidcConsts.AccessTokenKeyName); await _storage.RemoveAsync(OidcConsts.RefreshTokenKeyName); } }
And this is the state provider
using Microsoft.AspNetCore.Components.Authorization; using System.Security.Claims; using Volo.Abp.DependencyInjection; [Volo.Abp.DependencyInjection.Dependency(ReplaceServices = true)] [ExposeServices(typeof(AuthenticationStateProvider))] public class MyProjectAuthenticationStateProvider : AuthenticationStateProvider, ISingletonDependency { private ClaimsPrincipal _currentUser = new ClaimsPrincipal(new ClaimsIdentity()); public void NotifyUserChanged(ClaimsPrincipal user) { _currentUser = user; NotifyAuthenticationStateChanged(GetAuthenticationStateAsync()); } public override Task<AuthenticationState> GetAuthenticationStateAsync() { return Task.FromResult(new AuthenticationState(_currentUser)); } }
Am I doing something wrong?
Also want to mention that I tried keeping TriggerUserChanged that comes from ExternalAuthServiceBase, no success