Starts in:
1 DAY
19 HRS
11 MIN
23 SEC
Starts in:
1 D
19 H
11 M
23 S
Open Closed

access_token expiration is not triggering the signout in Blazor Server #7023


User avatar
0
antosubash-dev created

Hi,

When the access token expires, the user is neither signed out nor is the token refreshed. We expect that the user should be prompted to log in once the token has expired. I have attached the repository link for reproducing this issue. Please let me know if you need any additional information.

Simply run the application and leave it idle for a few minutes. The application will encounter an ‘unauthorized’ error without logging out, but it will resume functioning after a refresh.

Please advise on how to handle token expiration in a Blazor server application.

Provide us with the following info:

  • ABP Framework version: v8.1
  • UI Type: Blazor Server
  • Database System: EF Core SQL Server
  • **Tiered **: yes

Repo to reproduce: https://github.com/antosubash/ABPAccessTokenProblem


9 Answer(s)
  • User Avatar
    0
    liangshiwei created
    Support Team Fullstack Developer

    Hi,

    You can try this:

    [Dependency(ReplaceServices = true)]
    [ExposeServices(typeof(IUserExceptionInformer))]
    public class MyUserExceptionInformer : IUserExceptionInformer, IScopedDependency
    {
        protected IHttpContextAccessor HttpContextAccessor { get; }
        
        public ILogger<UserExceptionInformer> Logger { get; set; }
        protected IUiMessageService MessageService { get; }
        protected IExceptionToErrorInfoConverter ExceptionToErrorInfoConverter { get; }
    
        protected AbpExceptionHandlingOptions Options { get; }
        
        protected IJSRuntime JsRuntime { get; }
        
        public MyUserExceptionInformer(
            IUiMessageService messageService,
            IExceptionToErrorInfoConverter exceptionToErrorInfoConverter,
            IOptions<AbpExceptionHandlingOptions> options,
            IHttpContextAccessor httpContextAccessor, IJSRuntime jsRuntime)
        {
            MessageService = messageService;
            ExceptionToErrorInfoConverter = exceptionToErrorInfoConverter;
            Options = options.Value;
            Logger = NullLogger<UserExceptionInformer>.Instance;
            HttpContextAccessor = httpContextAccessor;
            JsRuntime = jsRuntime;
        }
        
        public new void Inform(UserExceptionInformerContext context)
        {
            //TODO: Create sync versions of the MessageService APIs.
    
            var errorInfo = GetErrorInfo(context);
    
            if (errorInfo.Details.IsNullOrEmpty())
            {
                MessageService.Error(errorInfo.Message!);
            }
            else
            {
                MessageService.Error(errorInfo.Details!, errorInfo.Message);
            }
        }
    
        public new async Task InformAsync(UserExceptionInformerContext context)
        {
            var errorInfo = GetErrorInfo(context);
    
    
            if (errorInfo.Message == "Unauthorized")
            {
                var httpContext = HttpContextAccessor.HttpContext;
                var openIdConnectOptions = httpContext.RequestServices.GetRequiredService<IOptionsMonitor<OpenIdConnectOptions>>().Get("oidc");
                var response = await openIdConnectOptions.Backchannel.IntrospectTokenAsync(new TokenIntrospectionRequest
                {
                    Address = openIdConnectOptions.Configuration?.IntrospectionEndpoint ?? openIdConnectOptions.Authority!.EnsureEndsWith('/') + "connect/introspect",
                    ClientId = openIdConnectOptions.ClientId!,
                    ClientSecret = openIdConnectOptions.ClientSecret,
                    Token = await httpContext.GetTokenAsync("access_token")
                });
    
                if (response.IsError || !response.IsActive)
                {
                    // handle the token is not active
                    await JsRuntime.InvokeVoidAsync("eval", "window.location.href = '/Account/Logout';");
                }
                
                return;
            }
            
            if (errorInfo.Details.IsNullOrEmpty())
            {
                await MessageService.Error(errorInfo.Message!);
            }
            else
            {
                await MessageService.Error(errorInfo.Details!, errorInfo.Message);
            }
            
        }
        
        protected virtual RemoteServiceErrorInfo GetErrorInfo(UserExceptionInformerContext context)
        {
            return ExceptionToErrorInfoConverter.Convert(context.Exception, options =>
            {
                options.SendExceptionsDetailsToClients = Options.SendExceptionsDetailsToClients;
                options.SendStackTraceToClients = Options.SendStackTraceToClients;
            });
        }
        
    }
    
    public override void ConfigureServices(ServiceConfigurationContext context)
    {
        context.Services.RemoveAll(x => x.ImplementationType == typeof(UserExceptionInformer));
    }
    
  • User Avatar
    0
    greifeneder created

    Is there a reason why the Blazor Server template does not use the standard OpenId refresh token flow to obtain new Access Tokens?

  • User Avatar
    0
    liangshiwei created
    Support Team Fullstack Developer

    Hi,

    Although the token has expired, the authorization server login status has not expired, so when you refresh the page it will challenge again and log in directly.

  • User Avatar
    0
    greifeneder created

    Hi,

    thank you for your reply, although it doesn't really answer the question.

    Even though we might get a proper logout working, and the user can at any time refresh the page - why should (s)he?!

    We don't see it as good user experience to just trigger a logout every 30mins. That's not how websites should behave nowadays.

    So is there any reason why the template doesn't do a refresh token flow and why we shouldn't? Or is it just not implemented yet?

    Other Framework also refresh it in the background. Like Duende BFF uses AccessTokenManagement. Here is the RefreshUserAccessToken in Duende, for example https://github.com/DuendeSoftware/Duende.AccessTokenManagement/blob/2a6d0a60a564a590ee4f0c87c08e8406eb753e88/src/Duende.AccessTokenManagement.OpenIdConnect/UserAccessTokenManagementService.cs#L145

  • User Avatar
    0
    liangshiwei created
    Support Team Fullstack Developer

    Hi,

    You can refer to the Microsoft's official implementation https://github.com/dotnet/blazor-samples/blob/main/8.0/BlazorWebAppOidc/BlazorWebAppOidc/CookieOidcRefresher.cs https://github.com/dotnet/blazor-samples/blob/main/8.0/BlazorWebAppOidc/BlazorWebAppOidc/CookieOidcServiceCollectionExtensions.cs

  • User Avatar
    0
    liangshiwei created
    Support Team Fullstack Developer

    Hi,

    I will consider adding refresh token to Blazor UI in the next version.

  • User Avatar
    0
    liangshiwei created
    Support Team Fullstack Developer

    https://github.com/abpframework/abp/issues/19522

  • User Avatar
    0
    greifeneder created

    Thanks, is there already an estimate for 8.2 release?

  • User Avatar
    0
    liangshiwei created
    Support Team Fullstack Developer

    https://github.com/abpframework/abp/milestone/95 Not yet.

Made with ❤️ on ABP v9.1.0-preview. Updated on November 20, 2024, 13:06