diff --git a/src/Http/Headers/src/PublicAPI.Unshipped.txt b/src/Http/Headers/src/PublicAPI.Unshipped.txt
index 7dc5c58110bf..cb25301411fe 100644
--- a/src/Http/Headers/src/PublicAPI.Unshipped.txt
+++ b/src/Http/Headers/src/PublicAPI.Unshipped.txt
@@ -1 +1,3 @@
#nullable enable
+Microsoft.Net.Http.Headers.SetCookieHeaderValue.Partitioned.get -> bool
+Microsoft.Net.Http.Headers.SetCookieHeaderValue.Partitioned.set -> void
diff --git a/src/Http/Headers/src/SetCookieHeaderValue.cs b/src/Http/Headers/src/SetCookieHeaderValue.cs
index 359e6014dae2..19e6e62a768c 100644
--- a/src/Http/Headers/src/SetCookieHeaderValue.cs
+++ b/src/Http/Headers/src/SetCookieHeaderValue.cs
@@ -30,6 +30,7 @@ public class SetCookieHeaderValue
private static readonly string SameSiteStrictToken = SameSiteMode.Strict.ToString().ToLowerInvariant();
private const string HttpOnlyToken = "httponly";
+ private const string PartitionedToken = "partitioned";
private const string SeparatorToken = "; ";
private const string EqualsToken = "=";
private const int ExpiresDateLength = 29;
@@ -176,6 +177,16 @@ public StringSegment Value
/// See .
public bool HttpOnly { get; set; }
+ ///
+ /// Gets or sets a value for the Partitioned cookie attribute.
+ ///
+ /// Partitioned instructs the user agent to
+ /// omit the cookie when providing access to cookies on a different top-level site
+ /// as part of CHIPS (Cookies Having Independent Partitioned State).
+ ///
+ ///
+ public bool Partitioned { get; set; }
+
///
/// Gets a collection of additional values to append to the cookie.
///
@@ -241,6 +252,11 @@ public override string ToString()
length += SeparatorToken.Length + HttpOnlyToken.Length;
}
+ if (Partitioned)
+ {
+ length += SeparatorToken.Length + PartitionedToken.Length;
+ }
+
if (_extensions?.Count > 0)
{
foreach (var extension in _extensions)
@@ -299,6 +315,11 @@ public override string ToString()
AppendSegment(ref span, HttpOnlyToken, null);
}
+ if (headerValue.Partitioned)
+ {
+ AppendSegment(ref span, PartitionedToken, null);
+ }
+
if (_extensions?.Count > 0)
{
foreach (var extension in _extensions)
@@ -384,6 +405,11 @@ public void AppendToStringBuilder(StringBuilder builder)
AppendSegment(builder, HttpOnlyToken, null);
}
+ if (Partitioned)
+ {
+ AppendSegment(builder, PartitionedToken, null);
+ }
+
if (_extensions?.Count > 0)
{
foreach (var extension in _extensions)
@@ -654,6 +680,11 @@ private static int GetSetCookieLength(StringSegment input, int startIndex, out S
{
result.HttpOnly = true;
}
+ // partitioned-av = "Partitioned"
+ else if (StringSegment.Equals(token, PartitionedToken, StringComparison.OrdinalIgnoreCase))
+ {
+ result.Partitioned = true;
+ }
// extension-av =
else
{
@@ -729,6 +760,7 @@ public override bool Equals(object? obj)
&& Secure == other.Secure
&& SameSite == other.SameSite
&& HttpOnly == other.HttpOnly
+ && Partitioned == other.Partitioned
&& HeaderUtilities.AreEqualCollections(_extensions, other._extensions, StringSegmentComparer.OrdinalIgnoreCase);
}
@@ -743,7 +775,8 @@ public override int GetHashCode()
^ (Path != null ? StringSegmentComparer.OrdinalIgnoreCase.GetHashCode(Path) : 0)
^ Secure.GetHashCode()
^ SameSite.GetHashCode()
- ^ HttpOnly.GetHashCode();
+ ^ HttpOnly.GetHashCode()
+ ^ Partitioned.GetHashCode();
if (_extensions?.Count > 0)
{
diff --git a/src/Http/Http.Abstractions/src/CookieBuilder.cs b/src/Http/Http.Abstractions/src/CookieBuilder.cs
index f846008d5444..85335ed219fb 100644
--- a/src/Http/Http.Abstractions/src/CookieBuilder.cs
+++ b/src/Http/Http.Abstractions/src/CookieBuilder.cs
@@ -49,6 +49,15 @@ public virtual string? Name
///
public virtual bool HttpOnly { get; set; }
+ ///
+ /// Gets or sets a value that indicates whether a cookie is partitioned across different sites.
+ /// Opts in to CHIPS (Cookies Having Independent Partitioned State).
+ ///
+ ///
+ /// Determines the value that will be set on .
+ ///
+ public virtual bool Partitioned { get; set; }
+
///
/// The SameSite attribute of the cookie. The default value is
/// but specific components may use a different value.
@@ -111,6 +120,7 @@ public virtual CookieOptions Build(HttpContext context, DateTimeOffset expiresFr
Path = Path ?? "/",
SameSite = SameSite,
HttpOnly = HttpOnly,
+ Partitioned = Partitioned,
MaxAge = MaxAge,
Domain = Domain,
IsEssential = IsEssential,
diff --git a/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt b/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt
index b08a98e0390c..57e483ecb3bf 100644
--- a/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt
+++ b/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt
@@ -4,3 +4,5 @@ Microsoft.AspNetCore.Http.HostString.HostString(string? value) -> void
*REMOVED*Microsoft.AspNetCore.Http.HostString.Value.get -> string!
Microsoft.AspNetCore.Http.HostString.Value.get -> string?
Microsoft.AspNetCore.Http.HttpValidationProblemDetails.HttpValidationProblemDetails(System.Collections.Generic.IEnumerable>! errors) -> void
+virtual Microsoft.AspNetCore.Http.CookieBuilder.Partitioned.get -> bool
+virtual Microsoft.AspNetCore.Http.CookieBuilder.Partitioned.set -> void
diff --git a/src/Http/Http.Features/src/CookieOptions.cs b/src/Http/Http.Features/src/CookieOptions.cs
index e652276eee5f..4cff5a0f074e 100644
--- a/src/Http/Http.Features/src/CookieOptions.cs
+++ b/src/Http/Http.Features/src/CookieOptions.cs
@@ -40,6 +40,7 @@ public CookieOptions(CookieOptions options)
Secure = options.Secure;
SameSite = options.SameSite;
HttpOnly = options.HttpOnly;
+ Partitioned = options.Partitioned;
MaxAge = options.MaxAge;
IsEssential = options.IsEssential;
@@ -85,6 +86,13 @@ public CookieOptions(CookieOptions options)
/// true if a cookie must not be accessible by client-side script; otherwise, false.
public bool HttpOnly { get; set; }
+ ///
+ /// Gets or sets a value that indicates whether a cookie is partitioned across different sites.
+ /// Opts in to CHIPS (Cookies Having Independent Partitioned State).
+ ///
+ /// true if a cookie is partitioned; otherwise, false.
+ public bool Partitioned { get; set; }
+
///
/// Gets or sets the max-age for the cookie.
///
@@ -117,6 +125,7 @@ public SetCookieHeaderValue CreateCookieHeader(string name, string value)
Expires = Expires,
Secure = Secure,
HttpOnly = HttpOnly,
+ Partitioned = Partitioned,
MaxAge = MaxAge,
SameSite = (Net.Http.Headers.SameSiteMode)SameSite,
};
diff --git a/src/Http/Http.Features/src/PublicAPI.Unshipped.txt b/src/Http/Http.Features/src/PublicAPI.Unshipped.txt
index 7dc5c58110bf..5a814dd666a5 100644
--- a/src/Http/Http.Features/src/PublicAPI.Unshipped.txt
+++ b/src/Http/Http.Features/src/PublicAPI.Unshipped.txt
@@ -1 +1,3 @@
#nullable enable
+Microsoft.AspNetCore.Http.CookieOptions.Partitioned.get -> bool
+Microsoft.AspNetCore.Http.CookieOptions.Partitioned.set -> void
diff --git a/src/Http/Http/src/Internal/ResponseCookies.cs b/src/Http/Http/src/Internal/ResponseCookies.cs
index 6b55d15c4a8c..7d739c476562 100644
--- a/src/Http/Http/src/Internal/ResponseCookies.cs
+++ b/src/Http/Http/src/Internal/ResponseCookies.cs
@@ -1,6 +1,7 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
+using System.Diagnostics.CodeAnalysis;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
@@ -15,7 +16,9 @@ namespace Microsoft.AspNetCore.Http;
internal sealed partial class ResponseCookies : IResponseCookies
{
private readonly IFeatureCollection _features;
+
private ILogger? _logger;
+ private bool _retrievedLogger;
///
/// Create a new wrapper.
@@ -45,19 +48,10 @@ public void Append(string key, string value, CookieOptions options)
{
ArgumentNullException.ThrowIfNull(options);
- // SameSite=None cookies must be marked as Secure.
- if (!options.Secure && options.SameSite == SameSiteMode.None)
+ var messagesToLog = GetMessagesToLog(options);
+ if (messagesToLog != MessagesToLog.None && TryGetLogger(out var logger))
{
- if (_logger == null)
- {
- var services = _features.Get()?.RequestServices;
- _logger = services?.GetService>();
- }
-
- if (_logger != null)
- {
- Log.SameSiteCookieNotSecure(_logger, key);
- }
+ LogMessages(logger, messagesToLog, key);
}
var cookie = options.CreateCookieHeader(key, Uri.EscapeDataString(value)).ToString();
@@ -69,21 +63,12 @@ public void Append(ReadOnlySpan> keyValuePairs, Coo
{
ArgumentNullException.ThrowIfNull(options);
- // SameSite=None cookies must be marked as Secure.
- if (!options.Secure && options.SameSite == SameSiteMode.None)
+ var messagesToLog = GetMessagesToLog(options);
+ if (messagesToLog != MessagesToLog.None && TryGetLogger(out var logger))
{
- if (_logger == null)
+ foreach (var keyValuePair in keyValuePairs)
{
- var services = _features.Get()?.RequestServices;
- _logger = services?.GetService>();
- }
-
- if (_logger != null)
- {
- foreach (var keyValuePair in keyValuePairs)
- {
- Log.SameSiteCookieNotSecure(_logger, keyValuePair.Key);
- }
+ LogMessages(logger, messagesToLog, keyValuePair.Key);
}
}
@@ -167,9 +152,95 @@ public void Delete(string key, CookieOptions options)
});
}
+ private bool TryGetLogger([NotNullWhen(true)] out ILogger? logger)
+ {
+ if (!_retrievedLogger)
+ {
+ _retrievedLogger = true;
+ var services = _features.Get()?.RequestServices;
+ _logger = services?.GetService>();
+ }
+
+ logger = _logger;
+ return logger is not null;
+ }
+
+ private static MessagesToLog GetMessagesToLog(CookieOptions options)
+ {
+ var toLog = MessagesToLog.None;
+
+ if (!options.Secure && options.SameSite == SameSiteMode.None)
+ {
+ toLog |= MessagesToLog.SameSiteNotSecure;
+ }
+
+ if (options.Partitioned)
+ {
+ if (!options.Secure)
+ {
+ toLog |= MessagesToLog.PartitionedNotSecure;
+ }
+
+ if (options.SameSite != SameSiteMode.None)
+ {
+ toLog |= MessagesToLog.PartitionedNotSameSiteNone;
+ }
+
+ // Chromium checks this
+ if (options.Path != "/")
+ {
+ toLog |= MessagesToLog.PartitionedNotPathRoot;
+ }
+ }
+
+ return toLog;
+ }
+
+ private static void LogMessages(ILogger logger, MessagesToLog messages, string cookieName)
+ {
+ if ((messages & MessagesToLog.SameSiteNotSecure) != 0)
+ {
+ Log.SameSiteCookieNotSecure(logger, cookieName);
+ }
+
+ if ((messages & MessagesToLog.PartitionedNotSecure) != 0)
+ {
+ Log.PartitionedCookieNotSecure(logger, cookieName);
+ }
+
+ if ((messages & MessagesToLog.PartitionedNotSameSiteNone) != 0)
+ {
+ Log.PartitionedCookieNotSameSiteNone(logger, cookieName);
+ }
+
+ if ((messages & MessagesToLog.PartitionedNotPathRoot) != 0)
+ {
+ Log.PartitionedCookieNotPathRoot(logger, cookieName);
+ }
+ }
+
+ [Flags]
+ private enum MessagesToLog
+ {
+ None,
+ SameSiteNotSecure = 1 << 0,
+ PartitionedNotSecure = 1 << 1,
+ PartitionedNotSameSiteNone = 1 << 2,
+ PartitionedNotPathRoot = 1 << 3,
+ }
+
private static partial class Log
{
- [LoggerMessage(1, LogLevel.Warning, "The cookie '{name}' has set 'SameSite=None' and must also set 'Secure'.", EventName = "SameSiteNotSecure")]
+ [LoggerMessage(1, LogLevel.Warning, "The cookie '{name}' has set 'SameSite=None' and must also set 'Secure'. This cookie will likely be rejected by the client.", EventName = "SameSiteNotSecure")]
public static partial void SameSiteCookieNotSecure(ILogger logger, string name);
+
+ [LoggerMessage(2, LogLevel.Warning, "The cookie '{name}' has set 'Partitioned' and must also set 'Secure'. This cookie will likely be rejected by the client.", EventName = "PartitionedNotSecure")]
+ public static partial void PartitionedCookieNotSecure(ILogger logger, string name);
+
+ [LoggerMessage(3, LogLevel.Debug, "The cookie '{name}' has set 'Partitioned' and should also set 'SameSite=None'. This cookie will likely be rejected by the client.", EventName = "PartitionedNotSameSiteNone")]
+ public static partial void PartitionedCookieNotSameSiteNone(ILogger logger, string name);
+
+ [LoggerMessage(4, LogLevel.Debug, "The cookie '{name}' has set 'Partitioned' and should also set 'Path=/'. This cookie may be rejected by the client.", EventName = "PartitionedNotPathRoot")]
+ public static partial void PartitionedCookieNotPathRoot(ILogger logger, string name);
}
}
diff --git a/src/Http/Http/test/CookieOptionsTests.cs b/src/Http/Http/test/CookieOptionsTests.cs
index 59c9db46fef0..83e6ec0b0fa0 100644
--- a/src/Http/Http/test/CookieOptionsTests.cs
+++ b/src/Http/Http/test/CookieOptionsTests.cs
@@ -23,6 +23,7 @@ public void CopyCtor_AllPropertiesCopied()
HttpOnly = true,
IsEssential = true,
MaxAge = TimeSpan.FromSeconds(10),
+ Partitioned = true,
Path = "/foo",
Secure = true,
SameSite = SameSiteMode.Strict,
@@ -40,6 +41,7 @@ public void CopyCtor_AllPropertiesCopied()
case "HttpOnly":
case "IsEssential":
case "MaxAge":
+ case "Partitioned":
case "Path":
case "Secure":
case "SameSite":
diff --git a/src/Http/Http/test/ResponseCookiesTest.cs b/src/Http/Http/test/ResponseCookiesTest.cs
index de5232eace68..a473d6787900 100644
--- a/src/Http/Http/test/ResponseCookiesTest.cs
+++ b/src/Http/Http/test/ResponseCookiesTest.cs
@@ -51,7 +51,176 @@ public void AppendSameSiteNoneWithoutSecureLogsWarning()
Assert.DoesNotContain("secure", cookieHeaderValues[0]);
var writeContext = Assert.Single(sink.Writes);
- Assert.Equal("The cookie 'TestCookie' has set 'SameSite=None' and must also set 'Secure'.", writeContext.Message);
+ Assert.Equal("The cookie 'TestCookie' has set 'SameSite=None' and must also set 'Secure'. This cookie will likely be rejected by the client.", writeContext.Message);
+ }
+
+ [Fact]
+ public void AppendSameSiteNoneWithoutSecureLogsWarningForEachCookie()
+ {
+ var headers = (IHeaderDictionary)new HeaderDictionary();
+ var features = MakeFeatures(headers);
+ var services = new ServiceCollection();
+
+ var sink = new TestSink(TestSink.EnableWithTypeName);
+ var loggerFactory = new TestLoggerFactory(sink, enabled: true);
+ services.AddLogging();
+ services.AddSingleton(loggerFactory);
+
+ features.Set(new ServiceProvidersFeature() { RequestServices = services.BuildServiceProvider() });
+
+ var cookies = new ResponseCookies(features);
+ var testCookie1 = "TestCookie1";
+ var testCookie2 = "TestCookie2";
+
+ var cookieDict = new[]
+ {
+ new KeyValuePair(testCookie1, "value1"),
+ new KeyValuePair(testCookie2, "value2"),
+ };
+
+ cookies.Append(cookieDict, new CookieOptions()
+ {
+ SameSite = SameSiteMode.None,
+ });
+
+ var cookieHeaderValues = headers.SetCookie;
+ Assert.All(headers.SetCookie, cookieHeaderValue =>
+ {
+ Assert.Contains("path=/", cookieHeaderValue);
+ Assert.Contains("samesite=none", cookieHeaderValue);
+ Assert.DoesNotContain("secure", cookieHeaderValue);
+ });
+
+ Assert.Collection(sink.Writes,
+ [
+ entry => Assert.Equal($"The cookie '{testCookie1}' has set 'SameSite=None' and must also set 'Secure'. This cookie will likely be rejected by the client.", entry.Message),
+ entry => Assert.Equal($"The cookie '{testCookie2}' has set 'SameSite=None' and must also set 'Secure'. This cookie will likely be rejected by the client.", entry.Message),
+ ]);
+ }
+
+ [Fact]
+ public void AppendPartitionedLogsWarnings()
+ {
+ var headers = (IHeaderDictionary)new HeaderDictionary();
+ var features = MakeFeatures(headers);
+ var services = new ServiceCollection();
+
+ var sink = new TestSink(TestSink.EnableWithTypeName);
+ var loggerFactory = new TestLoggerFactory(sink, enabled: true);
+ services.AddLogging();
+ services.AddSingleton(loggerFactory);
+
+ features.Set(new ServiceProvidersFeature() { RequestServices = services.BuildServiceProvider() });
+
+ var cookies = new ResponseCookies(features);
+ var testCookie = "TestCookie";
+
+ cookies.Append(testCookie, "value", new CookieOptions()
+ {
+ Partitioned = true,
+ // Missing SameSite = SameSiteMode.None,
+ // Missing Secure = true,
+ Path = "/a", // Should be Path = "/",
+ });
+
+ var cookieHeaderValues = headers.SetCookie;
+ Assert.Single(cookieHeaderValues);
+ Assert.Contains("partitioned", cookieHeaderValues[0]);
+ Assert.DoesNotContain("secure", cookieHeaderValues[0]);
+ Assert.DoesNotContain("samesite", cookieHeaderValues[0]);
+
+ Assert.Collection(sink.Writes,
+ [
+ entry => Assert.Equal($"The cookie '{testCookie}' has set 'Partitioned' and must also set 'Secure'. This cookie will likely be rejected by the client.", entry.Message),
+ entry => Assert.Equal($"The cookie '{testCookie}' has set 'Partitioned' and should also set 'SameSite=None'. This cookie will likely be rejected by the client.", entry.Message),
+ entry => Assert.Equal($"The cookie '{testCookie}' has set 'Partitioned' and should also set 'Path=/'. This cookie may be rejected by the client.", entry.Message),
+ ]);
+ }
+
+ [Fact]
+ public void AppendPartitionedLogsWarningsForEachCookie()
+ {
+ var headers = (IHeaderDictionary)new HeaderDictionary();
+ var features = MakeFeatures(headers);
+ var services = new ServiceCollection();
+
+ var sink = new TestSink(TestSink.EnableWithTypeName);
+ var loggerFactory = new TestLoggerFactory(sink, enabled: true);
+ services.AddLogging();
+ services.AddSingleton(loggerFactory);
+
+ features.Set(new ServiceProvidersFeature() { RequestServices = services.BuildServiceProvider() });
+
+ var cookies = new ResponseCookies(features);
+ var testCookie1 = "TestCookie1";
+ var testCookie2 = "TestCookie2";
+
+ var cookieDict = new[]
+ {
+ new KeyValuePair(testCookie1, "value1"),
+ new KeyValuePair(testCookie2, "value2"),
+ };
+
+ cookies.Append(cookieDict, new CookieOptions()
+ {
+ Partitioned = true,
+ // Missing SameSite = SameSiteMode.None,
+ // Missing Secure = true,
+ Path = "/a", // Should be Path = "/",
+ });
+
+ var cookieHeaderValues = headers.SetCookie;
+ Assert.All(headers.SetCookie, cookieHeaderValue =>
+ {
+ Assert.Contains("partitioned", cookieHeaderValue);
+ Assert.DoesNotContain("secure", cookieHeaderValue);
+ Assert.DoesNotContain("samesite", cookieHeaderValue);
+ });
+
+ Assert.Collection(sink.Writes,
+ [
+ entry => Assert.Equal($"The cookie '{testCookie1}' has set 'Partitioned' and must also set 'Secure'. This cookie will likely be rejected by the client.", entry.Message),
+ entry => Assert.Equal($"The cookie '{testCookie1}' has set 'Partitioned' and should also set 'SameSite=None'. This cookie will likely be rejected by the client.", entry.Message),
+ entry => Assert.Equal($"The cookie '{testCookie1}' has set 'Partitioned' and should also set 'Path=/'. This cookie may be rejected by the client.", entry.Message),
+ entry => Assert.Equal($"The cookie '{testCookie2}' has set 'Partitioned' and must also set 'Secure'. This cookie will likely be rejected by the client.", entry.Message),
+ entry => Assert.Equal($"The cookie '{testCookie2}' has set 'Partitioned' and should also set 'SameSite=None'. This cookie will likely be rejected by the client.", entry.Message),
+ entry => Assert.Equal($"The cookie '{testCookie2}' has set 'Partitioned' and should also set 'Path=/'. This cookie may be rejected by the client.", entry.Message),
+ ]);
+ }
+
+ [Fact]
+ public void AppendPartitionedCorrectlyDoesNotLog()
+ {
+ var headers = (IHeaderDictionary)new HeaderDictionary();
+ var features = MakeFeatures(headers);
+ var services = new ServiceCollection();
+
+ var sink = new TestSink(TestSink.EnableWithTypeName);
+ var loggerFactory = new TestLoggerFactory(sink, enabled: true);
+ services.AddLogging();
+ services.AddSingleton(loggerFactory);
+
+ features.Set(new ServiceProvidersFeature() { RequestServices = services.BuildServiceProvider() });
+
+ var cookies = new ResponseCookies(features);
+ var testCookie = "TestCookie";
+
+ cookies.Append(testCookie, "value", new CookieOptions()
+ {
+ Partitioned = true,
+ SameSite = SameSiteMode.None,
+ Secure = true,
+ // Path = "/", // implied
+ });
+
+ var cookieHeaderValues = headers.SetCookie;
+ Assert.Single(cookieHeaderValues);
+ Assert.Contains("partitioned", cookieHeaderValues[0]);
+ Assert.Contains("secure", cookieHeaderValues[0]);
+ Assert.Contains("samesite=none", cookieHeaderValues[0]);
+ Assert.Contains("path=/", cookieHeaderValues[0]);
+
+ Assert.Empty(sink.Writes);
}
[Fact]
diff --git a/src/Security/Authentication/test/CookieTests.cs b/src/Security/Authentication/test/CookieTests.cs
index bc9ff451415d..5d2646dc8559 100644
--- a/src/Security/Authentication/test/CookieTests.cs
+++ b/src/Security/Authentication/test/CookieTests.cs
@@ -363,6 +363,7 @@ public async Task CookieOptionsAlterSetCookieHeader()
o.Cookie.SecurePolicy = CookieSecurePolicy.Always;
o.Cookie.SameSite = SameSiteMode.None;
o.Cookie.HttpOnly = true;
+ o.Cookie.Partitioned = true;
o.Cookie.Extensions.Add("extension0");
o.Cookie.Extensions.Add("extension1=value1");
}, SignInAsAlice, baseAddress: new Uri("http://example.com/base"));
@@ -378,6 +379,7 @@ public async Task CookieOptionsAlterSetCookieHeader()
Assert.Contains(" secure", setCookie1);
Assert.Contains(" samesite=none", setCookie1);
Assert.Contains(" httponly", setCookie1);
+ Assert.Contains(" partitioned", setCookie1);
Assert.Contains(" extension0", setCookie1);
Assert.Contains(" extension1=value1", setCookie1);
@@ -400,6 +402,7 @@ public async Task CookieOptionsAlterSetCookieHeader()
Assert.DoesNotContain(" domain=", setCookie2);
Assert.DoesNotContain(" secure", setCookie2);
Assert.DoesNotContain(" httponly", setCookie2);
+ Assert.DoesNotContain(" partitioned", setCookie2);
Assert.DoesNotContain(" extension", setCookie2);
}