web-dev-qa-db-fra.com

La deuxième connexion crée une boucle de redirection infinie après la première connexion réussie MVC .NET 5 OWIN ADAL OpenIDConnect

premier post alors soyez gentil! :)

Je développe une application Web MVC .NET 5 pour Office 365 et j'utilise l'infrastructure OpenIDConnect. J'ai configuré OWIN (3), ADAL (2) et mon application Azure AD. Il n'y a pas de connexion actionnée par l'utilisateur, le contrôleur domestique a un attribut [Autoriser] associé, ce qui force la redirection de connexion immédiate vers Azure AD. Je n'utilise pas de rôles dans aucun de mes attributs d'autorisation.

Le problème: je peux me connecter à mes applications avec succès - UNE FOIS! Après la première connexion, je ferme le navigateur (ou ouvre un nouveau navigateur sur une autre machine) et je lance à nouveau l'application. Il me redirige vers l'écran de connexion Azure AD auquel je me connecte, puis il redirige en continu entre l'application et Azure jusqu'à ce que les 400 en-têtes infâmes deviennent un problème de longue durée. En regardant dans le magasin de biscuits, je trouve qu'il y a plein de choses. Je vérifie le cache (recette EFADALCache de Vittorio, bien que j'utilisais TokenCache.DefaultShared lorsque ce problème a été découvert) et qu'il contient des centaines de lignes de données de cache (une seule ligne générée avec une connexion réussie). 

Je peux voir que les redirections se produisent via la fenêtre de sortie qu'un nouvel jeton d'accès et d'actualisation est généré à chaque aller-retour:

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=

La notification AuthorizationCodeReceived de mon OpenIdConnectAuthenticationOptions est touchée lorsque le problème se produit. Je sais donc qu'Azure pense que la connexion a été établie avec succès (sinon la redirection vers l'application ne se produirait pas):

    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);
                    }
                }
            });
    }
}

Après avoir découvert le problème, j'ai remplacé l'attribut Authorized par mon propre attribut Auth, héritant de AuthorizeAttribute, juste pour pouvoir essayer et entrer dans le code Authorize et voir ce qu'il se passe. J'ai construit un fichier PDB à partir de la version 5 du code source de MVC 5, mais tout ce qui se passe est de revenir dans mon propre code :( Cela dit, j'ai annulé ce que je pouvais et trouvé filterContext. HttpContext.User.Identity.IsAuthenticated est false, ce qui est logique, car cela entraînerait la redirection vers la connexion Azure. 

Donc, je sais que:

  • Azure accepte mes identifiants et renvoie les jetons correspondants
  • Lors de la seconde connexion, avant OnAuthorization, filterContext.HttpContext.User.Identity.IsAuthenticated renvoie false.
  • ma configuration d'application Azure est correcte, ou elle ne s'authentifierait pas du tout.

Je pense que:

  • Il y a quelque chose qui ne va pas dans la configuration de MVC Identity. Azure fonctionne correctement ou il ne s’authentifierait pas du tout.
  • Ce n'est pas un problème de cookie car le problème se pose si vous effectuez la deuxième connexion sur une autre machine.

Je suis désolé, c'est un peu long, mais il y a tellement de problèmes de redirection infinie qu'il me fallait expliquer pourquoi ma situation était différente!

Ce que je recherche (si ce n’est une réponse!), C’est un push dans la bonne direction pour déboguer plus avant.

Appréciez toute aide que vous pouvez donner!

Andy

28
Andy Bullivent

J'ai trouvé la réponse pour toute personne intéressée. C'est un bug connu dans Katana où le gestionnaire de cookies Katana et le ASP .NET gestionnaire de cookies .NET s'affrontent et écrasent les cookies les uns des autres. Détails complets et solution ici: 

http://katanaproject.codeplex.com/wikipage?title=System.Web%20response%20cookie%20integration%20issues&referringTitle=Documentation

Le SystemWebCookieManager illustré ci-dessous se trouve maintenant dans le package Microsoft.Owin.Host.SystemWeb Nuget.

Ajout du code pour la mort de 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),
                });
        }
    }

J'en ai aussi fait un résumé: https://Gist.github.com/irwinwilliams/823f43ef8a5e8019a95874049dbb8b00

19
Andy Bullivent

J'ai rencontré ce problème et appliqué TOUS LES CORRECTIFS SUR INTERNET. Aucun d'entre eux n'a fonctionné, puis je suis entré et j'ai regardé mon biscuit. C'était énorme. Le middleware Owin était en train de le tronquer, puis l'attribut [Autoriser] n'était pas en mesure de vérifier l'identité -> envoyer l'utilisateur à oidc -> identité bonne - redirection vers le client -> tronquer le cookie -> impossible de vérifier dans [Autoriser] - > envoyer l'utilisateur à oidc -> etc.

Le correctif se trouvait dans Microsoft.Owin.Host.SystemWeb 3.1.0.0 et à l'aide de SystemWebChunkingCookieManager.

Il va diviser les cookies et les analyser.

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

Je n'avais pas exactement le problème décrit, mais j'avais également une boucle de redirection lors de la connexion basée sur OpenId connect sur ma machine DEV. 

Dans mon cas, c'était une simple erreur avec les cookies. J'accédais à l'URL protégée via HTTP. Assurez-vous que vous accédez à l'URL protégée de votre partie de confiance par HTTPS. 

Une fois que vous êtes authentifié, le cookie d'authentification ne sera envoyé que via HTTPS. Cela signifie que lorsque vous accédez à une URL protégée via HTTP, le navigateur n'enverra pas votre cookie d'authentification avec la demande et le serveur vous verra donc comme non authentifié. À ce stade, le serveur vous redirigera vers le serveur d'authentification (où vous êtes déjà connecté). Le serveur d'authentification vous redirigera vers l'URL d'origine, assurant ainsi une boucle de redirection.

Cela ne devrait jamais être le cas dans vos déploiements, car vous devez toujours utiliser la technologie tout SSL dans votre application, si vous disposez de fonctionnalités telles que l'authentification. Cela réduit le risque de détournement de session. 

1
Gopal

J'ai eu exactement le même problème. Impossible de modifier l'URL de HTTP à HTTPS en raison d'autres dépendances. Finalement résolu en ajoutant session_start et session_end dans 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

        }
1
exotic

Le code ci-dessous a résolu mon problème en ajoutant des événements de session dans le fichier Golbal.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

    }

Et en ajoutant le code ci-dessous dans la méthode VOAP public ConfigureAuth (IAppBuilder app) du fichier Startup.Auth.cs 

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