Skip to content
This repository has been archived by the owner on Apr 23, 2020. It is now read-only.

Added support for the SameSite cookie attribute #24

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 7 additions & 6 deletions Kentor.OwinCookieSaver.Tests/CookieParserTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,15 @@ public void CookieParser_NameAndValueOnly()
Path = null
};

result.ShouldBeEquivalentTo(expected);
result.Should().BeEquivalentTo(expected);
}

[TestMethod]
public void CookieParser_AllFields()
{
var cookieHeader = new List<string>()
{
"Name=Value; Domain=example.com; Expires=Wed, 13 Jan 2021, 22:23:01 GMT; HttpOnly; Secure; Path=/SomePath"
"Name=Value; Domain=example.com; Expires=Wed, 13 Jan 2021, 22:23:01 GMT; HttpOnly; Secure; Path=/SomePath; SameSite=Lax"
};

var result = CookieParser.Parse(cookieHeader).Single();
Expand All @@ -43,10 +43,11 @@ public void CookieParser_AllFields()
Expires = new DateTime(2021, 01, 13, 22, 23, 01, DateTimeKind.Utc).ToLocalTime(),
HttpOnly = true,
Path = "/SomePath",
Secure = true
Secure = true,
SameSite = SameSiteMode.Lax
};

result.ShouldBeEquivalentTo(expected);
result.Should().BeEquivalentTo(expected);
}

[TestMethod]
Expand All @@ -72,7 +73,7 @@ public void CookieParser_DateWithDifferentCulture()
Path = null
};

result.ShouldBeEquivalentTo(expected);
result.Should().BeEquivalentTo(expected);
}
finally
{
Expand Down Expand Up @@ -100,7 +101,7 @@ public void CookieParser_StrangeCasing()
Secure = true
};

result.ShouldBeEquivalentTo(expected);
result.Should().BeEquivalentTo(expected);
}
}
}
14 changes: 7 additions & 7 deletions Kentor.OwinCookieSaver.Tests/HttpCookieExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using System.Web;

namespace Kentor.OwinCookieSaver.Tests
{
static class HttpCookieExtensions
{
static PropertyInfo fromHeaderProperty = typeof(HttpCookie).GetProperty(
"FromHeader", BindingFlags.NonPublic | BindingFlags.Instance);
/// <summary>
/// The "From Header" Property no longer exists in HttpCookie :-(
/// </summary>
[Obsolete("The FromHeader property no longer exists in System.Web.HttpCookie")]
private static readonly PropertyInfo FromHeaderProperty = typeof(HttpCookie).GetProperty("FromHeader", BindingFlags.NonPublic | BindingFlags.Instance);

[Obsolete("The FromHeader property no longer exists in System.Web.HttpCookie")]
public static bool IsFromHeader(this HttpCookie cookie)
{
return (bool)fromHeaderProperty.GetValue(cookie);
return (bool)FromHeaderProperty.GetValue(cookie);
}
}
}
38 changes: 19 additions & 19 deletions Kentor.OwinCookieSaver.Tests/Kentor.OwinCookieSaver.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>Kentor.OwinCookieSaver.Tests</RootNamespace>
<AssemblyName>Kentor.OwinCookieSaver.Tests</AssemblyName>
<TargetFrameworkVersion>v4.5.2</TargetFrameworkVersion>
<TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<ProjectTypeGuids>{3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">10.0</VisualStudioVersion>
Expand All @@ -35,22 +35,11 @@
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
<GenerateBindingRedirectsOutputType>true</GenerateBindingRedirectsOutputType>
</PropertyGroup>
<ItemGroup>
<Reference Include="FluentAssertions">
<HintPath>..\packages\FluentAssertions.3.2.1\lib\net45\FluentAssertions.dll</HintPath>
</Reference>
<Reference Include="FluentAssertions.Core">
<HintPath>..\packages\FluentAssertions.3.2.1\lib\net45\FluentAssertions.Core.dll</HintPath>
</Reference>
<Reference Include="Microsoft.Owin">
<HintPath>..\packages\Microsoft.Owin.3.0.0\lib\net45\Microsoft.Owin.dll</HintPath>
</Reference>
<Reference Include="NSubstitute">
<HintPath>..\packages\NSubstitute.1.8.0.0\lib\net45\NSubstitute.dll</HintPath>
</Reference>
<Reference Include="Owin">
<HintPath>..\packages\Owin.1.0\lib\net40\Owin.dll</HintPath>
</Reference>
<Reference Include="System" />
<Reference Include="System.Web" />
<Reference Include="System.Xml" />
Expand All @@ -75,15 +64,26 @@
<Compile Include="KentorOwinCookieSaverMiddlewareTests.cs" />
<Compile Include="CookieParserTests.cs" />
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Kentor.OwinCookieSaver\Kentor.OwinCookieSaver.csproj">
<Project>{21cefca1-e4ee-4581-8f10-f91c6c6f77e3}</Project>
<Name>Kentor.OwinCookieSaver</Name>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<PackageReference Include="FluentAssertions">
<Version>5.10.0</Version>
</PackageReference>
<PackageReference Include="Microsoft.Owin">
<Version>3.0.0</Version>
</PackageReference>
<PackageReference Include="NSubstitute">
<Version>4.2.1</Version>
</PackageReference>
<PackageReference Include="Owin">
<Version>1.0.0</Version>
</PackageReference>
</ItemGroup>
<Choose>
<When Condition="'$(VisualStudioVersion)' == '10.0' And '$(IsCodedUITest)' == 'True'">
<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ public OwinContext CreateOwinContext()
{
var httpRequest = new HttpRequest("", "http://localhost/", "");
var stringWriter = new StringWriter();
var httpResponce = new HttpResponse(stringWriter);
var httpContext = new HttpContext(httpRequest, httpResponce);
var httpResponse = new HttpResponse(stringWriter);
var httpContext = new HttpContext(httpRequest, httpResponse);
var httpContextBase = new HttpContextWrapper(httpContext);

var context = new OwinContext();
Expand All @@ -36,6 +36,9 @@ public OwinContext CreateOwinContext()

class MiddlewareMock : OwinMiddleware
{
public const string OwinCookie = "OwinCookie=OwinValue; expires=Wed, 13-Jan-2021 22:23:01 GMT; path=/; secure; HttpOnly; SameSite=Strict";
public const string MinimalCookie = "MinimalCookie=MinimalValue";

public MiddlewareMock() : base(null) { }

public IOwinContext callingContext = null;
Expand All @@ -46,8 +49,8 @@ public override Task Invoke(IOwinContext context)

context.Response.Headers.Add("Set-Cookie", new[]
{
"OwinCookie=OwinValue; expires=Wed, 13-Jan-2021 22:23:01 GMT; path=/; secure; HttpOnly",
"MinimalCookie=MinimalValue"
OwinCookie,
MinimalCookie
});

context.Response.Cookies.Append("ComplexCookie", "ComplexValue", new CookieOptions
Expand Down Expand Up @@ -94,6 +97,7 @@ public async Task KentorOwinCookieSaverMiddleware_AddsOwinCookies()
await subject.Invoke(context);

httpContext.Response.Cookies.AllKeys.Should().Contain("OwinCookie");
httpContext.Response.Cookies.AllKeys.Should().Contain("SystemWebCookie", because: "The original cookie should not be removed");

var cookie = httpContext.Response.Cookies["OwinCookie"];

Expand All @@ -106,20 +110,42 @@ public async Task KentorOwinCookieSaverMiddleware_AddsOwinCookies()
cookie.Expires.Should().Be(expectedExpires);
cookie.Secure.Should().BeTrue("cookie string contains Secure");
cookie.HttpOnly.Should().BeTrue("cookie string contains HttpOnly");
cookie.IsFromHeader().Should().BeTrue();
cookie.SameSite.Should().Be(SameSiteMode.Strict, "cookie string contains Strict SameSite setting");
//cookie.IsFromHeader().Should().BeTrue();
}

[TestMethod]
public async Task KentorOwinCookieSaverMiddleware_ShouldNotOverwriteResponseCookie()
{
var context = CreateOwinContext();

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

httpContext.Response.Cookies.Add(new HttpCookie("OwinCookie", "TheOriginalCookie"));

var next = new MiddlewareMock();

var subject = new KentorOwinCookieSaverMiddleware(next);

await subject.Invoke(context);

httpContext.Response.Cookies.AllKeys.Should().Contain("OwinCookie");

var cookie = httpContext.Response.Cookies["OwinCookie"];
cookie.Should().NotBeNull();
cookie.Value.Should().Be("TheOriginalCookie");
}

[TestMethod]
public async Task KentorOwinCookieSaverMiddleware_RoundtripsComplexCookie()
public async Task KentorOwinCookieSaverMiddleware_RoundTripsComplexCookie()
{
var context = CreateOwinContext();
var next = new MiddlewareMock();
var subject = new KentorOwinCookieSaverMiddleware(next);

await subject.Invoke(context);

// The first cookie is 85 chars long.
var before = context.Response.Headers["Set-Cookie"].Substring(0, 85);
var before = MiddlewareMock.OwinCookie;

var rebuiltHeader = RegenerateSetCookieHeader(context)
.Single(s => s.StartsWith("OwinCookie"));
Expand All @@ -128,16 +154,15 @@ public async Task KentorOwinCookieSaverMiddleware_RoundtripsComplexCookie()
}

[TestMethod]
public async Task KentorOwinCookieSaverMiddleware_RoundtripsMinimalCookie()
public async Task KentorOwinCookieSaverMiddleware_RoundTripsMinimalCookie()
{
var context = CreateOwinContext();
var next = new MiddlewareMock();
var subject = new KentorOwinCookieSaverMiddleware(next);

await subject.Invoke(context);

// The interesting cookie is at offset 86 and is 26 chars long.
var before = context.Response.Headers["Set-Cookie"].Substring(86, 26);
var before = MiddlewareMock.MinimalCookie;

var rebuiltHeader = RegenerateSetCookieHeader(context)
.Single(s => s.StartsWith("MinimalCookie"));
Expand Down Expand Up @@ -252,6 +277,12 @@ private static IEnumerable<string> RegenerateSetCookieHeader(IOwinContext contex
s.Append("; HttpOnly");
}

if (Enum.IsDefined(typeof(SameSiteMode), cookie.SameSite))
{
s.Append("; SameSite=");
s.Append(cookie.SameSite.ToString());
}

yield return s.ToString();
}
}
Expand Down
7 changes: 0 additions & 7 deletions Kentor.OwinCookieSaver.Tests/packages.config

This file was deleted.

85 changes: 51 additions & 34 deletions Kentor.OwinCookieSaver/CookieParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,51 +10,68 @@ namespace Kentor.OwinCookieSaver
{
static class CookieParser
{
public static IEnumerable<HttpCookie> Parse(IList<string> setCookieHeader)
public static IEnumerable<HttpCookie> Parse(IEnumerable<string> setCookieHeader)
{
foreach(var c in setCookieHeader)
return setCookieHeader.Select(MakeHttpCookie);
}

private static HttpCookie MakeHttpCookie(string cookieText)
{
var segments = cookieText.Split(';');

var nameAndValue = GetKeyValue(segments[0]);
HttpCookie cookie = new HttpCookie(nameAndValue.Key, nameAndValue.Value)
{
var segments = c.Split(';');
// Path defaults to /, want to be able to roundtrip non-existing field.
Path = null
};

var nameAndValue = GetKeyValue(segments[0]);
// First key-value-pair is cookie name and value, now look at the rest.
for (int i = 1; i < segments.Length; i++)
{
var kv = GetKeyValue(segments[i]);

HttpCookie cookie = new HttpCookie(nameAndValue.Key, nameAndValue.Value)
if ("Expires".Equals(kv.Key, StringComparison.OrdinalIgnoreCase))
{
// Path defaults to /, want to be able to roundtrip non-existing field.
Path = null
};

// First key-value-pair is cookie name and value, now look at the rest.
for (int i = 1; i < segments.Length; i++)
cookie.Expires = DateTime.Parse(kv.Value, CultureInfo.InvariantCulture);
}
else if ("Secure".Equals(kv.Key, StringComparison.OrdinalIgnoreCase))
{
var kv = GetKeyValue(segments[i]);

if("Expires".Equals(kv.Key, StringComparison.OrdinalIgnoreCase))
{
cookie.Expires = DateTime.Parse(kv.Value, CultureInfo.InvariantCulture);
}
else if("Secure".Equals(kv.Key, StringComparison.OrdinalIgnoreCase))
{
cookie.Secure = true;
}
else if("HttpOnly".Equals(kv.Key, StringComparison.OrdinalIgnoreCase))
{
cookie.HttpOnly = true;
}
else if("Path".Equals(kv.Key, StringComparison.OrdinalIgnoreCase))
{
cookie.Path = kv.Value;
}
else if ("Domain".Equals(kv.Key, StringComparison.OrdinalIgnoreCase))
{
cookie.Domain = kv.Value;
}
cookie.Secure = true;
}
else if ("HttpOnly".Equals(kv.Key, StringComparison.OrdinalIgnoreCase))
{
cookie.HttpOnly = true;
}
else if ("Path".Equals(kv.Key, StringComparison.OrdinalIgnoreCase))
{
cookie.Path = kv.Value;
}
else if ("Domain".Equals(kv.Key, StringComparison.OrdinalIgnoreCase))
{
cookie.Domain = kv.Value;
}
else if ("SameSite".Equals(kv.Key, StringComparison.OrdinalIgnoreCase))
{
cookie.SameSite = ToSameSiteMode(kv.Value, cookie.SameSite);
}
}

yield return cookie;
return cookie;
}

private static SameSiteMode ToSameSiteMode(string value, SameSiteMode defaultValue)
{
if (Enum.TryParse(value, true, out SameSiteMode result))
{
return result;
}

// The given value couldn't be parsed, so return the given default
return defaultValue;
}


public static KeyValuePair<string, string> GetKeyValue(string segment)
{
var separatorIndex = segment.IndexOf('=');
Expand Down
Loading