ASP.NET_SessionId + OWIN Cookies 不发送到浏览器
Posted
技术标签:
【中文标题】ASP.NET_SessionId + OWIN Cookies 不发送到浏览器【英文标题】:ASP.NET_SessionId + OWIN Cookies do not send to browser 【发布时间】:2014-01-11 07:40:56 【问题描述】:我在使用 Owin cookie 身份验证时遇到了一个奇怪的问题。
当我启动 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: 在使用 OWIN 进行身份验证之前,首先在我的 login_load 页面上强制创建 ASPNET_SessionId
。
1) 在使用 OWIN 进行身份验证之前,我在登录页面上创建了一个随机的 Session.Content
值以启动 ASP.NET_SessionId
2)然后我进行身份验证并进行进一步的会话
3) 其他浏览器现在似乎可以工作了
这很奇怪。我只能得出结论,这与 ASP 和 OWIN 认为它们位于不同的域或类似的东西有关。
更新 3 - 两者之间的奇怪行为。
发现了其他奇怪的行为 - Owin 和 ASP 会话的超时不同。我所看到的是,通过某种机制,我的 Owin 会话比我的 ASP 会话存活的时间更长。所以登录时: 1.) 我有一个基于 cookie 的身份验证会话 2.) 我设置了一些会话变量
我的会话变量 (2) 在 owin cookie 会话变量强制重新登录之前“死亡”,这会导致整个应用程序出现意外行为。 (此人已登录但并未真正登录)
更新 3B
经过一番挖掘,我在一个页面上看到一些 cmets 说“表单”身份验证超时和会话超时需要匹配。我通常认为两者是同步的,但无论出于何种原因,两者都不同步。
解决方法摘要
1) 始终在身份验证之前先创建会话。基本上在启动应用程序时创建会话Session["Workaround"] = 0;
2) [实验性] 如果您保留 cookie,请确保您的 OWIN 超时/长度比 web.config 中的 sessionTimeout 长(测试中)
【问题讨论】:
可以确认将会话调用添加到 ActionResult Login 和 ActionResult ExternalLogin 解决了这个问题。我确定只需要一个,但我都有。 谢谢!...在 ExternalLogin 中添加 Session 为我解决了这个问题...这是巫术魔法...我已经浪费了 6 个小时来解决这个问题... 【参考方案1】:我也遇到了同样的问题,并将原因追溯到 OWIN ASP.NET 托管实现。我会说这是一个错误。
一些背景
我的发现基于这些程序集版本:
Microsoft.Owin,版本=2.0.2.0,文化=中性,PublicKeyToken=31bf3856ad364e35 Microsoft.Owin.Host.SystemWeb,版本=2.0.2.0,文化=中性,PublicKeyToken=31bf3856ad364e35 System.Web,版本=4.0.0.0,文化=中性,PublicKeyToken=b03f5f7f11d50a3aOWIN 使用它自己的抽象来处理响应 Cookie (Microsoft.Owin.ResponseCookieCollection)。此实现直接包装响应头集合并相应地更新 Set-Cookie 头。 OWIN ASP.NET 主机 (Microsoft.Owin.Host.SystemWeb) 只是包装 System.Web.HttpResponse 和它的标头集合。所以当通过 OWIN 创建新的 cookie 时,响应 Set-Cookie 头会直接改变。
但是 ASP.NET 也使用它自己的抽象来处理响应 Cookie。这作为 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。此外,通过名为 s_sessionEverSet 的静态属性实现的 ASP.NET 会话状态模块 (System.Web.SessionState.SessionStateModule) 中也有一些基本的优化,这是不言自明的。如果您曾经在应用程序中将某些内容存储到会话状态,则此模块将为每个请求做更多的工作。
回到我们的登录问题
通过所有这些片段可以解释您的场景。
案例 1 - 从未设置会话
System.Web.SessionState.SessionStateModule,s_sessionEverSet 属性为 false。会话状态模块不生成会话 ID,并且 System.Web.HttpResponse.Cookies 集合状态未检测到已更改。在这种情况下,OWIN cookie 会正确发送到浏览器并且登录正常。
案例 2 - 会话在应用程序的某处使用,但在用户尝试验证之前没有使用
System.Web.SessionState.SessionStateModule,s_sessionEverSet 属性为真。会话 ID 由 SessionStateModule 生成,ASP.NET_SessionId 被添加到 System.Web.HttpResponse.Cookies 集合中,但由于用户的会话实际上是空的,因此在请求生命周期的后期将其删除。在这种情况下,System.Web.HttpResponse.Cookies 集合状态被检测为已更改,并且 Set-Cookie 标头在 cookie 被序列化之前首先被清除标头值。
在这种情况下,OWIN 响应 cookie “丢失”,用户未通过身份验证并被重定向回登录页面。
案例 3 - 在用户尝试验证之前使用会话
System.Web.SessionState.SessionStateModule,s_sessionEverSet 属性为真。会话 ID 由 SessionStateModule 生成,ASP.NET_SessionId 被添加到 System.Web.HttpResponse.Cookies。由于 System.Web.HttpCookieCollection 和 System.Web.HttpResponse.GenerateResponseHeadersForCookies() 的内部优化,Set-Cookie 标头未首先清除,但仅更新。
在这种情况下,OWIN 身份验证 cookie 和 ASP.NET_SessionId cookie 都会作为响应发送,并且登录正常。
Cookie 的更一般性问题
如您所见,问题更为普遍,不仅限于 ASP.NET 会话。如果您通过 Microsoft.Owin.Host.SystemWeb 托管 OWIN,并且您/某物直接使用 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 项目模板进行测试。
【讨论】:
我花了几天时间尝试在我们的测试环境中调试和解决这个问题。我发现的唯一解决方法与您建议的相同(在用户验证之前设置会话)。我已将问题报告给 katanaproject...katanaproject.codeplex.com/workitem/197,所以也许有人会在那里发表评论。 这是一个相当严重的缺陷,特别是因为他们打包在 vs2013 的模板中。 对于任何想进一步调查的人,我在 github.com/Neilski/IdentityBugDemo 创建了一个测试项目 由于使用了使用 Session 作为后备存储的 Controller.TempData 而遇到了这个问题。如果先前请求中不存在 ASP_NET.SessionId cookie,则可以轻松重现无法登录的情况。 终于!这是一个多么奇怪的问题。谢谢你。在写完这个答案两年多之后,这仍然是一个问题。【参考方案2】:简而言之,.NET cookie 管理器将战胜 OWIN cookie 管理器并覆盖 OWIN 层上设置的 cookie。解决方法是使用SystemWebCookieManager class, provided as a solution on the Katana Project here。您需要使用此类或类似的类,这将强制 OWIN 使用 .NET cookie 管理器,因此不会出现不一致:
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 项目的外部链接可能会断开并且这也应该作为一个解决方案在这里被充分记录。
【讨论】:
谢谢,它为我工作,但我也通过调用此 ControllerContext.HttpContext.Session.RemoveAll(); 清除所有会话;在 externallogincallback 函数中 适用于 ASP.NET Webforms 4.6.1 吗?我的 webApp 使用ASP.NET Webforms, OWIN, ADFS
@Kiquenet 您的网络应用程序是否使用 OWIN cookie?那么是的。
在代码 Startup.ConfigureAuth
我们有 app.UseCookieAuthentication
和 app.UseWsFederationAuthentication
最后是 app.UseStageMarker
@Alexandru 您可能会考虑进行编辑,我的团队遇到了这个错误,它很少见且随机,它通过 DEV 和 UAT 环境对我们隐藏。您回答中的这句话对我们不成立:“.NET cookie 管理器将永远获胜。”这将很容易找到和修复,如果 OWIN cookie 总是被覆盖,我们的任何 OIDC 中间件都不会离开我们的开发工作站。但随机性意味着该错误在它大规模袭击我们之前一直在生产中进行了 2 天(我们的一半内部用户无法通过 AAD 登录)。介意我从你的答案中删除“总是”这个词吗?【参考方案3】:
从@TomasDolezal 的精彩分析开始,我查看了 Owin 和 System.Web 源代码。
问题在于 System.Web 有自己的 cookie 信息主来源,而不是 Set-Cookie 标头。 Owin 只知道 Set-Cookie 标头。一种解决方法是确保 Owin 设置的所有 cookie 也设置在 HttpContext.Current.Response.Cookies
集合中。
我已经制作了一个小型中间件(source,nuget),它可以直接放在 cookie 中间件注册的上方。
app.UseKentorOwinCookieSaver();
app.UseCookieAuthentication(new CookieAuthenticationOptions());
【讨论】:
会试一试。由于在asp.net Identity 2.2.0-alpha1之后,我不仅在登录时出现问题,而且在用户注销时也出现问题(用户不会在注销时注销单击|通常,以防我让网站打开一段时间什么都不做|)..在用户登录之前设置会话解决了登录问题但注销问题仍然存在..感谢您的努力..顺便说一句,除了安装包裹? 您必须在 Startup.Auth.cs 中使用app.UseKentorCookieMiddlewareSaver();
激活它。它也应该处理注销 cookie 清除。
非常感谢 Anders Abel,现在登录和注销都可以正常工作。但是上面注释中的代码需要更改(因为我遵循它:) 没有任何成功)为:app.UseKentorOwinCookieSaver()
并且可能包含在您的原始答案中,如包的GitHub page 中。
感谢您注意到不正确的文档。它实际上已经在 GitHub 页面上修复了,但我现在也在这里更新了我的答案。
@AndersAbel 我正在尝试为这个 github 项目添加 Meetup 注册:github.com/owin-middleware/OwinOAuthProviders''。前几天我添加了 Asana,没有任何问题,但由于某种原因,使用 Meetup,Account//ExternalLoginCallback 中的 await AuthenticationManager.GetExternalLoginInfoAsync() 方法返回 null。不幸的是,您的 NuGet 包没有解决我的问题。我想知道您是否有时间与我一起审查,因为您可能能够更好地解决问题并推动您的项目。【参考方案4】:
Katana 团队回复了issue Tomas Dolezar 提出的问题,并发布了documentation about workarounds:
解决方法分为两类。一是重新配置 System.Web,因此它避免使用 Response.Cookies 集合和 覆盖 OWIN cookie。另一种方法是重新配置 受影响的 OWIN 组件,因此它们直接将 cookie 写入 System.Web 的 Response.Cookies 集合。
确保在身份验证之前建立会话:System.Web 和 Katana cookie 之间的冲突是每个请求的,所以它可能是 应用程序可以根据某些请求建立会话 在身份验证流程之前。这应该很容易做到,当 用户先到,但以后可能更难保证 session 或 auth cookie 过期和/或需要刷新。 禁用 SessionStateModule - 如果应用程序不依赖会话信息,但会话模块仍在设置 导致上述冲突的cookie,那么您可以考虑禁用 会话状态模块。 重新配置 CookieAuthenticationMiddleware 以直接写入 System.Web 的 cookie 集合。
app.UseCookieAuthentication(new CookieAuthenticationOptions
// ...
CookieManager = new SystemWebCookieManager()
);
请参阅文档中的 SystemWebCookieManager 实现(上面的链接)
更多信息here
编辑
以下是我们为解决问题所采取的步骤。 1. 和 2. 也分别解决了这个问题,但我们决定同时应用两者以防万一:
1。 使用SystemWebCookieManager
2。 设置会话变量:
protected override void Initialize(RequestContext requestContext)
base.Initialize(requestContext);
// See http://***.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 project 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),
);
【讨论】:
我发现这个答案更直接,更容易解决问题。谢谢,-也许我说话太突然了。这并没有解决我的问题。 @JCS 包括我们为解决问题而采取的步骤。您是否查明您的问题是否相关? 我正在使用 Web Api 2 + Owin 中间件 + redis 缓存进行会话管理以进行身份验证。我尝试使用 SystemWebCookieManager 并没有解决我遇到的未设置身份验证 cookie 的问题。使用“UseKentorOwinCookieSaver”解决了它,但我不太喜欢额外的外部依赖...... 清除会话对我有用。不需要外部依赖。在调用ChallengeResult()
之前,将此ControllerContext.HttpContext.Session.RemoveAll();
放入您的ExternalLogin()
操作中。不知道是不是最好的解决办法,反正是最简单的。
@chemitaxis 当然,请注意 ?.
(空条件运算符)仅适用于 C# 6。【参考方案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()
...
);
【讨论】:
在 3.1.0 中这仍然是一个问题吗? 是的,对我来说这在 3.1.0 中仍然是一个问题,需要这个 cookie 管理器,因为默认的仍然是 ChunkingCookieManager。 可以用在什么地方?以及如何? @jonmeyer 谢谢。我想昨天我错过了 SystemCCM 和 CCM 之间的区别,所以我一定会检查一下 即使在添加了上面的行之后它对我也不起作用。我正在使用 3.1.0 版本。主要是我第一次可以登录,但注销后它不允许我登录。【参考方案6】:如果您自己在 OWIN 中间件中设置 cookie,那么使用 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】:我在 Visual Studio 2017 和 .net MVC 5.2.4 中遇到了类似问题,正在更新 Nuget Microsoft.Owin.Security.Google到当前为 4.0.1 的最新版本为我工作! 希望这对某人有帮助!
【讨论】:
把我的培根保存在这个上! android Chrome 出现问题,特别是随机丢失身份验证。此线程中的其他任何内容均无效。我正在使用 VS2019 和 ASP MVC 5。【参考方案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);
【讨论】:
你把这个代码HttpContext.Current.Session["RunSession"] = "1";
放在哪里?在 Globa.asax Session_Start
?
它实际上是最简单和最快的解决方案,并且直到该问题的解决方案不会包含在框架中(已经宣布它将包含在内) - 例如,我更喜欢单线,而不是一个类+一堆依赖项。恕我直言,这个解决方案被低估了。
我在我的 AuthManager 中添加到 IssueAuthToken 方法的顶部【参考方案9】:
我有相同的症状,即未发送 Set-Cookie 标头,但这些答案都没有帮助我。一切都在我的本地机器上运行,但是当部署到生产环境时,set-cookie 标头永远不会设置。
事实证明,这是使用自定义 CookieAuthenticationMiddleware
和 WebApi 以及 WebApi compression support 的组合
幸运的是,我在我的项目中使用了 ELMAH,这让我记录了这个异常:
System.Web.HttpException 服务器无法在 HTTP 之后附加标头 标头已发送。
这让我想到了这个GitHub Issue
基本上,如果您有像我这样的奇怪设置,您会想要 disable compression 为您设置 cookie 的 WebApi 控制器/方法,或尝试 OwinServerCompressionHandler
。
【讨论】:
以上是关于ASP.NET_SessionId + OWIN Cookies 不发送到浏览器的主要内容,如果未能解决你的问题,请参考以下文章
当 requireSSL 为 true 时,asp.net_sessionid 是不是应该出现在 http 请求中
Chrome 和 IE 不发送 ASP.NET_SessionID - 即会话变量丢失?
如果定义了 Session_Start,ASP.NET 如何知道创建 ASP.NET_SessionId cookie?