Open Closed

User timeout/permission behaviour #5022


User avatar
0
Talal created
  • ABP Framework version: v7.1.0
  • UI type: MVC
  • DB provider: EF Core
  • Tiered (MVC) or Identity Server Separated (Angular): yes
    • Exception message and stack trace:
  • Steps to reproduce the issue:"

This is happening in production in multiple applications.

I may be doing something wrong so please be patient and let me know what I can do to fix it.

  • The user is logged in. (image 1)
  • The session timeout after x mins (expected)
  • When the user comes back the page is still on image 1. Clicks anywhere. Notice that the menu is removed (permissions are gone because of the user no longer logged in. Expected)
  • However, notice that now the user is still showing up in the top right corner (admin). (Image 2)
  • I logged out
  • I log in again.
  • Still shows up as if the current user has no permission to do anything (the user is admin and has full permissions) (Image 2)
  • Sometimes if the user times out on a page and clicks a link after a few hours the auth server goes into an endless loop

This is a production issue. I appreciate some help/direction.

Thank you


25 Answer(s)
  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    hi

    Please try using CheckExpiresAt with AddCookie.

    context.Services.AddAuthentication(options =>
    {
        //...
    })
    .AddCookie("Cookies", options =>
    {
        options.ExpireTimeSpan = TimeSpan.FromDays(365);
        options.CheckExpiresAt();
    })
    .AddAbpOpenIdConnect("oidc", options =>
    {
        //...
    });
    
    using System;
    using System.Globalization;
    using Microsoft.AspNetCore.Authentication;
    using Microsoft.AspNetCore.Authentication.Cookies;
    
    namespace MyCompanyName.MyProjectName.Web;
    
    public static class CookieAuthenticationOptionsExtensions
    {
        public static CookieAuthenticationOptions CheckExpiresAt(this CookieAuthenticationOptions options)
        {
            var originalHandler = options.Events.OnValidatePrincipal;
            options.Events.OnValidatePrincipal = async principalContext =>
            {
                originalHandler?.Invoke(principalContext);
    
                if (principalContext.Principal != null && principalContext.Principal.Identity != null && principalContext.Principal.Identity.IsAuthenticated)
                {
                    var tokenExpiresAt = principalContext.Properties.Items[".Token.expires_at"];
                    if (tokenExpiresAt != null &&
                        DateTimeOffset.TryParseExact(tokenExpiresAt, "yyyy-MM-ddTHH:mm:ss.fffffffzzz", null, DateTimeStyles.AdjustToUniversal, out var expiresAt) &&
                        expiresAt < DateTimeOffset.UtcNow.Subtract(TimeSpan.FromMinutes(5)))
                    {
                        principalContext.RejectPrincipal();
                        await principalContext.HttpContext.SignOutAsync(principalContext.Scheme.Name);
                    }
                }
            };
    
            return options;
        }
    }
    
    
    
  • User Avatar
    0
    Talal created

    @maliming Sadly it dd not work.

  • User Avatar
    0
    Talal created

    sorry to say but when things get stuck the application becomes useless :( this is production and now even I delete cookies I flush redis I restart the service/the application even restarted production server. no matter what I do the logged in admin now only sees home.

    if (after login as admin) I try to manually type in a secure page, the auth server goes in an endless loop.

    What should I do?

  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    hi

    Please share the full logs of your backend app, including the authserver and web app.

    liming.ma@volosoft.com

  • User Avatar
    0
    Talal created

    sent. Thanks

  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    the auth server goes in an endless loop.

    hi

    The current user doesn't have the Cssea.Cetrs permission, so the endless loop happened.

    We will avoid this problem in the next version.

    Request starting HTTP/2 GET https://apps.cssea.bc.ca/Cetrs - -
    These requirements were not met: PermissionRequirement: Cssea.Cetrs
    
  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    no matter what I do the logged in admin now only sees home.

    Can you share a username and password?

    I will test it online.

    liming.ma@volosoft.com

  • User Avatar
    0
    Talal created

    Actually the user has access to everything

    This is misleading and not correct:

    Request starting HTTP/2 GET https://apps.cssea.bc.ca/Cetrs - - These requirements were not met: PermissionRequirement: Cssea.Cetrs

  • User Avatar
    0
    Talal created

    the site is not accessible externally. Are you available for zoom?

  • User Avatar
    0
    Talal created

    by the way, this is what Web project is using:

       context.Services.AddAuthentication(options =>
            {
                options.DefaultScheme = "Cookies";
                options.DefaultChallengeScheme = "oidc";
            })
            .AddCookie("Cookies", options =>
            {
                options.ExpireTimeSpan = TimeSpan.FromDays(365);
                options.CheckExpiresAt();
            })
            .AddAbpOpenIdConnect("oidc", options =>
            {
                options.Authority = configuration["AuthServer:Authority"];
                options.RequireHttpsMetadata = Convert.ToBoolean(configuration["AuthServer:RequireHttpsMetadata"]);  // true
                options.ResponseType = OpenIdConnectResponseType.CodeIdToken;
    
                options.ClientId = configuration["AuthServer:ClientId"];
                options.ClientSecret = configuration["AuthServer:ClientSecret"];
    
                options.UsePkce = true;
                options.SaveTokens = false;   // I tried true or false
                options.GetClaimsFromUserInfoEndpoint = true;
    
                options.Scope.Add("roles");
                options.Scope.Add("email");
                options.Scope.Add("phone");
                options.Scope.Add("api"); // the API client name
            });
    
  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    hi

    options.SaveTokens = true; //this must set as true.
    
  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    I can check it by zoom.

  • User Avatar
    0
    Talal created

    Whenever you are ready. You can send the link to my email you have it. I am on standby now

  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    Please share your screen, Thanks https://us05web.zoom.us/j/86851103486?pwd=UkFBQzN5OWZLRkdwZzRlS3VtZWJodz09

  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    hi

    I will share some code with you, then you can test it on production.

  • User Avatar
    0
    Talal created

    sure i will wait for your code .. this is a production issue so I am thankful for the help

  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    hi

    Add below code to your Web.Host project

    using System.Threading.Tasks;
    using IdentityModel.Client;
    using Microsoft.AspNetCore.Authentication;
    using Microsoft.Extensions.Logging;
    using Microsoft.IdentityModel.Tokens;
    using Volo.Abp.DependencyInjection;
    using Volo.Abp.Http.Client;
    using Volo.Abp.Http.Client.Authentication;
    using Volo.Abp.Http.Client.IdentityModel.Web;
    using Volo.Abp.IdentityModel;
    
    namespace MyCompanyName.MyProjectName.Web;
    
    [Dependency(ReplaceServices = true)]
    public class MyHttpContextIdentityModelRemoteServiceHttpClientAuthenticator : HttpContextIdentityModelRemoteServiceHttpClientAuthenticator
    {
        public ILogger<MyHttpContextIdentityModelRemoteServiceHttpClientAuthenticator> Logger { get; set; }
    
        public MyHttpContextIdentityModelRemoteServiceHttpClientAuthenticator(IIdentityModelAuthenticationService identityModelAuthenticationService,
            ILogger<MyHttpContextIdentityModelRemoteServiceHttpClientAuthenticator> logger)
            : base(identityModelAuthenticationService)
        {
            Logger = logger;
        }
    
        public async override Task Authenticate(RemoteServiceHttpClientAuthenticateContext context)
        {
            if (context.RemoteService.GetUseCurrentAccessToken() != false)
            {
                var accessToken = await GetAccessTokenFromHttpContextOrNullAsync();
                if (accessToken != null)
                {
                    context.Request.SetBearerToken(accessToken);
                    return;
                }
            }
    
            await base.Authenticate(context);
        }
    
        protected async override Task<string> GetAccessTokenFromHttpContextOrNullAsync()
        {
            var httpContext = HttpContextAccessor?.HttpContext;
            if (httpContext == null)
            {
                Logger.LogError("Could not get HttpContext!");
                return null;
            }
    
            var token = await httpContext.GetTokenAsync("access_token");
            if (token.IsNullOrEmpty())
            {
                Logger.LogError("Could not get access_token!");
                return null;
            }
    
            Logger.LogError("access_token: " + token);
            return token;
        }
    
    }
    
    
    using System.Globalization;
    using System.Text.Json;
    using System.Threading.Tasks;
    using Microsoft.AspNetCore.Http;
    using Microsoft.Extensions.Caching.Distributed;
    using Microsoft.Extensions.Logging;
    using Microsoft.Extensions.Options;
    using Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations;
    using Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations.ClientProxies;
    using Volo.Abp.AspNetCore.Mvc.Client;
    using Volo.Abp.Caching;
    using Volo.Abp.DependencyInjection;
    using Volo.Abp.Threading;
    using Volo.Abp.Users;
    
    namespace MyCompanyName.MyProjectName.Web;
    
    [Dependency(ReplaceServices = true)]
    [ExposeServices(typeof(ICachedApplicationConfigurationClient))]
    public class MyMvcCachedApplicationConfigurationClient : ICachedApplicationConfigurationClient, ITransientDependency
    {
        public ILogger<MvcCachedApplicationConfigurationClient> Logger { get; set; }
    
        protected IHttpContextAccessor HttpContextAccessor { get; }
        protected AbpApplicationConfigurationClientProxy ApplicationConfigurationAppService { get; }
        protected AbpApplicationLocalizationClientProxy ApplicationLocalizationClientProxy { get; }
        protected ICurrentUser CurrentUser { get; }
        protected IDistributedCache<ApplicationConfigurationDto> Cache { get; }
        protected AbpAspNetCoreMvcClientCacheOptions Options { get; }
    
        public MyMvcCachedApplicationConfigurationClient(
            IDistributedCache<ApplicationConfigurationDto> cache,
            AbpApplicationConfigurationClientProxy applicationConfigurationAppService,
            ICurrentUser currentUser,
            IHttpContextAccessor httpContextAccessor,
            AbpApplicationLocalizationClientProxy applicationLocalizationClientProxy,
            IOptions<AbpAspNetCoreMvcClientCacheOptions> options, ILogger<MvcCachedApplicationConfigurationClient> logger)
        {
            ApplicationConfigurationAppService = applicationConfigurationAppService;
            CurrentUser = currentUser;
            HttpContextAccessor = httpContextAccessor;
            ApplicationLocalizationClientProxy = applicationLocalizationClientProxy;
            Logger = logger;
            Options = options.Value;
            Cache = cache;
        }
    
        public async Task<ApplicationConfigurationDto> GetAsync()
        {
            var cacheKey = CreateCacheKey();
            var httpContext = HttpContextAccessor?.HttpContext;
    
            if (httpContext != null && httpContext.Items[cacheKey] is ApplicationConfigurationDto configuration)
            {
                return configuration;
            }
    
            configuration = await Cache.GetOrAddAsync(
                cacheKey,
                async () => await GetRemoteConfigurationAsync(),
                () => new DistributedCacheEntryOptions
                {
                    AbsoluteExpirationRelativeToNow = Options.ApplicationConfigurationDtoCacheAbsoluteExpiration
                }
            );
    
            if (httpContext != null)
            {
                httpContext.Items[cacheKey] = configuration;
            }
    
            Logger.LogError(JsonSerializer.Serialize(configuration, new JsonSerializerOptions()
            {
                WriteIndented = true
            }));
    
            return configuration;
        }
    
        private async Task<ApplicationConfigurationDto> GetRemoteConfigurationAsync()
        {
            var config = await ApplicationConfigurationAppService.GetAsync(
                new ApplicationConfigurationRequestOptions
                {
                    IncludeLocalizationResources = false
                }
            );
    
            var localizationDto = await ApplicationLocalizationClientProxy.GetAsync(
                new ApplicationLocalizationRequestDto {
                    CultureName = config.Localization.CurrentCulture.Name,
                    OnlyDynamics = true
                }
            );
    
            config.Localization.Resources = localizationDto.Resources;
    
            return config;
        }
    
        public ApplicationConfigurationDto Get()
        {
            var cacheKey = CreateCacheKey();
            var httpContext = HttpContextAccessor?.HttpContext;
    
            if (httpContext != null && httpContext.Items[cacheKey] is ApplicationConfigurationDto configuration)
            {
                return configuration;
            }
    
            return AsyncHelper.RunSync(GetAsync);
        }
    
        protected virtual string CreateCacheKey()
        {
            return MvcCachedApplicationConfigurationClientHelper.CreateCacheKey(CurrentUser);
        }
    }
    
    static class MvcCachedApplicationConfigurationClientHelper
    {
        public static string CreateCacheKey(ICurrentUser currentUser)
        {
            var userKey = currentUser.Id?.ToString("N") ?? "Anonymous";
            return $"ApplicationConfiguration_{userKey}_{CultureInfo.CurrentUICulture.Name}";
        }
    }
    
    
  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    These codes will write the Error logs. Please share the logs of web host.

    Thanks

    liming.ma@volosoft.com

  • User Avatar
    0
    Talal created

    just to be clear. the WEB or HOST? You said web.host and the namespace says .web

    I think you mean to the HOST project

    I will add and deploy now

  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    The project that is using

     context.Services.AddAuthentication(options =>
                {
                    options.DefaultScheme = "Cookies";
                    options.DefaultChallengeScheme = "oidc";
                })
                .AddCookie("Cookies", options =>
                {
                    options.ExpireTimeSpan = TimeSpan.FromDays(365);
                })
                .AddAbpOpenIdConnect("oidc", options =>
                {
                    options.Authority = configuration["AuthServer:Authority"];
                    options.RequireHttpsMetadata = Convert.ToBoolean(configuration["AuthServer:RequireHttpsMetadata"]);
                    options.ResponseType = OpenIdConnectResponseType.CodeIdToken;
    
                    options.ClientId = configuration["AuthServer:ClientId"];
                    options.ClientSecret = configuration["AuthServer:ClientSecret"];
    
                    options.UsePkce = true;
                    options.SaveTokens = true;
                    options.GetClaimsFromUserInfoEndpoint = true;
    
                    options.Scope.Add("roles");
                    options.Scope.Add("email");
                    options.Scope.Add("phone");
                    options.Scope.Add("MyProjectName");
                });
    
  • User Avatar
    0
    Talal created

    Web. ok

  • User Avatar
    0
    trendline created

    Did you solved this odd behavior? I have the same issue like you

  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    I will update my code Then you can retry.

    Wait a sec.

  • User Avatar
    0
    Talal created

    @maliming

    Thank you. so far; The changes seem to work. I will continue testing today.

    I would like to ask though; why that code for the CheckExpiresAt() not in the core ABP? Is there any other side effect?

    Also should I use the same code for the API HOST project?

    For reference to others having the issue I believe the issue is that the auth cookie not expiring with the session expiration.

    The fix that was suggested (and seems working) by maliming):

    in the WEB project Module:

       private void ConfigureAuthentication(ServiceConfigurationContext context, IConfiguration configuration)
        {
            context.Services.AddAuthentication(options =>
                {
                    options.DefaultScheme = "Cookies";
                    options.DefaultChallengeScheme = "oidc";
                })
                .AddCookie("Cookies", options =>
                {
                    options.ExpireTimeSpan = TimeSpan.FromDays(365);
                    options.CheckExpiresAt();  //  << ADDED
                })
    

    and added a file/class CookieAuthenticationOptionsExtensions.cs in the web module with the class below:

    using System;
    using System.Globalization;
    using Microsoft.AspNetCore.Authentication;
    using Microsoft.AspNetCore.Authentication.Cookies;
    
    namespace { your namespace }.Web.Extensions
    {
        public static class CookieAuthenticationOptionsExtensions
        {
            public static CookieAuthenticationOptions CheckExpiresAt(this CookieAuthenticationOptions options,
        string oidcAuthenticationScheme = "oidc")
            {
                var originalHandler = options.Events.OnValidatePrincipal;
                options.Events.OnValidatePrincipal = async principalContext =>
                {
                    originalHandler?.Invoke(principalContext);
    
                    if (principalContext.Principal != null && principalContext.Principal.Identity != null && principalContext.Principal.Identity.IsAuthenticated)
                    {
                        var tokenExpiresAt = principalContext.Properties.Items[".Token.expires_at"];
                        if (tokenExpiresAt != null &&
                            DateTimeOffset.TryParseExact(tokenExpiresAt, "yyyy-MM-ddTHH:mm:ss.fffffffzzz", null, DateTimeStyles.AdjustToUniversal, out var expiresAt) &&
                            expiresAt < DateTimeOffset.UtcNow.Subtract(TimeSpan.FromMinutes(5)))
                        {
                            principalContext.RejectPrincipal();
                            await principalContext.HttpContext.SignOutAsync(principalContext.Scheme.Name);
                        }
                    }
                };
    
                return options;
            }
    
        }
    }
    

    Thank you

  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    I would like to ask though; why that code for the CheckExpiresAt() not in the core ABP? Is there any other side effect?

    We will do that. https://github.com/abpframework/abp/pull/16504

    Also should I use the same code for the API HOST project?

    No. This only needs for the web project.

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