From e72a5639024eba48a021960f2bc141372c337afc Mon Sep 17 00:00:00 2001 From: Hao Kung Date: Wed, 21 Oct 2015 15:19:16 -0700 Subject: [PATCH] Add initial Owin Security cookie interop package --- Security.sln | 30 ++ .../CookieAuthenticationMiddleware.cs | 3 +- .../CookieAuthenticationOptions.cs | 6 + .../DataHandler/TicketSerializer.cs | 1 + .../AspNetTicketDataFormat.cs | 17 ++ .../AspNetTicketSerializer.cs | 220 ++++++++++++++ .../CookieAuthenticationExtensions.cs | 26 ++ .../DataProtectorShim.cs | 31 ++ .../DefaultCompatibilityConstants.cs | 22 ++ ...rosoft.Owin.Security.Cookies.Interop.xproj | 19 ++ .../project.json | 14 + .../Cookies/CookieMiddlewareTests.cs | 2 - .../project.json | 1 - ...t.Owin.Security.Cookies.Interop.Test.xproj | 19 ++ .../TicketInteropTests.cs | 269 ++++++++++++++++++ ...y-38ae71c9-485f-46f6-8b5d-a1da2230875f.xml | 16 ++ .../project.json | 18 ++ 17 files changed, 710 insertions(+), 4 deletions(-) create mode 100644 src/Microsoft.Owin.Security.Cookies.Interop/AspNetTicketDataFormat.cs create mode 100644 src/Microsoft.Owin.Security.Cookies.Interop/AspNetTicketSerializer.cs create mode 100644 src/Microsoft.Owin.Security.Cookies.Interop/CookieAuthenticationExtensions.cs create mode 100644 src/Microsoft.Owin.Security.Cookies.Interop/DataProtectorShim.cs create mode 100644 src/Microsoft.Owin.Security.Cookies.Interop/DefaultCompatibilityConstants.cs create mode 100644 src/Microsoft.Owin.Security.Cookies.Interop/Microsoft.Owin.Security.Cookies.Interop.xproj create mode 100644 src/Microsoft.Owin.Security.Cookies.Interop/project.json create mode 100644 test/Microsoft.Owin.Security.Cookies.Interop.Test/Microsoft.Owin.Security.Cookies.Interop.Test.xproj create mode 100644 test/Microsoft.Owin.Security.Cookies.Interop.Test/TicketInteropTests.cs create mode 100644 test/Microsoft.Owin.Security.Cookies.Interop.Test/key-38ae71c9-485f-46f6-8b5d-a1da2230875f.xml create mode 100644 test/Microsoft.Owin.Security.Cookies.Interop.Test/project.json diff --git a/Security.sln b/Security.sln index 486611009f..6367cfeadf 100644 --- a/Security.sln +++ b/Security.sln @@ -50,6 +50,10 @@ Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.CookiePoli EndProject Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Authentication.JwtBearer", "src\Microsoft.AspNet.Authentication.JwtBearer\Microsoft.AspNet.Authentication.JwtBearer.xproj", "{2755BFE5-7421-4A31-A644-F817DF5CAA98}" EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.Owin.Security.Cookies.Interop", "src\Microsoft.Owin.Security.Cookies.Interop\Microsoft.Owin.Security.Cookies.Interop.xproj", "{21A56E78-31DE-4868-9778-7E4DBE2A4E35}" +EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.Owin.Security.Cookies.Interop.Test", "test\Microsoft.Owin.Security.Cookies.Interop.Test\Microsoft.Owin.Security.Cookies.Interop.Test.xproj", "{73E8E654-A2AC-4848-95F3-EB55512F6C39}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -270,6 +274,30 @@ Global {2755BFE5-7421-4A31-A644-F817DF5CAA98}.Release|Mixed Platforms.Build.0 = Release|Any CPU {2755BFE5-7421-4A31-A644-F817DF5CAA98}.Release|x86.ActiveCfg = Release|Any CPU {2755BFE5-7421-4A31-A644-F817DF5CAA98}.Release|x86.Build.0 = Release|Any CPU + {21A56E78-31DE-4868-9778-7E4DBE2A4E35}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {21A56E78-31DE-4868-9778-7E4DBE2A4E35}.Debug|Any CPU.Build.0 = Debug|Any CPU + {21A56E78-31DE-4868-9778-7E4DBE2A4E35}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {21A56E78-31DE-4868-9778-7E4DBE2A4E35}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {21A56E78-31DE-4868-9778-7E4DBE2A4E35}.Debug|x86.ActiveCfg = Debug|Any CPU + {21A56E78-31DE-4868-9778-7E4DBE2A4E35}.Debug|x86.Build.0 = Debug|Any CPU + {21A56E78-31DE-4868-9778-7E4DBE2A4E35}.Release|Any CPU.ActiveCfg = Release|Any CPU + {21A56E78-31DE-4868-9778-7E4DBE2A4E35}.Release|Any CPU.Build.0 = Release|Any CPU + {21A56E78-31DE-4868-9778-7E4DBE2A4E35}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {21A56E78-31DE-4868-9778-7E4DBE2A4E35}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {21A56E78-31DE-4868-9778-7E4DBE2A4E35}.Release|x86.ActiveCfg = Release|Any CPU + {21A56E78-31DE-4868-9778-7E4DBE2A4E35}.Release|x86.Build.0 = Release|Any CPU + {73E8E654-A2AC-4848-95F3-EB55512F6C39}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {73E8E654-A2AC-4848-95F3-EB55512F6C39}.Debug|Any CPU.Build.0 = Debug|Any CPU + {73E8E654-A2AC-4848-95F3-EB55512F6C39}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {73E8E654-A2AC-4848-95F3-EB55512F6C39}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {73E8E654-A2AC-4848-95F3-EB55512F6C39}.Debug|x86.ActiveCfg = Debug|Any CPU + {73E8E654-A2AC-4848-95F3-EB55512F6C39}.Debug|x86.Build.0 = Debug|Any CPU + {73E8E654-A2AC-4848-95F3-EB55512F6C39}.Release|Any CPU.ActiveCfg = Release|Any CPU + {73E8E654-A2AC-4848-95F3-EB55512F6C39}.Release|Any CPU.Build.0 = Release|Any CPU + {73E8E654-A2AC-4848-95F3-EB55512F6C39}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {73E8E654-A2AC-4848-95F3-EB55512F6C39}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {73E8E654-A2AC-4848-95F3-EB55512F6C39}.Release|x86.ActiveCfg = Release|Any CPU + {73E8E654-A2AC-4848-95F3-EB55512F6C39}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -293,5 +321,7 @@ Global {86183DC3-02A8-4A68-8B60-71ECEC066E79} = {4D2B6A51-2F9F-44F5-8131-EA5CAC053652} {1790E052-646F-4529-B90E-6FEA95520D69} = {7BF11F3A-60B6-4796-B504-579C67FFBA34} {2755BFE5-7421-4A31-A644-F817DF5CAA98} = {4D2B6A51-2F9F-44F5-8131-EA5CAC053652} + {21A56E78-31DE-4868-9778-7E4DBE2A4E35} = {4D2B6A51-2F9F-44F5-8131-EA5CAC053652} + {73E8E654-A2AC-4848-95F3-EB55512F6C39} = {7BF11F3A-60B6-4796-B504-579C67FFBA34} EndGlobalSection EndGlobal diff --git a/src/Microsoft.AspNet.Authentication.Cookies/CookieAuthenticationMiddleware.cs b/src/Microsoft.AspNet.Authentication.Cookies/CookieAuthenticationMiddleware.cs index bce9648b87..aca234da08 100644 --- a/src/Microsoft.AspNet.Authentication.Cookies/CookieAuthenticationMiddleware.cs +++ b/src/Microsoft.AspNet.Authentication.Cookies/CookieAuthenticationMiddleware.cs @@ -54,7 +54,8 @@ namespace Microsoft.AspNet.Authentication.Cookies } if (Options.TicketDataFormat == null) { - var dataProtector = dataProtectionProvider.CreateProtector(typeof(CookieAuthenticationMiddleware).FullName, Options.AuthenticationScheme, "v2"); + var provider = Options.DataProtectionProvider ?? dataProtectionProvider; + var dataProtector = provider.CreateProtector(typeof(CookieAuthenticationMiddleware).FullName, Options.AuthenticationScheme, "v2"); Options.TicketDataFormat = new TicketDataFormat(dataProtector); } if (Options.CookieManager == null) diff --git a/src/Microsoft.AspNet.Authentication.Cookies/CookieAuthenticationOptions.cs b/src/Microsoft.AspNet.Authentication.Cookies/CookieAuthenticationOptions.cs index 8bb1d39598..1d0484ab64 100644 --- a/src/Microsoft.AspNet.Authentication.Cookies/CookieAuthenticationOptions.cs +++ b/src/Microsoft.AspNet.Authentication.Cookies/CookieAuthenticationOptions.cs @@ -4,6 +4,7 @@ using System; using System.ComponentModel; using System.Diagnostics.CodeAnalysis; +using Microsoft.AspNet.DataProtection; using Microsoft.AspNet.Http; using Microsoft.Extensions.OptionsModel; @@ -74,6 +75,11 @@ namespace Microsoft.AspNet.Authentication.Cookies /// public CookieSecureOption CookieSecure { get; set; } + /// + /// If set this will be used by the CookieAuthenticationMiddleware for data protection. + /// + public IDataProtectionProvider DataProtectionProvider { get; set; } + /// /// Controls how much time the cookie will remain valid from the point it is created. The expiration /// information is in the protected cookie ticket. Because of that an expired cookie will be ignored diff --git a/src/Microsoft.AspNet.Authentication/DataHandler/TicketSerializer.cs b/src/Microsoft.AspNet.Authentication/DataHandler/TicketSerializer.cs index b27027d11b..8fae5f9235 100644 --- a/src/Microsoft.AspNet.Authentication/DataHandler/TicketSerializer.cs +++ b/src/Microsoft.AspNet.Authentication/DataHandler/TicketSerializer.cs @@ -8,6 +8,7 @@ using System.Security.Claims; namespace Microsoft.AspNet.Authentication { + // This MUST be kept in sync with Microsoft.Owin.Security.Cookies.AspNetTicketSerializer public class TicketSerializer : IDataSerializer { private const string DefaultStringPlaceholder = "\0"; diff --git a/src/Microsoft.Owin.Security.Cookies.Interop/AspNetTicketDataFormat.cs b/src/Microsoft.Owin.Security.Cookies.Interop/AspNetTicketDataFormat.cs new file mode 100644 index 0000000000..48ded33091 --- /dev/null +++ b/src/Microsoft.Owin.Security.Cookies.Interop/AspNetTicketDataFormat.cs @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.Owin.Security.DataHandler; +using Microsoft.Owin.Security.DataHandler.Encoder; +using Microsoft.Owin.Security.DataProtection; + +namespace Microsoft.Owin.Security.Cookies.Interop +{ + public class AspNetTicketDataFormat : SecureDataFormat + { + public AspNetTicketDataFormat(IDataProtector protector) + : base(new AspNetTicketSerializer(), protector, TextEncodings.Base64Url) + { + } + } +} \ No newline at end of file diff --git a/src/Microsoft.Owin.Security.Cookies.Interop/AspNetTicketSerializer.cs b/src/Microsoft.Owin.Security.Cookies.Interop/AspNetTicketSerializer.cs new file mode 100644 index 0000000000..8727f7c46b --- /dev/null +++ b/src/Microsoft.Owin.Security.Cookies.Interop/AspNetTicketSerializer.cs @@ -0,0 +1,220 @@ +// Copyright (c) Microsoft Open Technologies, Inc. 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.IO; +using System.Linq; +using System.Security.Claims; +using Microsoft.Owin.Security.DataHandler.Serializer; + +namespace Microsoft.Owin.Security.Cookies.Interop +{ + // This MUST be kept in sync with Microsoft.AspNet.Authentication.DataHandler.TicketSerializer + public class AspNetTicketSerializer : IDataSerializer + { + private const string DefaultStringPlaceholder = "\0"; + private const int FormatVersion = 5; + + public static TicketSerializer Default { get; } = new TicketSerializer(); + + public virtual byte[] Serialize(AuthenticationTicket ticket) + { + using (var memory = new MemoryStream()) + { + using (var writer = new BinaryWriter(memory)) + { + Write(writer, ticket); + } + return memory.ToArray(); + } + } + + public virtual AuthenticationTicket Deserialize(byte[] data) + { + using (var memory = new MemoryStream(data)) + { + using (var reader = new BinaryReader(memory)) + { + return Read(reader); + } + } + } + + public virtual void Write(BinaryWriter writer, AuthenticationTicket ticket) + { + writer.Write(FormatVersion); + writer.Write(ticket.Identity.AuthenticationType); + + var identity = ticket.Identity; + if (identity == null) + { + throw new ArgumentNullException("ticket.Identity"); + } + + // There is always a single identity + writer.Write(1); + WriteIdentity(writer, identity); + PropertiesSerializer.Write(writer, ticket.Properties); + } + + protected virtual void WriteIdentity(BinaryWriter writer, ClaimsIdentity identity) + { + var authenticationType = identity.AuthenticationType ?? string.Empty; + + writer.Write(authenticationType); + WriteWithDefault(writer, identity.NameClaimType, ClaimsIdentity.DefaultNameClaimType); + WriteWithDefault(writer, identity.RoleClaimType, ClaimsIdentity.DefaultRoleClaimType); + + // Write the number of claims contained in the identity. + writer.Write(identity.Claims.Count()); + + foreach (var claim in identity.Claims) + { + WriteClaim(writer, claim); + } + + var bootstrap = identity.BootstrapContext as string; + if (!string.IsNullOrEmpty(bootstrap)) + { + writer.Write(true); + writer.Write(bootstrap); + } + else + { + writer.Write(false); + } + + if (identity.Actor != null) + { + writer.Write(true); + WriteIdentity(writer, identity.Actor); + } + else + { + writer.Write(false); + } + } + + protected virtual void WriteClaim(BinaryWriter writer, Claim claim) + { + WriteWithDefault(writer, claim.Type, claim.Subject?.NameClaimType ?? ClaimsIdentity.DefaultNameClaimType); + writer.Write(claim.Value); + WriteWithDefault(writer, claim.ValueType, ClaimValueTypes.String); + WriteWithDefault(writer, claim.Issuer, ClaimsIdentity.DefaultIssuer); + WriteWithDefault(writer, claim.OriginalIssuer, claim.Issuer); + + // Write the number of properties contained in the claim. + writer.Write(claim.Properties.Count); + + foreach (var property in claim.Properties) + { + writer.Write(property.Key ?? string.Empty); + writer.Write(property.Value ?? string.Empty); + } + } + + public virtual AuthenticationTicket Read(BinaryReader reader) + { + if (reader.ReadInt32() != FormatVersion) + { + return null; + } + + var scheme = reader.ReadString(); + + // Any identities after the first will be ignored. + var count = reader.ReadInt32(); + if (count < 0) + { + return null; + } + + var identity = ReadIdentity(reader); + var properties = PropertiesSerializer.Read(reader); + + return new AuthenticationTicket(identity, properties); + } + + protected virtual ClaimsIdentity ReadIdentity(BinaryReader reader) + { + var authenticationType = reader.ReadString(); + var nameClaimType = ReadWithDefault(reader, ClaimsIdentity.DefaultNameClaimType); + var roleClaimType = ReadWithDefault(reader, ClaimsIdentity.DefaultRoleClaimType); + + // Read the number of claims contained + // in the serialized identity. + var count = reader.ReadInt32(); + + var identity = new ClaimsIdentity(authenticationType, nameClaimType, roleClaimType); + + for (int index = 0; index != count; ++index) + { + var claim = ReadClaim(reader, identity); + + identity.AddClaim(claim); + } + + // Determine whether the identity + // has a bootstrap context attached. + if (reader.ReadBoolean()) + { + identity.BootstrapContext = reader.ReadString(); + } + + // Determine whether the identity + // has an actor identity attached. + if (reader.ReadBoolean()) + { + identity.Actor = ReadIdentity(reader); + } + + return identity; + } + + protected virtual Claim ReadClaim(BinaryReader reader, ClaimsIdentity identity) + { + var type = ReadWithDefault(reader, identity.NameClaimType); + var value = reader.ReadString(); + var valueType = ReadWithDefault(reader, ClaimValueTypes.String); + var issuer = ReadWithDefault(reader, ClaimsIdentity.DefaultIssuer); + var originalIssuer = ReadWithDefault(reader, issuer); + + var claim = new Claim(type, value, valueType, issuer, originalIssuer, identity); + + // Read the number of properties stored in the claim. + var count = reader.ReadInt32(); + + for (var index = 0; index != count; ++index) + { + var key = reader.ReadString(); + var propertyValue = reader.ReadString(); + + claim.Properties.Add(key, propertyValue); + } + + return claim; + } + + private static void WriteWithDefault(BinaryWriter writer, string value, string defaultValue) + { + if (string.Equals(value, defaultValue, StringComparison.Ordinal)) + { + writer.Write(DefaultStringPlaceholder); + } + else + { + writer.Write(value); + } + } + + private static string ReadWithDefault(BinaryReader reader, string defaultValue) + { + var value = reader.ReadString(); + if (string.Equals(value, DefaultStringPlaceholder, StringComparison.Ordinal)) + { + return defaultValue; + } + return value; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.Owin.Security.Cookies.Interop/CookieAuthenticationExtensions.cs b/src/Microsoft.Owin.Security.Cookies.Interop/CookieAuthenticationExtensions.cs new file mode 100644 index 0000000000..3bbe74aad6 --- /dev/null +++ b/src/Microsoft.Owin.Security.Cookies.Interop/CookieAuthenticationExtensions.cs @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNet.DataProtection; +using Microsoft.Owin.Security.Cookies; +using Microsoft.Owin.Security.Cookies.Interop; + +namespace Owin +{ + public static class CookieAuthenticationExtensions + { + public static IAppBuilder UseCookieAuthentication( + this IAppBuilder app, + CookieAuthenticationOptions options, + DataProtectionProvider dataProtectionProvider, + PipelineStage stage = PipelineStage.Authenticate) + { + var dataProtector = dataProtectionProvider.CreateProtector( + "Microsoft.AspNet.Authentication.Cookies.CookieAuthenticationMiddleware", // full name of the ASP.NET 5 type + options.AuthenticationType, "v2"); + options.TicketDataFormat = new AspNetTicketDataFormat(new DataProtectorShim(dataProtector)); + + return app.UseCookieAuthentication(options, stage); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.Owin.Security.Cookies.Interop/DataProtectorShim.cs b/src/Microsoft.Owin.Security.Cookies.Interop/DataProtectorShim.cs new file mode 100644 index 0000000000..0dab249aea --- /dev/null +++ b/src/Microsoft.Owin.Security.Cookies.Interop/DataProtectorShim.cs @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNet.DataProtection; + +namespace Microsoft.Owin.Security.Cookies.Interop +{ + /// + /// Converts an to an + /// . + /// + internal sealed class DataProtectorShim : Microsoft.Owin.Security.DataProtection.IDataProtector + { + private readonly IDataProtector _protector; + + public DataProtectorShim(IDataProtector protector) + { + _protector = protector; + } + + public byte[] Protect(byte[] userData) + { + return _protector.Protect(userData); + } + + public byte[] Unprotect(byte[] protectedData) + { + return _protector.Unprotect(protectedData); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.Owin.Security.Cookies.Interop/DefaultCompatibilityConstants.cs b/src/Microsoft.Owin.Security.Cookies.Interop/DefaultCompatibilityConstants.cs new file mode 100644 index 0000000000..b403033e32 --- /dev/null +++ b/src/Microsoft.Owin.Security.Cookies.Interop/DefaultCompatibilityConstants.cs @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNet.Identity +{ + /// + /// Helpful constants for working with the authentication cookie compatibility shim. + /// + public static class DefaultCompatibilityConstants + { + /// + /// The default authentication type for application authentication cookies. + /// + public const string ApplicationCookieAuthenticationType = "Microsoft.AspNet.Identity.Application.AuthType"; + + /// + /// The default cookie name for application authentication cookies. + /// Used by . + /// + public const string CookieName = ".AspNet.Microsoft.AspNet.Identity.Application"; + } +} \ No newline at end of file diff --git a/src/Microsoft.Owin.Security.Cookies.Interop/Microsoft.Owin.Security.Cookies.Interop.xproj b/src/Microsoft.Owin.Security.Cookies.Interop/Microsoft.Owin.Security.Cookies.Interop.xproj new file mode 100644 index 0000000000..fd666b061d --- /dev/null +++ b/src/Microsoft.Owin.Security.Cookies.Interop/Microsoft.Owin.Security.Cookies.Interop.xproj @@ -0,0 +1,19 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + 21a56e78-31de-4868-9778-7e4dbe2a4e35 + Microsoft.Owin.Security.Cookies.Shareable + ..\..\artifacts\obj\$(MSBuildProjectName) + ..\..\artifacts\bin\$(MSBuildProjectName)\ + + + + 2.0 + + + \ No newline at end of file diff --git a/src/Microsoft.Owin.Security.Cookies.Interop/project.json b/src/Microsoft.Owin.Security.Cookies.Interop/project.json new file mode 100644 index 0000000000..58830a72cc --- /dev/null +++ b/src/Microsoft.Owin.Security.Cookies.Interop/project.json @@ -0,0 +1,14 @@ +{ + "version": "1.0.0-*", + "description": "A compatibility layer for sharing authentication tickets between Microsoft.Owin.Security.Cookies and Microsoft.AspNet.Authentication.Cookies.", + "dependencies": { + }, + "frameworks": { + "net451": { + "dependencies": { + "Microsoft.AspNet.DataProtection.Extensions": "1.0.0-*", + "Microsoft.Owin.Security.Cookies": "3.0.1" + } + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Authentication.Test/Cookies/CookieMiddlewareTests.cs b/test/Microsoft.AspNet.Authentication.Test/Cookies/CookieMiddlewareTests.cs index ff1a67d532..3f40b1cebb 100644 --- a/test/Microsoft.AspNet.Authentication.Test/Cookies/CookieMiddlewareTests.cs +++ b/test/Microsoft.AspNet.Authentication.Test/Cookies/CookieMiddlewareTests.cs @@ -2,7 +2,6 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; -using System.IO; using System.Linq; using System.Net; using System.Net.Http; @@ -10,7 +9,6 @@ using System.Security.Claims; using System.Security.Principal; using System.Text; using System.Threading.Tasks; -using System.Xml; using System.Xml.Linq; using Microsoft.AspNet.Builder; using Microsoft.AspNet.DataProtection; diff --git a/test/Microsoft.AspNet.Authentication.Test/project.json b/test/Microsoft.AspNet.Authentication.Test/project.json index 9b0c6a2375..af0759d42b 100644 --- a/test/Microsoft.AspNet.Authentication.Test/project.json +++ b/test/Microsoft.AspNet.Authentication.Test/project.json @@ -10,7 +10,6 @@ "Microsoft.AspNet.Authentication.MicrosoftAccount": "1.0.0-*", "Microsoft.AspNet.Authentication.OpenIdConnect": "1.0.0-*", "Microsoft.AspNet.Authentication.Twitter": "1.0.0-*", - "Microsoft.AspNet.DataProtection": "1.0.0-*", "Microsoft.AspNet.TestHost": "1.0.0-*", "Microsoft.AspNet.Testing": "1.0.0-*", "xunit.runner.aspnet": "2.0.0-aspnet-*" diff --git a/test/Microsoft.Owin.Security.Cookies.Interop.Test/Microsoft.Owin.Security.Cookies.Interop.Test.xproj b/test/Microsoft.Owin.Security.Cookies.Interop.Test/Microsoft.Owin.Security.Cookies.Interop.Test.xproj new file mode 100644 index 0000000000..26bc63e140 --- /dev/null +++ b/test/Microsoft.Owin.Security.Cookies.Interop.Test/Microsoft.Owin.Security.Cookies.Interop.Test.xproj @@ -0,0 +1,19 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + 73e8e654-a2ac-4848-95f3-eb55512f6c39 + Microsoft.Owin.Security.Cookies.Interop.Test + ..\..\artifacts\obj\$(MSBuildProjectName) + ..\..\artifacts\bin\$(MSBuildProjectName)\ + + + + 2.0 + + + \ No newline at end of file diff --git a/test/Microsoft.Owin.Security.Cookies.Interop.Test/TicketInteropTests.cs b/test/Microsoft.Owin.Security.Cookies.Interop.Test/TicketInteropTests.cs new file mode 100644 index 0000000000..63ab2d36cd --- /dev/null +++ b/test/Microsoft.Owin.Security.Cookies.Interop.Test/TicketInteropTests.cs @@ -0,0 +1,269 @@ +// 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.IO; +using System.Linq; +using System.Net.Http; +using Microsoft.AspNet.Http; +using System.Security.Claims; +using System.Text; +using System.Threading.Tasks; +using System.Xml; +using System.Xml.Linq; +using Microsoft.AspNet.Authentication; +using Microsoft.AspNet.Builder; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Owin; +using Microsoft.Owin.Security.Cookies; +using Microsoft.Owin.Security.Cookies.Interop; +using Microsoft.Owin.Testing; +using Owin; +using Xunit; + +namespace Microsoft.AspNet.CookiePolicy.Test +{ + public class TicketInteropTests + { + [Fact] + public void NewSerializerCanReadInteropTicket() + { + var identity = new ClaimsIdentity("scheme"); + identity.AddClaim(new Claim("Test", "Value")); + + var expires = DateTime.Today; + var issued = new DateTime(1979, 11, 11); + var properties = new Owin.Security.AuthenticationProperties(); + properties.IsPersistent = true; + properties.RedirectUri = "/redirect"; + properties.Dictionary["key"] = "value"; + properties.ExpiresUtc = expires; + properties.IssuedUtc = issued; + + var interopTicket = new Owin.Security.AuthenticationTicket(identity, properties); + var interopSerializer = new AspNetTicketSerializer(); + + var bytes = interopSerializer.Serialize(interopTicket); + + var newSerializer = new TicketSerializer(); + var newTicket = newSerializer.Deserialize(bytes); + + Assert.NotNull(newTicket); + Assert.Equal(1, newTicket.Principal.Identities.Count()); + var newIdentity = newTicket.Principal.Identity as ClaimsIdentity; + Assert.NotNull(newIdentity); + Assert.Equal("scheme", newIdentity.AuthenticationType); + Assert.True(newIdentity.HasClaim(c => c.Type == "Test" && c.Value == "Value")); + Assert.NotNull(newTicket.Properties); + Assert.True(newTicket.Properties.IsPersistent); + Assert.Equal("/redirect", newTicket.Properties.RedirectUri); + Assert.Equal("value", newTicket.Properties.Items["key"]); + Assert.Equal(expires, newTicket.Properties.ExpiresUtc); + Assert.Equal(issued, newTicket.Properties.IssuedUtc); + } + + [Fact] + public void InteropSerializerCanReadNewTicket() + { + var user = new ClaimsPrincipal(); + var identity = new ClaimsIdentity("scheme"); + identity.AddClaim(new Claim("Test", "Value")); + user.AddIdentity(identity); + + var expires = DateTime.Today; + var issued = new DateTime(1979, 11, 11); + var properties = new Http.Authentication.AuthenticationProperties(); + properties.IsPersistent = true; + properties.RedirectUri = "/redirect"; + properties.Items["key"] = "value"; + properties.ExpiresUtc = expires; + properties.IssuedUtc = issued; + + var newTicket = new AuthenticationTicket(user, properties, "scheme"); + var newSerializer = new TicketSerializer(); + + var bytes = newSerializer.Serialize(newTicket); + + var interopSerializer = new AspNetTicketSerializer(); + var interopTicket = interopSerializer.Deserialize(bytes); + + Assert.NotNull(interopTicket); + var newIdentity = interopTicket.Identity; + Assert.NotNull(newIdentity); + Assert.Equal("scheme", newIdentity.AuthenticationType); + Assert.True(newIdentity.HasClaim(c => c.Type == "Test" && c.Value == "Value")); + Assert.NotNull(interopTicket.Properties); + Assert.True(interopTicket.Properties.IsPersistent); + Assert.Equal("/redirect", interopTicket.Properties.RedirectUri); + Assert.Equal("value", interopTicket.Properties.Dictionary["key"]); + Assert.Equal(expires, interopTicket.Properties.ExpiresUtc); + Assert.Equal(issued, interopTicket.Properties.IssuedUtc); + } + + [Fact] + public async Task AspNet5WithInteropCookieContainsIdentity() + { + var identity = new ClaimsIdentity("Cookies"); + identity.AddClaim(new Claim(ClaimTypes.Name, "Alice")); + + var dataProtection = new DataProtection.DataProtectionProvider(new DirectoryInfo(".")); + + var interopServer = TestServer.Create(app => + { + app.Properties["host.AppName"] = "Microsoft.Owin.Security.Tests"; + app.UseCookieAuthentication(new CookieAuthenticationOptions(), dataProtection); + app.Run(context => + { + context.Authentication.SignIn(identity); + return Task.FromResult(0); + }); + }); + + var transaction = await SendAsync(interopServer, "http://example.com"); + + var newServer = TestHost.TestServer.Create(app => + { + app.UseCookieAuthentication(options => options.DataProtectionProvider = dataProtection); + app.Run(async context => + { + var result = await context.Authentication.AuthenticateAsync("Cookies"); + await context.Response.WriteAsync(result.Identity.Name); + }); + }, services => services.AddAuthentication()); + + var request = new HttpRequestMessage(HttpMethod.Get, "http://example.com/login"); + request.Headers.Add("Cookie", transaction.SetCookie.Split(new[] { ';' }, 2).First()); + var response = await newServer.CreateClient().SendAsync(request); + + Assert.Equal("Alice", await response.Content.ReadAsStringAsync()); + } + + [Fact] + public async Task InteropWithNewCookieContainsIdentity() + { + var user = new ClaimsPrincipal(); + var identity = new ClaimsIdentity("scheme"); + identity.AddClaim(new Claim(ClaimTypes.Name, "Alice")); + user.AddIdentity(identity); + + var dataProtection = new DataProtection.DataProtectionProvider(new DirectoryInfo(".")); + + var newServer = TestHost.TestServer.Create(app => + { + app.UseCookieAuthentication(options => options.DataProtectionProvider = dataProtection); + app.Run(context => context.Authentication.SignInAsync("Cookies", user)); + }, services => services.AddAuthentication()); + + var cookie = await SendAndGetCookie(newServer, "http://example.com/login"); + + var server = TestServer.Create(app => + { + app.Properties["host.AppName"] = "Microsoft.Owin.Security.Tests"; + app.UseCookieAuthentication(new CookieAuthenticationOptions(), dataProtection); + app.Run(async context => + { + var result = await context.Authentication.AuthenticateAsync("Cookies"); + Describe(context.Response, result); + }); + }); + + var transaction2 = await SendAsync(server, "http://example.com/me/Cookies", cookie); + + Assert.Equal("Alice", FindClaimValue(transaction2, ClaimTypes.Name)); + } + + private static async Task SendAndGetCookie(TestHost.TestServer server, string uri) + { + var request = new HttpRequestMessage(HttpMethod.Get, uri); + var response = await server.CreateClient().SendAsync(request); + if (response.Headers.Contains("Set-Cookie")) + { + return response.Headers.GetValues("Set-Cookie").ToList().First(); + } + return null; + } + + private static string FindClaimValue(Transaction transaction, string claimType) + { + XElement claim = transaction.ResponseElement.Elements("claim").SingleOrDefault(elt => elt.Attribute("type").Value == claimType); + if (claim == null) + { + return null; + } + return claim.Attribute("value").Value; + } + + private static void Describe(IOwinResponse res, Owin.Security.AuthenticateResult result) + { + res.StatusCode = 200; + res.ContentType = "text/xml"; + var xml = new XElement("xml"); + if (result != null && result.Identity != null) + { + xml.Add(result.Identity.Claims.Select(claim => new XElement("claim", new XAttribute("type", claim.Type), new XAttribute("value", claim.Value)))); + } + if (result != null && result.Properties != null) + { + xml.Add(result.Properties.Dictionary.Select(extra => new XElement("extra", new XAttribute("type", extra.Key), new XAttribute("value", extra.Value)))); + } + 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); + } + } + + private static async Task SendAsync(TestServer server, string uri, string cookieHeader = null, bool ajaxRequest = false) + { + var request = new HttpRequestMessage(HttpMethod.Get, uri); + if (!string.IsNullOrEmpty(cookieHeader)) + { + request.Headers.Add("Cookie", cookieHeader); + } + if (ajaxRequest) + { + request.Headers.Add("X-Requested-With", "XMLHttpRequest"); + } + var transaction = new Transaction + { + Request = request, + Response = await server.HttpClient.SendAsync(request), + }; + if (transaction.Response.Headers.Contains("Set-Cookie")) + { + transaction.SetCookie = transaction.Response.Headers.GetValues("Set-Cookie").SingleOrDefault(); + } + if (!string.IsNullOrEmpty(transaction.SetCookie)) + { + transaction.CookieNameValue = transaction.SetCookie.Split(new[] { ';' }, 2).First(); + } + 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; + } + + private class Transaction + { + public HttpRequestMessage Request { get; set; } + public HttpResponseMessage Response { get; set; } + + public string SetCookie { get; set; } + public string CookieNameValue { get; set; } + + public string ResponseText { get; set; } + public XElement ResponseElement { get; set; } + } + + } +} + + diff --git a/test/Microsoft.Owin.Security.Cookies.Interop.Test/key-38ae71c9-485f-46f6-8b5d-a1da2230875f.xml b/test/Microsoft.Owin.Security.Cookies.Interop.Test/key-38ae71c9-485f-46f6-8b5d-a1da2230875f.xml new file mode 100644 index 0000000000..67d822bca5 --- /dev/null +++ b/test/Microsoft.Owin.Security.Cookies.Interop.Test/key-38ae71c9-485f-46f6-8b5d-a1da2230875f.xml @@ -0,0 +1,16 @@ + + + 2015-10-21T22:18:44.8335016Z + 2015-10-21T22:18:44.8335016Z + 2016-01-19T22:18:44.8335016Z + + + + + + + 7LN6JAKaUxuHQmzldfRpxCuHZtkEoG6Zrvc0LaNXgP0Ful2wYocEwlB7JRdkKAEcmY53W5wqVNSVfcgln+hNVA== + + + + \ No newline at end of file diff --git a/test/Microsoft.Owin.Security.Cookies.Interop.Test/project.json b/test/Microsoft.Owin.Security.Cookies.Interop.Test/project.json new file mode 100644 index 0000000000..c206ad20eb --- /dev/null +++ b/test/Microsoft.Owin.Security.Cookies.Interop.Test/project.json @@ -0,0 +1,18 @@ +{ + "compilationOptions": { + "warningsAsErrors": true + }, + "dependencies": { + "Microsoft.AspNet.Authentication.Cookies": "1.0.0-*", + "Microsoft.AspNet.TestHost": "1.0.0-*", + "Microsoft.Owin.Security.Cookies.Interop": "1.0.0-*", + "Microsoft.Owin.Testing": "3.0.1", + "xunit.runner.aspnet": "2.0.0-aspnet-*" + }, + "commands": { + "test": "xunit.runner.aspnet" + }, + "frameworks": { + "dnx451": { } + } +}