C# 使用Azure SSO实现而不使用标识时,用户被重定向到登录页面太早

C# 使用Azure SSO实现而不使用标识时,用户被重定向到登录页面太早,c#,asp.net-mvc,azure,asp.net-core,authentication,C#,Asp.net Mvc,Azure,Asp.net Core,Authentication,我有一个ASP.NET MVC Core 2.2应用程序,它实现了Azure SSO身份验证。Azure SSO是使用快速入门教程为Azure门户中可访问的已注册应用程序设置的。此外,我还通过从数据库手动查询角色列表并在用户登录过程中将声明添加到Principal.Identity来实现授权。我还需要一个自定义的Authorize属性,因为我很难在AJAX调用的控制器操作上使用它 我的团队遇到的一个问题是提前重定向到登录页面。当HttpContext.User.Identity.IsAuthen

我有一个ASP.NET MVC Core 2.2应用程序,它实现了Azure SSO身份验证。Azure SSO是使用快速入门教程为Azure门户中可访问的已注册应用程序设置的。此外,我还通过从数据库手动查询角色列表并在用户登录过程中将声明添加到
Principal.Identity
来实现授权。我还需要一个自定义的
Authorize
属性,因为我很难在AJAX调用的控制器操作上使用它

我的团队遇到的一个问题是提前重定向到登录页面。当
HttpContext.User.Identity.IsAuthenticated
为false时,重定向本身可能发生在
CustomAuthorize
筛选器中,因为它重定向到登录页面。用户的请求在应用程序处于非活动状态30分钟左右后被重定向。正如您在下面的代码中所看到的,cookie过期时间为120分钟,滑动过期设置为true

需要明确的是,我们只是被重定向到登录页面,好像
HttpContext.User.Identity.IsAuthenticated
变成了
false
。我们没有被注销我们的Microsoft帐户。当我们单击以重新登录时,我们将自动重新验证并进入应用程序,而无需输入凭据

给我带来最大困难的是,这个问题似乎只发生在生产服务器上。我们可以在登台时使用相同的代码,它在登台时可以很好地工作,但问题仍然存在于生产中。这表明服务器配置可能有问题,但我希望尽可能确定这不是代码

我可以确认本地cookie的过期时间和在暂存服务器和本地构建上的滑动过期正常工作

我发现了几篇关于人们有一个非常类似的问题的帖子,这是由于Identity的
securityStamp
validation:,”的错误造成的。现在我不能完全确定,但我知道只有通过添加
AddIdentity()
服务来使用标识时,
securityStamp
才会出现问题。我的代码不这样做,正如您在下面看到的

Startup.cs>ConfigureServices方法:

// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection Services)
{
    //https://weblog.west-wind.com/posts/2017/dec/12/easy-configuration-binding-in-aspnet-core-revisited
    IAppSettings config = new AppSettings();
    Configuration.Bind("AppSettings", config);

    Services.AddMvc();
    Services.AddSingleton(config);
    Services.AddScoped<IUserAccess, UserAccess>();
    //..more services..

    Services.Configure<CookiePolicyOptions>(options =>
    {
        // This lambda determines whether user consent for non-essential cookies is needed for a given request.
        options.CheckConsentNeeded = context => true;
        options.MinimumSameSitePolicy = SameSiteMode.None;
    });

    Services.AddAuthentication(AzureADDefaults.AuthenticationScheme)
        .AddAzureAD(options =>
        {
            options.Instance = config.AzureAD.Instance;
            options.Domain = config.AzureAD.Domain;
            options.TenantId = config.AzureAD.TenantId;
            options.ClientId = config.AzureAD.ClientId;
            options.CallbackPath = config.AzureAD.CallbackPath;
        });

    Services.Configure<OpenIdConnectOptions>(AzureADDefaults.OpenIdScheme, options =>
    {
        options.UseTokenLifetime = false;
        options.Authority = options.Authority + "/v2.0/"; //Microsoft identity platform
        
        options.TokenValidationParameters.ValidateIssuer = true;
        options.TokenValidationParameters.ValidIssuers = config.AzureAD.Organizations;

        //Code below is to add claims during login (we add roles from db): https://stackoverflow.com/questions/51965665/adding-custom-claims-to-claimsprincipal-when-using-addazureadb2c-in-mvc-core-app
        //some discussion about this here: https://stackoverflow.com/questions/59564952/net-core-add-claim-after-azuerad-authentication
        //and here: https://stackoverflow.com/questions/52727146/net-core-2-openid-connect-authentication-and-multiple-identities
        options.Events.OnTicketReceived = context =>
        {
            string ObjectID = context.Principal.FindFirst(CustomClaimTypes.ObjectIdentifier).Value;
            //I get an instance of UserAccess service so I can get user ID, termination status, roles.
            IUserAccess userAccess = context.HttpContext.RequestServices.GetService<IUserAccess>();
            int userID = userAccess.GetUserIDByObjectID(ObjectID, config.ConnectionString);
            //if UserID is 0 (wasn't found in Users table), then that means user is not activated or not in Users table. So
            //we don't try to get their roles because we can't.
            ClaimsIdentity claimsIdentity = (ClaimsIdentity)context.Principal.Identity;
            if (userID == 0)
            {
                //this claim is checked in CustomAuthorize authorize filter
                claimsIdentity.AddClaim(new Claim(CustomClaimTypes.UserNotFound, true.ToString()));
                return Task.CompletedTask;
            }

            claimsIdentity.AddClaim(new Claim(CustomClaimTypes.UserID, userID.ToString()));
            bool terminated = userAccess.IsUserTerminatedByUserID(userID, config.ConnectionString);
            if (terminated == false)
            {
                List<string> roles = userAccess.GetUserRoleNamesByUserID(userID, config.ConnectionString);
                foreach (var role in roles)
                {
                    claimsIdentity.AddClaim(new Claim(ClaimTypes.Role, role));
                }
            }

            //In the region below, we sign in user manually instead of allowing the RemoteAuthenticationHandler
            //complete the sign in. We do this because the 'OnTicketReceived' event runs right before the user 
            //is signed into the application and we want to make sure that there is no error when we sign
            //in the user before we log a message that they have signed in.
            //Explanation of what's happening here: https://www.jerriepelser.com/blog/managing-session-lifetime-aspnet-core-oauth-providers/
            #region User sign-in code
            // Sign the user in ourselves
            context.HttpContext.SignInAsync(context.Options.SignInScheme, context.Principal, context.Properties);

            // Indicate that we handled the sign in
            context.HandleResponse();

            // Default redirect path is the base path
            if (string.IsNullOrEmpty(context.ReturnUri))
            {
                context.ReturnUri = "/";
            }

            context.Response.Redirect(context.ReturnUri);
            #endregion User sign-in code

            _logger.LogTrace("User {ObjectID} logged in", context.Principal.Identity.Name);

            return Task.CompletedTask;
        };
    });

    Services.Configure<CookieAuthenticationOptions>(AzureADDefaults.CookieScheme, options =>
    {
        options.AccessDeniedPath = "/UserAccess/NotAuthorized";
        options.LogoutPath = "/UserAccess/SignOut";
        options.ExpireTimeSpan = TimeSpan.FromMinutes(120);
        options.SlidingExpiration = true;
    });
}
public class CustomAuthorize : AuthorizeAttribute, IAuthorizationFilter
{
    public void OnAuthorization(AuthorizationFilterContext context)
    {
        string signInUrl = "/UserAccess/Index";

        if (context.HttpContext.User.Identity.IsAuthenticated)
        {
            if (context.HttpContext.User.HasClaim(CustomClaimTypes.UserNotFound, true.ToString()))
            {
                context.Result = new RedirectResult("/UserAccess/NotAuthorized");
            }
        }
        else
        {
            if (context.HttpContext.Request.IsAjaxRequest())
            {
                context.HttpContext.Response.StatusCode = 401;
                JsonResult jsonResult = new JsonResult(new { redirectUrl = signInUrl });
                context.Result = jsonResult;
            }
            else
            {
                context.Result = new RedirectResult(signInUrl);
            }
        }
    }
}