Файлы cookie ASP.NET_SessionId + OWIN не отправляются в браузер

У меня есть странная проблема с использованием проверки подлинности cookie Owin.

Когда я запускаю аутентификацию сервера IIS, отлично работает на IE/Firefox и Chrome.

Я начал выполнять некоторые тесты с помощью проверки подлинности и входа в систему на разных платформах, и я придумал странную ошибку. Спорадически структура Owin/IIS просто не отправляет файлы cookie в браузер. Я буду вводить имя пользователя и пароль, который правильный код запускается, но cookie не доставляется в браузер вообще. Если я перезапущу сервер, он начнет работать, и в какой-то момент я попробую войти в систему, а cookie перестанет получать доставку. Переход по коду ничего не делает и не вызывает ошибок.

 app.UseCookieAuthentication(new CookieAuthenticationOptions
        {
            AuthenticationMode = AuthenticationMode.Active,
            CookieHttpOnly = true,
            AuthenticationType = "ABC",
            LoginPath = new PathString("/Account/Login"),
            CookiePath = "/",
            CookieName = "ABC",
            Provider = new CookieAuthenticationProvider
               {
                  OnApplyRedirect = ctx =>
                  {
                     if (!IsAjaxRequest(ctx.Request))
                     {
                        ctx.Response.Redirect(ctx.RedirectUri);
                     }
                 }
               }
        });

И в моей процедуре входа в систему у меня есть следующий код:

IAuthenticationManager authenticationManager = HttpContext.Current.GetOwinContext().Authentication;
                            authenticationManager.SignOut(DefaultAuthenticationTypes.ExternalCookie);

var authentication = HttpContext.Current.GetOwinContext().Authentication;
var identity = new ClaimsIdentity("ABC");
identity.AddClaim(new Claim(ClaimTypes.Name, user.Username));
identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, user.User_ID.ToString()));
identity.AddClaim(new Claim(ClaimTypes.Role, role.myRole.ToString()));
    authentication.AuthenticationResponseGrant =
        new AuthenticationResponseGrant(identity, new AuthenticationProperties()
                                                   {
                                                       IsPersistent = isPersistent
                                                   });

authenticationManager.SignIn(new AuthenticationProperties() {IsPersistent = isPersistent}, identity);

Обновление 1: Кажется, что одной из причин проблемы является то, что я добавляю элементы в сеанс, когда запускаются проблемы. Добавление чего-то простого типа Session.Content["ABC"]= 123, похоже, создает проблему.

Что я могу разобрать, так выглядит следующим образом: 1) (Chrome) Когда я вхожу в систему, я получаю ASP.NET_SessionId + мой файл cookie для проверки подлинности. 2) Я перехожу на страницу, которая устанавливает session.contents... 3) Откройте новый браузер (Firefox) и попробуйте войти в систему, и он не получит ASP.NET_SessionId и не получит Cookie аутентификации 4) Хотя первый браузер имеет ASP.NET_SessionId, он продолжает работать. В тот момент, когда я удаляю этот файл cookie, у него такая же проблема, как и все другие браузеры Я работаю над ip-адресом (10.x.x.x) и localhost.

Обновление 2: Создайте создание ASPNET_SessionId сначала на моей странице login_load перед аутентификацией с помощью OWIN.

1) перед тем, как я аутентифицируюсь с помощью OWIN, я делаю случайное значение Session.Content на моей странице входа, чтобы запустить ASP.NET_SessionId 2), затем я проверяю подлинность и выполняю дальнейшие сеансы 3) Другие браузеры теперь работают

Это странно. Я могу только сделать вывод, что это имеет какое-то отношение к ASP и OWIN, думая, что они находятся в разных областях или что-то в этом роде.

Обновление 3 - странное поведение между ними.

Определено дополнительное странное поведение - время ожидания сеанса Owin и ASP отличается. Я вижу, что мои сеансы Owin остаются в живых дольше, чем мои сеансы ASP через какой-то механизм. Поэтому при входе в систему: 1.) У меня есть готовая начальная сессия 2.) Я установил несколько переменных сеанса

Мои переменные сеанса (2) "умирают" до того, как переменная сеанса cookie owin заставляет повторно войти в систему, что вызывает неожиданное поведение во всем моем приложении. (Лицо зарегистрировалось, но на самом деле оно не вошло в систему)

Обновить 3B

После некоторого копания я увидел несколько комментариев на странице, в которых говорится, что тайм-аут аутентификации "форм" должен совпадать. Я обычно думаю, что они синхронизированы, но по какой-то причине они не синхронизированы.

Резюме обходных решений

1) Перед началом проверки всегда создавайте сеанс. В основном создайте сеанс при запуске приложения Session["Workaround"] = 0;

2) [Экспериментально], если вы сохраняете файлы cookie, убедитесь, что ваш тайм-аут/длина OWIN больше, чем ваш sessionTimeout в вашем web.config(при тестировании)

Ответы

Ответ 1

Я столкнулся с той же проблемой и проследил причину реализации хостинга OWIN ASP.NET. Я бы сказал, что это ошибка.

Некоторые фон

Мои выводы основаны на этих версиях сборки:

  • Microsoft.Owin, Version = 2.0.2.0, Culture = neutral, PublicKeyToken = 31bf3856ad364e35
  • Microsoft.Owin.Host.SystemWeb, Version = 2.0.2.0, Culture = neutral, PublicKeyToken = 31bf3856ad364e35
  • System.Web, Version = 4.0.0.0, Culture = нейтральный, PublicKeyToken = b03f5f7f11d50a3a

OWIN использует собственную абстракцию для работы с ответными файлами cookie (Microsoft.Owin.ResponseCookieCollection). Эта реализация напрямую обертывает коллекцию заголовков ответов и соответственно обновляет заголовок Set-Cookie. Хост OWIN ASP.NET(Microsoft.Owin.Host.SystemWeb) просто обертывает System.Web.HttpResponse и коллекцию заголовков. Поэтому, когда новый файл cookie создается через OWIN, заголовок Set-Cookie ответа изменяется напрямую.

Но ASP.NET также использует свою собственную абстракцию для работы с ответами Cookies. Это отображается нам как свойство System.Web.HttpResponse.Cookies и реализуется с помощью закрытого класса System.Web.HttpCookieCollection. Эта реализация не переносит ответ Set-Cookie напрямую, но использует некоторые оптимизации и несколько внутренних уведомлений, чтобы показать, что оно изменило состояние на объект ответа.

Затем есть точка, которая заканчивается временем ожидания запроса, в котором проверено состояние состояния HttpCookieCollection (System.Web.HttpResponse.GenerateResponseHeadersForCookies()), и файлы cookie сериализуются в заголовок Set-Cookie. Если эта коллекция находится в определенном состоянии, весь заголовок Set-Cookie сначала очищается и воссоздается из файлов cookie, хранящихся в коллекции.

Реализация сессии ASP.NET использует свойство System.Web.HttpResponse.Cookies для хранения его ASP.NET_SessionId cookie. Также есть базовая оптимизация в модуле состояния сеанса ASP.NET(System.Web.SessionState.SessionStateModule), реализованная через статическое свойство s_sessionEverSet, которое вполне объяснимо. Если вы когда-либо сохранили что-то в состоянии сеанса в своем приложении, этот модуль будет выполнять немного больше работы для каждого запроса.


Вернуться к нашей проблеме входа

Со всеми этими частями ваши сценарии можно объяснить.

Случай 1 - сеанс никогда не был установлен

Свойство System.Web.SessionState.SessionStateModule, s_sessionEverSet является ложным. Идентификатор сеанса не генерируется модулем состояния сеанса, а состояние коллекции System.Web.HttpResponse.Cookies не определяется как измененное. В этом случае cookie OWIN отправляется корректно в браузер, и вход в систему работает.

Случай 2 - сеанс использовался где-то в приложении, но не перед тем, как пользователь пытается аутентифицировать

свойство System.Web.SessionState.SessionStateModule, s_sessionEverSet является истинным. Идентификатор сеанса генерируется SessionStateModule, ASP.NET_SessionId добавляется в коллекцию System.Web.HttpResponse.Cookies, но позже удаляется в течение времени ожидания запроса, поскольку сеанс пользователя фактически пуст. В этом случае состояние коллекции System.Web.HttpResponse.Cookies определяется как измененное, а заголовок Set-Cookie сначала очищается до того, как файлы cookie сериализуются в значение заголовка.

В этом случае файлы cookie OWIN "потеряны", а пользователь не аутентифицирован и перенаправляется на страницу входа.

Случай 3 - сеанс используется до того, как пользователь пытается аутентифицировать

свойство System.Web.SessionState.SessionStateModule, s_sessionEverSet является истинным. Идентификатор сеанса генерируется SessionStateModule, ASP.NET_SessionId добавляется в System.Web.HttpResponse.Cookies. Из-за внутренней оптимизации в System.Web.HttpCookieCollection и System.Web.HttpResponse.GenerateResponseHeadersForCookies() Заголовок Set-Cookie НЕ сначала очищается, но только обновляется.

В этом случае как cookie аутентификации OWIN, так и файл cookie ASP.NET_SessionId отправляются в ответ, и вход в систему работает.


Более общая проблема с файлами cookie

Как вы можете видеть, проблема более общая и не ограничивается сеансом ASP.NET. Если вы размещаете OWIN через Microsoft.Owin.Host.SystemWeb, и вы/что-то прямо используете коллекцию System.Web.HttpResponse.Cookies, вы рискуете.

Например работает, и оба файла cookie правильно отправляются в браузер...

public ActionResult Index()
{
    HttpContext.GetOwinContext()
        .Response.Cookies.Append("OwinCookie", "SomeValue");
    HttpContext.Response.Cookies["ASPCookie"].Value = "SomeValue";

    return View();
}

Но это не, а OwinCookie "потеряно"...

public ActionResult Index()
{
    HttpContext.GetOwinContext()
        .Response.Cookies.Append("OwinCookie", "SomeValue");
    HttpContext.Response.Cookies["ASPCookie"].Value = "SomeValue";
    HttpContext.Response.Cookies.Remove("ASPCookie");

    return View();
}

Оба тестируются из VS2013, IISExpress и шаблона проекта MVC по умолчанию.

Ответ 2

Начиная с большого анализа @TomasDolezal, я посмотрел как на источник Owin, так и на System.Web.

Проблема заключается в том, что System.Web имеет свой собственный главный источник информации о файлах cookie, и это не заголовок Set-Cookie. Owin знает только о заголовке Set-Cookie. Обходной путь заключается в том, чтобы убедиться, что любые cookie, установленные Owin, также установлены в коллекции HttpContext.Current.Response.Cookies.

Я сделал небольшое промежуточное ПО (источник, nuget), который делает именно это, который предназначен для размещения непосредственно над регистрацией промежуточного программного обеспечения cookie.

app.UseKentorOwinCookieSaver();

app.UseCookieAuthentication(new CookieAuthenticationOptions());

Ответ 3

Короче говоря, менеджер файлов cookie.NET превзойдет менеджер файлов cookie OWIN и перезапишет файлы cookie, установленные на слое OWIN. Исправление заключается в использовании класса SystemWebCookieManager, предоставленного в качестве решения для проекта Katana здесь. Вам нужно использовать этот класс или аналогичный ему, что заставит OWIN использовать менеджер файлов cookie.NET, чтобы не было несоответствий:

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

При запуске приложения просто назначьте его при создании зависимостей OWIN:

app.UseCookieAuthentication(new CookieAuthenticationOptions
{
    ...
    CookieManager = new SystemWebCookieManager()
    ...
});

Подобный ответ был предоставлен здесь, но он не включает всю кодовую базу, необходимую для решения проблемы, поэтому я вижу необходимость добавить его здесь, потому что внешняя ссылка на проект Katana может быть недоступна, и это должно быть полностью записано как решение здесь также.

Ответ 4

Команда Katana ответила на вопрос, поднятый Томасом Долезаром, и разместила документацию об обходных путях:

Обходные пути делятся на две категории. Одним из них является переконфигурирование System.Web, чтобы избежать использования коллекции Response.Cookies и перезаписи файлов cookie OWIN. Другой подход заключается в повторной настройке затронутых компонентов OWIN, чтобы они записывали файлы cookie непосредственно в коллекцию System.Web Response.Cookies.

  • Убедитесь, что сеанс установлен до проверки подлинности. Конфликт между файлами cookie System.Web и Katana происходит по запросу, поэтому приложение может установить сеанс по какому-либо запросу до процесса проверки подлинности. Это должно быть легко сделать, когда пользователь впервые прибывает, но это может быть труднее гарантировать позже, когда сессия или файлы cookie авторизации истекают и/или должны быть обновлены.
  • Отключите SessionStateModule - если приложение не полагается на информацию о сеансе, но модуль сеанса по-прежнему устанавливает файл cookie, который вызывает вышеуказанный конфликт, вы можете отключить модуль состояния сеанса.
  • Переконфигурируйте CookieAuthenticationMiddleware для прямой записи в коллекцию файлов cookie System.Web.
app.UseCookieAuthentication(new CookieAuthenticationOptions
                                {
                                    // ...
                                    CookieManager = new SystemWebCookieManager()
                                });

См. Реализацию SystemWebCookieManager из документации (ссылка выше)

Больше информации здесь

редактировать

Ниже приведены шаги, которые мы предприняли для решения проблемы. И 1., и 2. решили проблему также отдельно, но мы решили применить оба на всякий случай:

1. Используйте SystemWebCookieManager

2. Установите переменную сеанса:

protected override void Initialize(RequestContext requestContext)
{
    base.Initialize(requestContext);

    // See http://stackoverflow.com/questions/20737578/asp-net-sessionid-owin-cookies-do-not-send-to-browser/
    requestContext.HttpContext.Session["FixEternalRedirectLoop"] = 1;
}

(примечание: метод Initialize, описанный выше, является логическим местом для исправления, потому что base.Initialize делает Session доступным. Однако исправление также можно применить позже, поскольку в OpenId сначала выполняется анонимный запрос, затем перенаправляется к поставщику OpenId, а затем обратно. к приложению. Проблемы будут возникать после перенаправления обратно в приложение, в то время как исправление устанавливает переменную сеанса уже во время первого анонимного запроса, таким образом устраняя проблему до того, как произойдет какое-либо обратное перенаправление)

Редактировать 2

Скопируйте из проекта Katana 2016-05-14:

Добавь это:

app.UseCookieAuthentication(new CookieAuthenticationOptions
                                {
                                    // ...
                                    CookieManager = new SystemWebCookieManager()
                                });

...и это:

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

Ответ 5

Ответы уже предоставлены, но в owin 3.1.0 существует класс SystemWebChunkingCookieManager, который можно использовать.

https://github.com/aspnet/AspNetKatana/blob/dev/src/Microsoft.Owin.Host.SystemWeb/SystemWebChunkingCookieManager.cs

https://raw.githubusercontent.com/aspnet/AspNetKatana/c33569969e79afd9fb4ec2d6bdff877e376821b2/src/Microsoft.Owin.Host.SystemWeb/SystemWebChunkingCookieManager.cs

app.UseCookieAuthentication(new CookieAuthenticationOptions
{
    ...
    CookieManager = new SystemWebChunkingCookieManager()
    ...
});

Ответ 6

Если вы устанавливаете файлы cookie в промежуточном ПО OWIN самостоятельно, использование OnSendingHeaders похоже на проблему.

Например, будет использоваться код ниже owinResponseCookie2, хотя owinResponseCookie1 не:

private void SetCookies()
{
    var owinContext = HttpContext.GetOwinContext();
    var owinResponse = owinContext.Response;

    owinResponse.Cookies.Append("owinResponseCookie1", "value1");

    owinResponse.OnSendingHeaders(state =>
    {
        owinResponse.Cookies.Append("owinResponseCookie2", "value2");
    },
    null);

    var httpResponse = HttpContext.Response;
    httpResponse.Cookies.Remove("httpResponseCookie1");
}

Ответ 7

У меня был тот же симптом, что заголовок Set-Cookie не отправлялся, но ни один из этих ответов не помог мне. Все работало на моем локальном компьютере, но при развертывании в производство заголовки set-cookie никогда не устанавливались.

Оказывается, это было сочетание использования специального CookieAuthenticationMiddleware с WebApi вместе с поддержкой сжатия WebApi.

К счастью, я использовал ELMAH в своем проекте, что позволило мне войти в это исключение:

Сервер System.Web.HttpException не может добавить заголовок после отправки заголовков HTTP.

Что привело меня к этой проблеме GitHub

По сути, если у вас странная установка, как у меня, вы захотите отключить сжатие для ваших контроллеров/методов WebApi, которые устанавливают куки, или попробуйте OwinServerCompressionHandler.

Ответ 8

Самое быстрое однострочное решение:

HttpContext.Current.Session["RunSession"] = "1";

Просто добавьте эту строку до метода CreateIdentity:

HttpContext.Current.Session["RunSession"] = "1";
var userIdentity = userManager.CreateIdentity(user, DefaultAuthenticationTypes.ApplicationCookie);
_authenticationManager.SignIn(new AuthenticationProperties { IsPersistent = rememberLogin }, userIdentity);