首次成功登录后,第二次登录会导致无限重定向循环 MVC .NET 5 OWIN ADAL OpenIDConnect。

32

第一篇帖子,请温柔!:)

我正在开发一个针对Office 365的MVC .NET 5 Web应用程序,并使用OpenIDConnect框架。 我已经设置了OWIN(3)和ADAL(2)以及我的Azure AD应用程序。 没有用户操作的登录,主控制器附加了[Authorize]属性,强制立即重定向到Azure AD进行登录。 我没有在任何我的Authorize属性中使用角色。

问题:我可以成功登录我的应用程序 - 一次! 在第一次登录之后,我关闭浏览器(或在不同的机器上打开新的浏览器),然后再次进入应用程序。 它将我重定向到Azure AD登录屏幕,我登录并且它不断在应用程序和Azure之间重定向,直到我遇到臭名昭著的400头过长问题。 查看cookie存储,我发现它充满了随机数。 我检查缓存(Vittorio的EFADALCache配方,尽管在发现此问题时我正在使用TokenCache.DefaultShared),它有数百行缓存数据(只有一个成功登录生成的行)。

我可以通过输出窗口看到重定向发生时每个往返都会生成一个新的访问和刷新令牌:

Microsoft.IdentityModel.Clients.ActiveDirectory Verbose: 1 : 31/07/2015 12:31:52: 15ad306e-e26d-4827-98dc-dea75853788a - AcquireTokenByAuthorizationCodeHandler: Resource value in the token response was used for storing tokens in the cache
iisexpress.exe Information: 0 : 31/07/2015 12:31:52: 15ad306e-e26d-4827-98dc-dea75853788a - AcquireTokenByAuthorizationCodeHandler: Resource value in the token response was used for storing tokens in the cache
Microsoft.IdentityModel.Clients.ActiveDirectory Information: 2 : 31/07/2015 12:31:52:  - TokenCache: Deserialized 1 items to token cache.
iisexpress.exe Information: 0 : 31/07/2015 12:31:52:  - TokenCache: Deserialized 1 items to token cache.
Microsoft.IdentityModel.Clients.ActiveDirectory Verbose: 1 : 31/07/2015 12:31:52: 15ad306e-e26d-4827-98dc-dea75853788a - TokenCache: Storing token in the cache...
iisexpress.exe Information: 0 : 31/07/2015 12:31:52: 15ad306e-e26d-4827-98dc-dea75853788a - TokenCache: Storing token in the cache...
Microsoft.IdentityModel.Clients.ActiveDirectory Verbose: 1 : 31/07/2015 12:31:52: 15ad306e-e26d-4827-98dc-dea75853788a - TokenCache: An item was stored in the cache
iisexpress.exe Information: 0 : 31/07/2015 12:31:52: 15ad306e-e26d-4827-98dc-dea75853788a - TokenCache: An item was stored in the cache
Microsoft.IdentityModel.Clients.ActiveDirectory Information: 2 : 31/07/2015 12:31:52: 15ad306e-e26d-4827-98dc-dea75853788a - AcquireTokenHandlerBase: === Token Acquisition finished successfully. An access token was retuned:
    Access Token Hash: PN5HoBHPlhhHIf1lxZhEWb4B4Hli69UKgcle0w7ssvo=
    Refresh Token Hash: 3xmypXCO6MIMS9qUV+37uPD4kPip9WDH6Ex29GdWL88=
    Expiration Time: 31/07/2015 13:31:51 +00:00
    User Hash: GAWUtY8c4EKcJnsHrO6NOzwcQDMW64z5BNOvVIl1vAI=
在发生问题时,我的OpenIdConnectAuthenticationOptions中的AuthorizationCodeReceived通知被触发,因此我知道Azure认为登录成功了(否则不会重定向回应用程序)。
    private static void PrepO365Auth(IAppBuilder app)
    {

        app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);

        app.UseCookieAuthentication(new CookieAuthenticationOptions());

        //Configure OpenIDConnect, register callbacks for OpenIDConnect Notifications
        app.UseOpenIdConnectAuthentication(
            new OpenIdConnectAuthenticationOptions
            {

                ClientId = ConfigHelper.ClientId,
                Authority = authority,
                PostLogoutRedirectUri = "https://localhost:44300/Account/SignedOut",
                RedirectUri = "https://localhost:44300/",
                Notifications = new OpenIdConnectAuthenticationNotifications
                {
                    AuthorizationCodeReceived = (context) =>
                    {
                        ClientCredential credential = new ClientCredential(ConfigHelper.ClientId, ConfigHelper.AppKey);
                        string signedInUserID = context.AuthenticationTicket.Identity.FindFirst(ClaimTypes.NameIdentifier).Value;

                        AuthenticationContext authContext = new AuthenticationContext(authority, new EFADALTokenCache(signedInUserID)); // TokenCache.DefaultShared Probably need a persistent token cache to handle app restarts etc
                        AuthenticationResult result = authContext.AcquireTokenByAuthorizationCode(
                            context.Code, new Uri(HttpContext.Current.Request.Url.GetLeftPart(UriPartial.Path)), credential, ConfigHelper.GraphResourceId);

                        return Task.FromResult(0);
                    },

                    AuthenticationFailed = context =>
                    {
                        context.HandleResponse();
                        context.Response.Redirect("/Error/ShowError?signIn=true&errorMessage=" + context.Exception.Message);
                        return Task.FromResult(0);
                    }
                }
            });
    }
}

在发现问题后,我使用自己的Auth属性替换了Authorized属性,并继承了AuthorizeAttribute,只是为了尝试并进入Authorize代码以查看发生了什么。我从MVC 5源代码的版本5构建了一个PDB文件,但所有发生的事情都会跳回到我的代码中:( 话虽如此,我已经覆盖了我能覆盖的部分,并发现filterContext.HttpContext.User.Identity.IsAuthenticated为false,这是有道理的,因为这会导致重定向回Azure登录。

所以我知道:

  • Azure接受我的登录并返回相关的令牌
  • 在OnAuthorization之前的第二次登录中,filterContext.HttpContext.User.Identity.IsAuthenticated返回false
  • 我的Azure应用程序配置正确,否则它就不会进行身份验证

我认为:

  • 在MVC Identity设置中存在错误。Azure正常工作,否则它就不能进行身份验证。
  • 这不是cookie问题,因为如果您在另一台机器上执行第二次登录,则会出现问题

很抱歉这有点冗长,但是有很多这些无限重定向问题,我需要解释一下我的情况为什么不同!

我想要的(如果不是答案!)是向正确的方向推动我进一步调试的方法。

感谢您能提供的任何帮助!

Andy


5
针对有兴趣的人,我已经找到了答案。在Katana中存在一个已知的bug,即Katana cookie管理器和ASP.NET cookie管理器冲突且彼此覆盖对方的cookie。完整的详细信息和解决方法可以在以下链接中找到: http://katanaproject.codeplex.com/wikipage?title=System.Web%20response%20cookie%20integration%20issues&referringTitle=Documentation - Andy Bullivent
1
看起来这个问题在ASP.NET 4中不会被修复,因为它今天仍然存在。@AndyBullivent请将您的评论作为答案。 - Daniel Gimenez
@AndyBullivent - 你解决了这个问题吗?我们遇到了间歇性的重定向循环,我很想知道这个问题是如何解决的。 - Scuba Steve
5个回答

23

对于任何感兴趣的人,我已经找到了答案。这是Katana中已知的一个bug,其中Katana cookie管理器和ASP .NET cookie管理器冲突并相互覆盖彼此的cookie。完整的详细信息和解决方法在此处:http://katanaproject.codeplex.com/wikipage?title=System.Web%20response%20cookie%20integration%20issues&referringTitle=Documentation

如下所示的SystemWebCookieManager现在可以在Microsoft.Owin.Host.SystemWeb Nuget软件包中找到。

在CodePlex关闭后添加代码:

//stick this in public void ConfigureAuth(IAppBuilder app)
  app.UseCookieAuthentication(new CookieAuthenticationOptions
            {
                // ...
                CookieManager = new SystemWebCookieManager()
            });

//And create this class elsewhere:
public class SystemWebCookieManager : ICookieManager
    {
        public string GetRequestCookie(IOwinContext context, string key)
        {
            if (context == null)
            {
                throw new ArgumentNullException("context");
            }

            var webContext = context.Get<HttpContextBase>(typeof(HttpContextBase).FullName);
            var cookie = webContext.Request.Cookies[key];
            return cookie == null ? null : cookie.Value;
        }

        public void AppendResponseCookie(IOwinContext context, string key, string value, CookieOptions options)
        {
            if (context == null)
            {
                throw new ArgumentNullException("context");
            }
            if (options == null)
            {
                throw new ArgumentNullException("options");
            }

            var webContext = context.Get<HttpContextBase>(typeof(HttpContextBase).FullName);

            bool domainHasValue = !string.IsNullOrEmpty(options.Domain);
            bool pathHasValue = !string.IsNullOrEmpty(options.Path);
            bool expiresHasValue = options.Expires.HasValue;

            var cookie = new HttpCookie(key, value);
            if (domainHasValue)
            {
                cookie.Domain = options.Domain;
            }
            if (pathHasValue)
            {
                cookie.Path = options.Path;
            }
            if (expiresHasValue)
            {
                cookie.Expires = options.Expires.Value;
            }
            if (options.Secure)
            {
                cookie.Secure = true;
            }
            if (options.HttpOnly)
            {
                cookie.HttpOnly = true;
            }

            webContext.Response.AppendCookie(cookie);
        }

        public void DeleteCookie(IOwinContext context, string key, CookieOptions options)
        {
            if (context == null)
            {
                throw new ArgumentNullException("context");
            }
            if (options == null)
            {
                throw new ArgumentNullException("options");
            }

            AppendResponseCookie(
                context,
                key,
                string.Empty,
                new CookieOptions
                {
                    Path = options.Path,
                    Domain = options.Domain,
                    Expires = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc),
                });
        }
    }

我也制作了一个要点摘要:https://gist.github.com/irwinwilliams/823f43ef8a5e8019a95874049dbb8b00


谢谢!完全解决了我的问题!我有一个MVC项目,使用owin auth中间件,首先是cookie auth,然后是oidc。只要我在任何地方放置Authorize属性,就会出现无限循环,并且我可以看到cookie auth进入ResponseSignedIn。似乎System.Web正在替换cookie。我注册了out of proc session并读取了Session可能是罪犯...无论如何再次感谢! - Quinton Smith
两个大拇指向上的救星...谢谢 - Whoever
谢谢,我过去一周一直在处理这个问题。现在它已经正常工作了 :) - Jayendran

10

我遇到了这个问题并尝试了所有在互联网上找到的解决方案。但是它们都没有起作用,然后我查看了我的cookie,发现它非常大。Owin中间件将其截断,然后[Authorize]属性无法验证身份->发送用户到oidc->身份良好 ->重定向到客户端->截断cookie->无法在[Authorize]中验证->发送用户到oidc->等等。

解决方法在Microsoft.Owin.Host.SystemWeb 3.1.0.0中,需要使用SystemWebChunkingCookieManager。

它会拆分并一起解析cookie。

  app.UseCookieAuthentication(new CookieAuthenticationOptions
  {
      AuthenticationType = "Cookies",
      CookieManager = new Microsoft.Owin.Host.SystemWeb.SystemWebChunkingCookieManager()
  });

不确定@Cyberpks需要解决什么问题,但这是主要问题的完美解决方案。非常感谢Scott Belchak!!! - timmi4sa
我该把花送到哪里? - HelloWorld

4

我没有遇到完全相同的问题,但是在我的DEV机器上进行基于OpenId连接的登录时,也遇到了重定向循环的问题。

在我的情况下,这是一个简单的cookie错误。我通过HTTP访问受保护的URL。请确保通过HTTPS访问依赖方上的受保护URL。

一旦您经过身份验证,身份验证cookie将仅通过HTTPS发送,这意味着当您通过HTTP访问受保护的URL时,浏览器不会将身份验证cookie随请求发送,因此服务器将视您为未经身份验证。此时,服务器将重定向您到身份验证服务器(您已经登录)。身份验证服务器将重定向您回原始URL,从而确保重定向循环。

在部署中,这永远不应该发生,因为如果您具有身份验证等功能,则应始终在应用程序中使用全SSL。这降低了会话劫持的风险。


2

我曾经遇到过完全相同的问题。由于其他依赖项,无法将 URL 从 HTTP 更改为 HTTPS。最终通过在 global.asax.cs 中添加 session_start 和 session_end 解决了该问题。

  protected void Session_Start(object sender, EventArgs e)
        {
            // event is raised each time a new session is created     
        }

  protected void Session_End(object sender, EventArgs e)
        {
            // event is raised when a session is abandoned or expires

        }

2
以下代码通过在Global.asax.cs文件中添加会话事件来解决了我的问题。
protected void Session_Start(object sender, EventArgs e)
    {
        // event is raised each time a new session is created     
    }



protected void Session_End(object sender, EventArgs e)
    {
        // event is raised when a session is abandoned or expires

    }

在 Startup.Auth.cs 文件的 public void ConfigureAuth(IAppBuilder app) 方法中添加以下代码:
  app.UseCookieAuthentication(new CookieAuthenticationOptions
        {
            AuthenticationType = "Cookies",
            CookieManager = new Microsoft.Owin.Host.SystemWeb.SystemWebChunkingCookieManager()
        });

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接