diff --git a/Security.sln b/Security.sln index 7fd810f8b..ee8180ede 100644 --- a/Security.sln +++ b/Security.sln @@ -46,6 +46,10 @@ Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Authorizat EndProject Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Authorization", "src\Microsoft.AspNet.Authorization\Microsoft.AspNet.Authorization.xproj", "{6AB3E514-5894-4131-9399-DC7D5284ADDB}" EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.CookiePolicy", "src\Microsoft.AspNet.CookiePolicy\Microsoft.AspNet.CookiePolicy.xproj", "{86183DC3-02A8-4A68-8B60-71ECEC066E79}" +EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.CookiePolicy.Test", "test\Microsoft.AspNet.CookiePolicy.Test\Microsoft.AspNet.CookiePolicy.Test.xproj", "{1790E052-646F-4529-B90E-6FEA95520D69}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -242,6 +246,30 @@ Global {6AB3E514-5894-4131-9399-DC7D5284ADDB}.Release|Mixed Platforms.Build.0 = Release|Any CPU {6AB3E514-5894-4131-9399-DC7D5284ADDB}.Release|x86.ActiveCfg = Release|Any CPU {6AB3E514-5894-4131-9399-DC7D5284ADDB}.Release|x86.Build.0 = Release|Any CPU + {86183DC3-02A8-4A68-8B60-71ECEC066E79}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {86183DC3-02A8-4A68-8B60-71ECEC066E79}.Debug|Any CPU.Build.0 = Debug|Any CPU + {86183DC3-02A8-4A68-8B60-71ECEC066E79}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {86183DC3-02A8-4A68-8B60-71ECEC066E79}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {86183DC3-02A8-4A68-8B60-71ECEC066E79}.Debug|x86.ActiveCfg = Debug|Any CPU + {86183DC3-02A8-4A68-8B60-71ECEC066E79}.Debug|x86.Build.0 = Debug|Any CPU + {86183DC3-02A8-4A68-8B60-71ECEC066E79}.Release|Any CPU.ActiveCfg = Release|Any CPU + {86183DC3-02A8-4A68-8B60-71ECEC066E79}.Release|Any CPU.Build.0 = Release|Any CPU + {86183DC3-02A8-4A68-8B60-71ECEC066E79}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {86183DC3-02A8-4A68-8B60-71ECEC066E79}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {86183DC3-02A8-4A68-8B60-71ECEC066E79}.Release|x86.ActiveCfg = Release|Any CPU + {86183DC3-02A8-4A68-8B60-71ECEC066E79}.Release|x86.Build.0 = Release|Any CPU + {1790E052-646F-4529-B90E-6FEA95520D69}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1790E052-646F-4529-B90E-6FEA95520D69}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1790E052-646F-4529-B90E-6FEA95520D69}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {1790E052-646F-4529-B90E-6FEA95520D69}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {1790E052-646F-4529-B90E-6FEA95520D69}.Debug|x86.ActiveCfg = Debug|Any CPU + {1790E052-646F-4529-B90E-6FEA95520D69}.Debug|x86.Build.0 = Debug|Any CPU + {1790E052-646F-4529-B90E-6FEA95520D69}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1790E052-646F-4529-B90E-6FEA95520D69}.Release|Any CPU.Build.0 = Release|Any CPU + {1790E052-646F-4529-B90E-6FEA95520D69}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {1790E052-646F-4529-B90E-6FEA95520D69}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {1790E052-646F-4529-B90E-6FEA95520D69}.Release|x86.ActiveCfg = Release|Any CPU + {1790E052-646F-4529-B90E-6FEA95520D69}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -263,5 +291,7 @@ Global {2755BFE5-7421-4A31-A644-F817DF5CAA98} = {4D2B6A51-2F9F-44F5-8131-EA5CAC053652} {7AF5AD96-EB6E-4D0E-8ABE-C0B543C0F4C2} = {7BF11F3A-60B6-4796-B504-579C67FFBA34} {6AB3E514-5894-4131-9399-DC7D5284ADDB} = {4D2B6A51-2F9F-44F5-8131-EA5CAC053652} + {86183DC3-02A8-4A68-8B60-71ECEC066E79} = {4D2B6A51-2F9F-44F5-8131-EA5CAC053652} + {1790E052-646F-4529-B90E-6FEA95520D69} = {7BF11F3A-60B6-4796-B504-579C67FFBA34} EndGlobalSection EndGlobal diff --git a/src/Microsoft.AspNet.CookiePolicy/AppendCookieContext.cs b/src/Microsoft.AspNet.CookiePolicy/AppendCookieContext.cs new file mode 100644 index 000000000..f9d816661 --- /dev/null +++ b/src/Microsoft.AspNet.CookiePolicy/AppendCookieContext.cs @@ -0,0 +1,23 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNet.Http; + +namespace Microsoft.AspNet.CookiePolicy +{ + public class AppendCookieContext + { + public AppendCookieContext(HttpContext context, CookieOptions options, string name, string value) + { + Context = context; + CookieOptions = options; + CookieName = name; + CookieValue = value; + } + + public HttpContext Context { get; } + public CookieOptions CookieOptions { get; } + public string CookieName { get; set; } + public string CookieValue { get; set; } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.CookiePolicy/CookiePolicyAppBuilderExtensions.cs b/src/Microsoft.AspNet.CookiePolicy/CookiePolicyAppBuilderExtensions.cs new file mode 100644 index 000000000..aa8c9af01 --- /dev/null +++ b/src/Microsoft.AspNet.CookiePolicy/CookiePolicyAppBuilderExtensions.cs @@ -0,0 +1,41 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNet.CookiePolicy; + +namespace Microsoft.AspNet.Builder +{ + /// + /// Extension methods provided by the cookie policy middleware + /// + public static class CookiePolicyAppBuilderExtensions + { + /// + /// Adds a cookie policy middleware to your web application pipeline. + /// + /// The IApplicationBuilder passed to your configuration method + /// The options for the middleware + /// The original app parameter + public static IApplicationBuilder UseCookiePolicy(this IApplicationBuilder app, CookiePolicyOptions options) + { + return app.UseMiddleware(options); + } + + /// + /// Adds a cookie policy middleware to your web application pipeline. + /// + /// The IApplicationBuilder passed to your configuration method + /// Used to configure the options for the middleware + /// The original app parameter + public static IApplicationBuilder UseCookiePolicy(this IApplicationBuilder app, Action configureOptions) + { + var options = new CookiePolicyOptions(); + if (configureOptions != null) + { + configureOptions(options); + } + return app.UseCookiePolicy(options); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.CookiePolicy/CookiePolicyMiddleware.cs b/src/Microsoft.AspNet.CookiePolicy/CookiePolicyMiddleware.cs new file mode 100644 index 000000000..d726d80cd --- /dev/null +++ b/src/Microsoft.AspNet.CookiePolicy/CookiePolicyMiddleware.cs @@ -0,0 +1,167 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Threading.Tasks; +using Microsoft.AspNet.Builder; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Http.Features; +using Microsoft.AspNet.Http.Features.Internal; + +namespace Microsoft.AspNet.CookiePolicy +{ + public class CookiePolicyMiddleware + { + private readonly RequestDelegate _next; + + public CookiePolicyMiddleware( + RequestDelegate next, + CookiePolicyOptions options) + { + Options = options; + _next = next; + } + + public CookiePolicyOptions Options { get; set; } + + public Task Invoke(HttpContext context) + { + var feature = context.Features.Get() ?? new ResponseCookiesFeature(context.Features); + context.Features.Set(new CookiesWrapperFeature(context, Options, feature)); + return _next(context); + } + + private class CookiesWrapperFeature : IResponseCookiesFeature + { + public CookiesWrapperFeature(HttpContext context, CookiePolicyOptions options, IResponseCookiesFeature feature) + { + Wrapper = new CookiesWrapper(context, options, feature); + } + + public IResponseCookies Wrapper { get; } + + public IResponseCookies Cookies + { + get + { + return Wrapper; + } + } + } + + private class CookiesWrapper : IResponseCookies + { + public CookiesWrapper(HttpContext context, CookiePolicyOptions options, IResponseCookiesFeature feature) + { + Context = context; + Feature = feature; + Policy = options; + } + + public HttpContext Context { get; } + + public IResponseCookiesFeature Feature { get; } + + public IResponseCookies Cookies + { + get + { + return Feature.Cookies; + } + } + + public CookiePolicyOptions Policy { get; } + + private bool PolicyRequiresCookieOptions() + { + return Policy.HttpOnly != HttpOnlyPolicy.None || Policy.Secure != SecurePolicy.None; + } + + public void Append(string key, string value) + { + if (PolicyRequiresCookieOptions() || Policy.OnAppendCookie != null) + { + Append(key, value, new CookieOptions()); + } + else + { + Cookies.Append(key, value); + } + } + + public void Append(string key, string value, CookieOptions options) + { + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + + ApplyPolicy(options); + if (Policy.OnAppendCookie != null) + { + var context = new AppendCookieContext(Context, options, key, value); + Policy.OnAppendCookie(context); + key = context.CookieName; + value = context.CookieValue; + } + Cookies.Append(key, value, options); + } + + public void Delete(string key) + { + if (PolicyRequiresCookieOptions() || Policy.OnDeleteCookie != null) + { + Delete(key, new CookieOptions()); + } + else + { + Cookies.Delete(key); + } + } + + public void Delete(string key, CookieOptions options) + { + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + + ApplyPolicy(options); + if (Policy.OnDeleteCookie != null) + { + var context = new DeleteCookieContext(Context, options, key); + Policy.OnDeleteCookie(context); + key = context.CookieName; + } + Cookies.Delete(key, options); + } + + private void ApplyPolicy(CookieOptions options) + { + switch (Policy.Secure) + { + case SecurePolicy.Always: + options.Secure = true; + break; + case SecurePolicy.SameAsRequest: + options.Secure = Context.Request.IsHttps; + break; + case SecurePolicy.None: + break; + default: + throw new InvalidOperationException(); + } + switch (Policy.HttpOnly) + { + case HttpOnlyPolicy.Always: + options.HttpOnly = true; + break; + case HttpOnlyPolicy.None: + break; + default: + throw new InvalidOperationException(); + } + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.CookiePolicy/CookiePolicyOptions.cs b/src/Microsoft.AspNet.CookiePolicy/CookiePolicyOptions.cs new file mode 100644 index 000000000..ce5a86698 --- /dev/null +++ b/src/Microsoft.AspNet.CookiePolicy/CookiePolicyOptions.cs @@ -0,0 +1,16 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; + +namespace Microsoft.AspNet.CookiePolicy +{ + public class CookiePolicyOptions + { + public HttpOnlyPolicy HttpOnly { get; set; } = HttpOnlyPolicy.None; + public SecurePolicy Secure { get; set; } = SecurePolicy.None; + + public Action OnAppendCookie { get; set; } + public Action OnDeleteCookie { get; set; } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.CookiePolicy/DeleteCookieContext.cs b/src/Microsoft.AspNet.CookiePolicy/DeleteCookieContext.cs new file mode 100644 index 000000000..c8cd208fb --- /dev/null +++ b/src/Microsoft.AspNet.CookiePolicy/DeleteCookieContext.cs @@ -0,0 +1,21 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNet.Http; + +namespace Microsoft.AspNet.CookiePolicy +{ + public class DeleteCookieContext + { + public DeleteCookieContext(HttpContext context, CookieOptions options, string name) + { + Context = context; + CookieOptions = options; + CookieName = name; + } + + public HttpContext Context { get; } + public CookieOptions CookieOptions { get; } + public string CookieName { get; set; } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.CookiePolicy/HttpOnlyPolicy.cs b/src/Microsoft.AspNet.CookiePolicy/HttpOnlyPolicy.cs new file mode 100644 index 000000000..276e3ed3e --- /dev/null +++ b/src/Microsoft.AspNet.CookiePolicy/HttpOnlyPolicy.cs @@ -0,0 +1,11 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNet.CookiePolicy +{ + public enum HttpOnlyPolicy + { + None, + Always + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.CookiePolicy/Microsoft.AspNet.CookiePolicy.xproj b/src/Microsoft.AspNet.CookiePolicy/Microsoft.AspNet.CookiePolicy.xproj new file mode 100644 index 000000000..7790eac27 --- /dev/null +++ b/src/Microsoft.AspNet.CookiePolicy/Microsoft.AspNet.CookiePolicy.xproj @@ -0,0 +1,19 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + 86183dc3-02a8-4a68-8b60-71ecec066e79 + Microsoft.AspNet.CookiePolicy + ..\..\artifacts\obj\$(MSBuildProjectName) + ..\..\artifacts\bin\$(MSBuildProjectName)\ + + + + 2.0 + + + \ No newline at end of file diff --git a/src/Microsoft.AspNet.CookiePolicy/Properties/AssemblyInfo.cs b/src/Microsoft.AspNet.CookiePolicy/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..b2437d9ad --- /dev/null +++ b/src/Microsoft.AspNet.CookiePolicy/Properties/AssemblyInfo.cs @@ -0,0 +1,8 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Reflection; +using System.Resources; + +[assembly: AssemblyMetadata("Serviceable", "True")] +[assembly: NeutralResourcesLanguage("en-us")] \ No newline at end of file diff --git a/src/Microsoft.AspNet.CookiePolicy/SecurePolicy.cs b/src/Microsoft.AspNet.CookiePolicy/SecurePolicy.cs new file mode 100644 index 000000000..962ecddff --- /dev/null +++ b/src/Microsoft.AspNet.CookiePolicy/SecurePolicy.cs @@ -0,0 +1,12 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNet.CookiePolicy +{ + public enum SecurePolicy + { + None, + Always, + SameAsRequest + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.CookiePolicy/project.json b/src/Microsoft.AspNet.CookiePolicy/project.json new file mode 100644 index 000000000..6d78e5eb8 --- /dev/null +++ b/src/Microsoft.AspNet.CookiePolicy/project.json @@ -0,0 +1,15 @@ +{ + "version": "1.0.0-*", + "description": "ASP.NET 5 cookie policy classes.", + "repository": { + "type": "git", + "url": "git://github.com/aspnet/security" + }, + "dependencies": { + "Microsoft.AspNet.Http": "1.0.0-*" + }, + "frameworks": { + "dnx451": { }, + "dnxcore50": { } + } +} diff --git a/test/Microsoft.AspNet.Authentication.Test/Cookies/CookieMiddlewareTests.cs b/test/Microsoft.AspNet.Authentication.Test/Cookies/CookieMiddlewareTests.cs index 4af812193..3fb293a59 100644 --- a/test/Microsoft.AspNet.Authentication.Test/Cookies/CookieMiddlewareTests.cs +++ b/test/Microsoft.AspNet.Authentication.Test/Cookies/CookieMiddlewareTests.cs @@ -731,7 +731,7 @@ public async Task ChallengeDoesNotSet401OnUnauthorized() transaction.Response.StatusCode.ShouldBe(HttpStatusCode.OK); } -/* [Fact] + [Fact] public async Task UseCookieWithInstanceDoesntUseSharedOptions() { var server = TestServer.Create(app => @@ -761,7 +761,7 @@ public async Task UseCookieWithOutInstanceDoesUseSharedOptions() transaction.Response.StatusCode.ShouldBe(HttpStatusCode.OK); Assert.True(transaction.SetCookie[0].StartsWith("One=")); - }*/ + } [Fact] public async Task MapWithSignInOnlyRedirectToReturnUrlOnLoginPath() diff --git a/test/Microsoft.AspNet.CookiePolicy.Test/CookiePolicyTests.cs b/test/Microsoft.AspNet.CookiePolicy.Test/CookiePolicyTests.cs new file mode 100644 index 000000000..78f20c9cf --- /dev/null +++ b/test/Microsoft.AspNet.CookiePolicy.Test/CookiePolicyTests.cs @@ -0,0 +1,268 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Threading.Tasks; +using Microsoft.AspNet.Builder; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Http.Features; +using Microsoft.AspNet.Http.Features.Internal; +using Microsoft.AspNet.TestHost; +using Xunit; + +namespace Microsoft.AspNet.CookiePolicy.Test +{ + public class CookiePolicyTests + { + private RequestDelegate SecureCookieAppends = context => + { + context.Response.Cookies.Append("A", "A"); + context.Response.Cookies.Append("B", "B", new CookieOptions { Secure = false }); + context.Response.Cookies.Append("C", "C", new CookieOptions()); + context.Response.Cookies.Append("D", "D", new CookieOptions { Secure = true }); + return Task.FromResult(0); + }; + private RequestDelegate HttpCookieAppends = context => + { + context.Response.Cookies.Append("A", "A"); + context.Response.Cookies.Append("B", "B", new CookieOptions { HttpOnly = false }); + context.Response.Cookies.Append("C", "C", new CookieOptions()); + context.Response.Cookies.Append("D", "D", new CookieOptions { HttpOnly = true }); + return Task.FromResult(0); + }; + + [Fact] + public async Task SecureAlwaysSetsSecure() + { + await RunTest("/secureAlways", + options => options.Secure = SecurePolicy.Always, + SecureCookieAppends, + new RequestTest("http://example.com/secureAlways", + transaction => + { + Assert.NotNull(transaction.SetCookie); + Assert.Equal("A=A; path=/; secure", transaction.SetCookie[0]); + Assert.Equal("B=B; path=/; secure", transaction.SetCookie[1]); + Assert.Equal("C=C; path=/; secure", transaction.SetCookie[2]); + Assert.Equal("D=D; path=/; secure", transaction.SetCookie[3]); + })); + } + + [Fact] + public async Task SecureNoneLeavesSecureUnchanged() + { + await RunTest("/secureNone", + options => options.Secure = SecurePolicy.None, + SecureCookieAppends, + new RequestTest("http://example.com/secureNone", + transaction => + { + Assert.NotNull(transaction.SetCookie); + Assert.NotNull(transaction.SetCookie); + Assert.Equal("A=A; path=/", transaction.SetCookie[0]); + Assert.Equal("B=B; path=/", transaction.SetCookie[1]); + Assert.Equal("C=C; path=/", transaction.SetCookie[2]); + Assert.Equal("D=D; path=/; secure", transaction.SetCookie[3]); + })); + } + + [Fact] + public async Task SecureSameUsesRequest() + { + await RunTest("/secureSame", + options => options.Secure = SecurePolicy.SameAsRequest, + SecureCookieAppends, + new RequestTest("http://example.com/secureSame", + transaction => + { + Assert.NotNull(transaction.SetCookie); + Assert.Equal("A=A; path=/", transaction.SetCookie[0]); + Assert.Equal("B=B; path=/", transaction.SetCookie[1]); + Assert.Equal("C=C; path=/", transaction.SetCookie[2]); + Assert.Equal("D=D; path=/", transaction.SetCookie[3]); + }), + new RequestTest("https://example.com/secureSame", + transaction => + { + Assert.NotNull(transaction.SetCookie); + Assert.Equal("A=A; path=/; secure", transaction.SetCookie[0]); + Assert.Equal("B=B; path=/; secure", transaction.SetCookie[1]); + Assert.Equal("C=C; path=/; secure", transaction.SetCookie[2]); + Assert.Equal("D=D; path=/; secure", transaction.SetCookie[3]); + })); + } + + [Fact] + public async Task HttpOnlyAlwaysSetsItAlways() + { + await RunTest("/httpOnlyAlways", + options => options.HttpOnly = HttpOnlyPolicy.Always, + HttpCookieAppends, + new RequestTest("http://example.com/httpOnlyAlways", + transaction => + { + Assert.NotNull(transaction.SetCookie); + Assert.Equal("A=A; path=/; httponly", transaction.SetCookie[0]); + Assert.Equal("B=B; path=/; httponly", transaction.SetCookie[1]); + Assert.Equal("C=C; path=/; httponly", transaction.SetCookie[2]); + Assert.Equal("D=D; path=/; httponly", transaction.SetCookie[3]); + })); + } + + [Fact] + public async Task HttpOnlyNoneLeavesItAlone() + { + await RunTest("/httpOnlyNone", + options => options.HttpOnly = HttpOnlyPolicy.None, + HttpCookieAppends, + new RequestTest("http://example.com/httpOnlyNone", + transaction => + { + Assert.NotNull(transaction.SetCookie); + Assert.Equal("A=A; path=/", transaction.SetCookie[0]); + Assert.Equal("B=B; path=/", transaction.SetCookie[1]); + Assert.Equal("C=C; path=/", transaction.SetCookie[2]); + Assert.Equal("D=D; path=/; httponly", transaction.SetCookie[3]); + })); + } + + [Fact] + public async Task CookiePolicyCanHijackAppend() + { + var server = TestServer.Create(app => + { + app.UseCookiePolicy(options => options.OnAppendCookie = ctx => ctx.CookieName = ctx.CookieValue = "Hao"); + app.Run(context => + { + context.Response.Cookies.Append("A", "A"); + context.Response.Cookies.Append("B", "B", new CookieOptions { Secure = false }); + context.Response.Cookies.Append("C", "C", new CookieOptions()); + context.Response.Cookies.Append("D", "D", new CookieOptions { Secure = true }); + return Task.FromResult(0); + }); + }); + + var transaction = await server.SendAsync("http://example.com/login"); + + Assert.NotNull(transaction.SetCookie); + Assert.Equal("Hao=Hao; path=/", transaction.SetCookie[0]); + Assert.Equal("Hao=Hao; path=/", transaction.SetCookie[1]); + Assert.Equal("Hao=Hao; path=/", transaction.SetCookie[2]); + Assert.Equal("Hao=Hao; path=/; secure", transaction.SetCookie[3]); + } + + [Fact] + public async Task CookiePolicyCanHijackDelete() + { + var server = TestServer.Create(app => + { + app.UseCookiePolicy(options => options.OnDeleteCookie = ctx => ctx.CookieName = "A"); + app.Run(context => + { + context.Response.Cookies.Delete("A"); + context.Response.Cookies.Delete("B", new CookieOptions { Secure = false }); + context.Response.Cookies.Delete("C", new CookieOptions()); + context.Response.Cookies.Delete("D", new CookieOptions { Secure = true }); + return Task.FromResult(0); + }); + }); + + var transaction = await server.SendAsync("http://example.com/login"); + + Assert.NotNull(transaction.SetCookie); + Assert.Equal(1, transaction.SetCookie.Count); + Assert.Equal("A=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/", transaction.SetCookie[0]); + } + + [Fact] + public async Task CookiePolicyCallsCookieFeature() + { + var server = TestServer.Create(app => + { + app.Use(next => context => + { + context.Features.Set(new TestCookieFeature()); + return next(context); + }); + app.UseCookiePolicy(options => options.OnDeleteCookie = ctx => ctx.CookieName = "A"); + app.Run(context => + { + Assert.Throws(() => context.Response.Cookies.Delete("A")); + Assert.Throws(() => context.Response.Cookies.Delete("A", new CookieOptions())); + Assert.Throws(() => context.Response.Cookies.Append("A", "A")); + Assert.Throws(() => context.Response.Cookies.Append("A", "A", new CookieOptions())); + return context.Response.WriteAsync("Done"); + }); + }); + + var transaction = await server.SendAsync("http://example.com/login"); + Assert.Equal("Done", transaction.ResponseText); + } + + private class TestCookieFeature : IResponseCookiesFeature + { + public IResponseCookies Cookies { get; } = new BadCookies(); + + private class BadCookies : IResponseCookies + { + public void Append(string key, string value) + { + throw new NotImplementedException(); + } + + public void Append(string key, string value, CookieOptions options) + { + throw new NotImplementedException(); + } + + public void Delete(string key) + { + throw new NotImplementedException(); + } + + public void Delete(string key, CookieOptions options) + { + throw new NotImplementedException(); + } + } + } + + private class RequestTest + { + public RequestTest(string testUri, Action verify) + { + TestUri = testUri; + Verification = verify; + } + + public async Task Execute(TestServer server) + { + var transaction = await server.SendAsync(TestUri); + Verification(transaction); + } + + public string TestUri { get; set; } + public Action Verification { get; set; } + } + + private async Task RunTest( + string path, + Action configureCookiePolicy, + RequestDelegate configureSetup, + params RequestTest[] tests) + { + var server = TestServer.Create(app => + { + app.Map(path, map => + { + map.UseCookiePolicy(configureCookiePolicy); + map.Run(configureSetup); + }); + }); + foreach (var test in tests) + { + await test.Execute(server); + } + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.CookiePolicy.Test/Microsoft.AspNet.CookiePolicy.Test.xproj b/test/Microsoft.AspNet.CookiePolicy.Test/Microsoft.AspNet.CookiePolicy.Test.xproj new file mode 100644 index 000000000..b0a49fddd --- /dev/null +++ b/test/Microsoft.AspNet.CookiePolicy.Test/Microsoft.AspNet.CookiePolicy.Test.xproj @@ -0,0 +1,21 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + 1790e052-646f-4529-b90e-6fea95520d69 + Microsoft.AspNet.CookiePolicy.Test + ..\..\artifacts\obj\$(MSBuildProjectName) + ..\..\artifacts\bin\$(MSBuildProjectName)\ + + + 2.0 + + + + + + \ No newline at end of file diff --git a/test/Microsoft.AspNet.CookiePolicy.Test/TestExtensions.cs b/test/Microsoft.AspNet.CookiePolicy.Test/TestExtensions.cs new file mode 100644 index 000000000..90b01af4b --- /dev/null +++ b/test/Microsoft.AspNet.CookiePolicy.Test/TestExtensions.cs @@ -0,0 +1,74 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Security.Claims; +using System.Text; +using System.Threading.Tasks; +using System.Xml; +using System.Xml.Linq; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.TestHost; + +namespace Microsoft.AspNet.CookiePolicy +{ + // REVIEW: Should find a shared home for these potentially (Copied from Auth tests) + public static class TestExtensions + { + public const string CookieAuthenticationScheme = "External"; + + public static async Task SendAsync(this TestServer server, string uri, string cookieHeader = null) + { + var request = new HttpRequestMessage(HttpMethod.Get, uri); + if (!string.IsNullOrEmpty(cookieHeader)) + { + request.Headers.Add("Cookie", cookieHeader); + } + var transaction = new Transaction + { + Request = request, + Response = await server.CreateClient().SendAsync(request), + }; + if (transaction.Response.Headers.Contains("Set-Cookie")) + { + transaction.SetCookie = transaction.Response.Headers.GetValues("Set-Cookie").ToList(); + } + transaction.ResponseText = await transaction.Response.Content.ReadAsStringAsync(); + + if (transaction.Response.Content != null && + transaction.Response.Content.Headers.ContentType != null && + transaction.Response.Content.Headers.ContentType.MediaType == "text/xml") + { + transaction.ResponseElement = XElement.Parse(transaction.ResponseText); + } + return transaction; + } + + public static void Describe(this HttpResponse res, ClaimsPrincipal principal) + { + res.StatusCode = 200; + res.ContentType = "text/xml"; + var xml = new XElement("xml"); + if (principal != null) + { + foreach (var identity in principal.Identities) + { + xml.Add(identity.Claims.Select(claim => + new XElement("claim", new XAttribute("type", claim.Type), + new XAttribute("value", claim.Value), + new XAttribute("issuer", claim.Issuer)))); + } + } + using (var memory = new MemoryStream()) + { + using (var writer = new XmlTextWriter(memory, Encoding.UTF8)) + { + xml.WriteTo(writer); + } + res.Body.Write(memory.ToArray(), 0, memory.ToArray().Length); + } + } + } +} diff --git a/test/Microsoft.AspNet.CookiePolicy.Test/Transaction.cs b/test/Microsoft.AspNet.CookiePolicy.Test/Transaction.cs new file mode 100644 index 000000000..afa9c0c99 --- /dev/null +++ b/test/Microsoft.AspNet.CookiePolicy.Test/Transaction.cs @@ -0,0 +1,51 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Xml.Linq; + +namespace Microsoft.AspNet.CookiePolicy +{ + // REVIEW: Should find a shared home for these potentially (Copied from Auth tests) + public class Transaction + { + public HttpRequestMessage Request { get; set; } + public HttpResponseMessage Response { get; set; } + + public IList SetCookie { get; set; } + + public string ResponseText { get; set; } + public XElement ResponseElement { get; set; } + + public string AuthenticationCookieValue + { + get + { + if (SetCookie != null && SetCookie.Count > 0) + { + var authCookie = SetCookie.SingleOrDefault(c => c.Contains(".AspNet." + TestExtensions.CookieAuthenticationScheme + "=")); + if (authCookie != null) + { + return authCookie.Substring(0, authCookie.IndexOf(';')); + } + } + + return null; + } + } + + public string FindClaimValue(string claimType, string issuer = null) + { + var claim = ResponseElement.Elements("claim") + .SingleOrDefault(elt => elt.Attribute("type").Value == claimType && + (issuer == null || elt.Attribute("issuer").Value == issuer)); + if (claim == null) + { + return null; + } + return claim.Attribute("value").Value; + } + } +} diff --git a/test/Microsoft.AspNet.CookiePolicy.Test/project.json b/test/Microsoft.AspNet.CookiePolicy.Test/project.json new file mode 100644 index 000000000..336c06a37 --- /dev/null +++ b/test/Microsoft.AspNet.CookiePolicy.Test/project.json @@ -0,0 +1,18 @@ +{ + "compilationOptions": { + "warningsAsErrors": true + }, + "dependencies": { + "Microsoft.AspNet.CookiePolicy": "1.0.0-*", + "Microsoft.AspNet.TestHost": "1.0.0-*", + "Microsoft.Framework.DependencyInjection": "1.0.0-*", + "xunit.runner.aspnet": "2.0.0-aspnet-*" + }, + "commands": { + "test": "xunit.runner.aspnet" + }, + "frameworks": { + "dnx451": { }, + "dnxcore50": { } + } +}