From 8c1cb911f28dfc15c3552867218422f4cc669e24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Chalet?= Date: Fri, 18 Sep 2015 10:35:02 +0200 Subject: [PATCH] Refactor TicketSerializer/PropertiesSerializer and add ClaimsIdentity.Actor/Claim.Properties support --- .../Messages/RequestTokenSerializer.cs | 4 +- .../DataHandler/PropertiesSerializer.cs | 23 +- .../DataHandler/TicketSerializer.cs | 225 ++++++++++++------ .../DataHandler/TicketSerializerTests.cs | 83 ++++++- 4 files changed, 242 insertions(+), 93 deletions(-) diff --git a/src/Microsoft.AspNet.Authentication.Twitter/Messages/RequestTokenSerializer.cs b/src/Microsoft.AspNet.Authentication.Twitter/Messages/RequestTokenSerializer.cs index b16a430cc4..30ab884c80 100644 --- a/src/Microsoft.AspNet.Authentication.Twitter/Messages/RequestTokenSerializer.cs +++ b/src/Microsoft.AspNet.Authentication.Twitter/Messages/RequestTokenSerializer.cs @@ -62,7 +62,7 @@ namespace Microsoft.AspNet.Authentication.Twitter writer.Write(token.Token); writer.Write(token.TokenSecret); writer.Write(token.CallbackConfirmed); - PropertiesSerializer.Write(writer, token.Properties); + PropertiesSerializer.Default.Write(writer, token.Properties); } /// @@ -80,7 +80,7 @@ namespace Microsoft.AspNet.Authentication.Twitter string token = reader.ReadString(); string tokenSecret = reader.ReadString(); bool callbackConfirmed = reader.ReadBoolean(); - AuthenticationProperties properties = PropertiesSerializer.Read(reader); + AuthenticationProperties properties = PropertiesSerializer.Default.Read(reader); if (properties == null) { return null; diff --git a/src/Microsoft.AspNet.Authentication/DataHandler/PropertiesSerializer.cs b/src/Microsoft.AspNet.Authentication/DataHandler/PropertiesSerializer.cs index d3957b7221..462a5e3163 100644 --- a/src/Microsoft.AspNet.Authentication/DataHandler/PropertiesSerializer.cs +++ b/src/Microsoft.AspNet.Authentication/DataHandler/PropertiesSerializer.cs @@ -13,8 +13,10 @@ namespace Microsoft.AspNet.Authentication { private const int FormatVersion = 1; + public static PropertiesSerializer Default { get; } = new PropertiesSerializer(); + [SuppressMessage("Microsoft.Usage", "CA2202:Do not dispose objects multiple times", Justification = "Dispose is idempotent")] - public byte[] Serialize(AuthenticationProperties model) + public virtual byte[] Serialize(AuthenticationProperties model) { using (var memory = new MemoryStream()) { @@ -28,7 +30,7 @@ namespace Microsoft.AspNet.Authentication } [SuppressMessage("Microsoft.Usage", "CA2202:Do not dispose objects multiple times", Justification = "Dispose is idempotent")] - public AuthenticationProperties Deserialize(byte[] data) + public virtual AuthenticationProperties Deserialize(byte[] data) { using (var memory = new MemoryStream(data)) { @@ -39,26 +41,29 @@ namespace Microsoft.AspNet.Authentication } } - public static void Write([NotNull] BinaryWriter writer, [NotNull] AuthenticationProperties properties) + public virtual void Write([NotNull] BinaryWriter writer, [NotNull] AuthenticationProperties properties) { writer.Write(FormatVersion); writer.Write(properties.Items.Count); - foreach (var kv in properties.Items) + + foreach (var item in properties.Items) { - writer.Write(kv.Key); - writer.Write(kv.Value); + writer.Write(item.Key ?? string.Empty); + writer.Write(item.Value ?? string.Empty); } } - public static AuthenticationProperties Read([NotNull] BinaryReader reader) + public virtual AuthenticationProperties Read([NotNull] BinaryReader reader) { if (reader.ReadInt32() != FormatVersion) { return null; } - int count = reader.ReadInt32(); + + var count = reader.ReadInt32(); var extra = new Dictionary(count); - for (int index = 0; index != count; ++index) + + for (var index = 0; index != count; ++index) { string key = reader.ReadString(); string value = reader.ReadString(); diff --git a/src/Microsoft.AspNet.Authentication/DataHandler/TicketSerializer.cs b/src/Microsoft.AspNet.Authentication/DataHandler/TicketSerializer.cs index 44284d788f..1568f58ec2 100644 --- a/src/Microsoft.AspNet.Authentication/DataHandler/TicketSerializer.cs +++ b/src/Microsoft.AspNet.Authentication/DataHandler/TicketSerializer.cs @@ -11,15 +11,18 @@ namespace Microsoft.AspNet.Authentication { public class TicketSerializer : IDataSerializer { - private const int FormatVersion = 4; + private const string DefaultStringPlaceholder = "\0"; + private const int FormatVersion = 5; - public virtual byte[] Serialize(AuthenticationTicket model) + 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, model); + Write(writer, ticket); } return memory.ToArray(); } @@ -36,98 +39,177 @@ namespace Microsoft.AspNet.Authentication } } - public static void Write([NotNull] BinaryWriter writer, [NotNull] AuthenticationTicket model) + public virtual void Write([NotNull] BinaryWriter writer, [NotNull] AuthenticationTicket ticket) { writer.Write(FormatVersion); - writer.Write(model.AuthenticationScheme); - var principal = model.Principal; + writer.Write(ticket.AuthenticationScheme); + + var principal = ticket.Principal; if (principal == null) { throw new ArgumentNullException("model.Principal"); } - else - { - writer.Write(principal.Identities.Count()); - foreach (var identity in principal.Identities) - { - var authenticationType = string.IsNullOrEmpty(identity.AuthenticationType) ? string.Empty : identity.AuthenticationType; - writer.Write(authenticationType); - WriteWithDefault(writer, identity.NameClaimType, DefaultValues.NameClaimType); - WriteWithDefault(writer, identity.RoleClaimType, DefaultValues.RoleClaimType); - writer.Write(identity.Claims.Count()); - foreach (var claim in identity.Claims) - { - WriteWithDefault(writer, claim.Type, identity.NameClaimType); - writer.Write(claim.Value); - WriteWithDefault(writer, claim.ValueType, DefaultValues.StringValueType); - WriteWithDefault(writer, claim.Issuer, DefaultValues.LocalAuthority); - WriteWithDefault(writer, claim.OriginalIssuer, claim.Issuer); - } - var bootstrap = identity.BootstrapContext as string; - if (string.IsNullOrEmpty(bootstrap)) - { - writer.Write(0); - } - else - { - writer.Write(bootstrap.Length); - writer.Write(bootstrap); - } - } + // Write the number of identities contained in the principal. + writer.Write(principal.Identities.Count()); + + foreach (var identity in principal.Identities) + { + WriteIdentity(writer, identity); } - PropertiesSerializer.Write(writer, model.Properties); + + PropertiesSerializer.Default.Write(writer, ticket.Properties); } - public static AuthenticationTicket Read([NotNull] BinaryReader reader) + protected virtual void WriteIdentity([NotNull] BinaryWriter writer, [NotNull] 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([NotNull] BinaryWriter writer, [NotNull] 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([NotNull] BinaryReader reader) { if (reader.ReadInt32() != FormatVersion) { return null; } - var authenticationScheme = reader.ReadString(); - var identityCount = reader.ReadInt32(); - if (identityCount < 0) + var scheme = reader.ReadString(); + + // Read the number of identities stored + // in the serialized payload. + var count = reader.ReadInt32(); + if (count < 0) { return null; } - var identities = new ClaimsIdentity[identityCount]; - for (var i = 0; i != identityCount; ++i) + var identities = new ClaimsIdentity[count]; + for (var index = 0; index != count; ++index) { - var authenticationType = reader.ReadString(); - var nameClaimType = ReadWithDefault(reader, DefaultValues.NameClaimType); - var roleClaimType = ReadWithDefault(reader, DefaultValues.RoleClaimType); - var count = reader.ReadInt32(); - var claims = new Claim[count]; - for (int index = 0; index != count; ++index) - { - var type = ReadWithDefault(reader, nameClaimType); - var value = reader.ReadString(); - var valueType = ReadWithDefault(reader, DefaultValues.StringValueType); - var issuer = ReadWithDefault(reader, DefaultValues.LocalAuthority); - var originalIssuer = ReadWithDefault(reader, issuer); - claims[index] = new Claim(type, value, valueType, issuer, originalIssuer); - } - var identity = new ClaimsIdentity(claims, authenticationType, nameClaimType, roleClaimType); - var bootstrapContextSize = reader.ReadInt32(); - if (bootstrapContextSize > 0) - { - identity.BootstrapContext = reader.ReadString(); - } - identities[i] = identity; + identities[index] = ReadIdentity(reader); } - var properties = PropertiesSerializer.Read(reader); - return new AuthenticationTicket(new ClaimsPrincipal(identities), properties, authenticationScheme); + var properties = PropertiesSerializer.Default.Read(reader); + + return new AuthenticationTicket(new ClaimsPrincipal(identities), properties, scheme); + } + + protected virtual ClaimsIdentity ReadIdentity([NotNull] 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([NotNull] BinaryReader reader, [NotNull] 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(DefaultValues.DefaultStringPlaceholder); + writer.Write(DefaultStringPlaceholder); } else { @@ -138,20 +220,11 @@ namespace Microsoft.AspNet.Authentication private static string ReadWithDefault(BinaryReader reader, string defaultValue) { var value = reader.ReadString(); - if (string.Equals(value, DefaultValues.DefaultStringPlaceholder, StringComparison.Ordinal)) + if (string.Equals(value, DefaultStringPlaceholder, StringComparison.Ordinal)) { return defaultValue; } return value; } - - private static class DefaultValues - { - public const string DefaultStringPlaceholder = "\0"; - public const string NameClaimType = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name"; - public const string RoleClaimType = "http://schemas.microsoft.com/ws/2008/06/identity/claims/role"; - public const string LocalAuthority = "LOCAL AUTHORITY"; - public const string StringValueType = "http://www.w3.org/2001/XMLSchema#string"; - } } } diff --git a/test/Microsoft.AspNet.Authentication.Test/DataHandler/TicketSerializerTests.cs b/test/Microsoft.AspNet.Authentication.Test/DataHandler/TicketSerializerTests.cs index 23c7add5a9..28811a9eca 100644 --- a/test/Microsoft.AspNet.Authentication.Test/DataHandler/TicketSerializerTests.cs +++ b/test/Microsoft.AspNet.Authentication.Test/DataHandler/TicketSerializerTests.cs @@ -15,6 +15,7 @@ namespace Microsoft.AspNet.Authentication [Fact] public void NullPrincipalThrows() { + var serializer = new TicketSerializer(); var properties = new AuthenticationProperties(); properties.RedirectUri = "bye"; var ticket = new AuthenticationTicket(properties, "Hello"); @@ -23,13 +24,14 @@ namespace Microsoft.AspNet.Authentication using (var writer = new BinaryWriter(stream)) using (var reader = new BinaryReader(stream)) { - Assert.Throws(() => TicketSerializer.Write(writer, ticket)); + Assert.Throws(() => serializer.Write(writer, ticket)); } } [Fact] public void CanRoundTripEmptyPrincipal() { + var serializer = new TicketSerializer(); var properties = new AuthenticationProperties(); properties.RedirectUri = "bye"; var ticket = new AuthenticationTicket(new ClaimsPrincipal(), properties, "Hello"); @@ -38,9 +40,9 @@ namespace Microsoft.AspNet.Authentication using (var writer = new BinaryWriter(stream)) using (var reader = new BinaryReader(stream)) { - TicketSerializer.Write(writer, ticket); + serializer.Write(writer, ticket); stream.Position = 0; - var readTicket = TicketSerializer.Read(reader); + var readTicket = serializer.Read(reader); Assert.Equal(0, readTicket.Principal.Identities.Count()); Assert.Equal("bye", readTicket.Properties.RedirectUri); Assert.Equal("Hello", readTicket.AuthenticationScheme); @@ -50,8 +52,9 @@ namespace Microsoft.AspNet.Authentication [Fact] public void CanRoundTripBootstrapContext() { + var serializer = new TicketSerializer(); var properties = new AuthenticationProperties(); - properties.RedirectUri = "bye"; + var ticket = new AuthenticationTicket(new ClaimsPrincipal(), properties, "Hello"); ticket.Principal.AddIdentity(new ClaimsIdentity("misc") { BootstrapContext = "bootstrap" }); @@ -59,13 +62,81 @@ namespace Microsoft.AspNet.Authentication using (var writer = new BinaryWriter(stream)) using (var reader = new BinaryReader(stream)) { - TicketSerializer.Write(writer, ticket); + serializer.Write(writer, ticket); stream.Position = 0; - var readTicket = TicketSerializer.Read(reader); + var readTicket = serializer.Read(reader); Assert.Equal(1, readTicket.Principal.Identities.Count()); Assert.Equal("misc", readTicket.Principal.Identity.AuthenticationType); Assert.Equal("bootstrap", readTicket.Principal.Identities.First().BootstrapContext); } } + + [Fact] + public void CanRoundTripActorIdentity() + { + var serializer = new TicketSerializer(); + var properties = new AuthenticationProperties(); + + var actor = new ClaimsIdentity("actor"); + var ticket = new AuthenticationTicket(new ClaimsPrincipal(), properties, "Hello"); + ticket.Principal.AddIdentity(new ClaimsIdentity("misc") { Actor = actor }); + + using (var stream = new MemoryStream()) + using (var writer = new BinaryWriter(stream)) + using (var reader = new BinaryReader(stream)) + { + serializer.Write(writer, ticket); + stream.Position = 0; + var readTicket = serializer.Read(reader); + Assert.Equal(1, readTicket.Principal.Identities.Count()); + Assert.Equal("misc", readTicket.Principal.Identity.AuthenticationType); + + var identity = (ClaimsIdentity) readTicket.Principal.Identity; + Assert.NotNull(identity.Actor); + Assert.Equal(identity.Actor.AuthenticationType, "actor"); + } + } + + [Fact] + public void CanRoundTripClaimProperties() + { + var serializer = new TicketSerializer(); + var properties = new AuthenticationProperties(); + + var claim = new Claim("type", "value", "valueType", "issuer", "original-issuer"); + claim.Properties.Add("property-1", "property-value"); + + // Note: a null value MUST NOT result in a crash + // and MUST instead be treated like an empty string. + claim.Properties.Add("property-2", null); + + var ticket = new AuthenticationTicket(new ClaimsPrincipal(), properties, "Hello"); + ticket.Principal.AddIdentity(new ClaimsIdentity(new[] { claim }, "misc")); + + using (var stream = new MemoryStream()) + using (var writer = new BinaryWriter(stream)) + using (var reader = new BinaryReader(stream)) + { + serializer.Write(writer, ticket); + stream.Position = 0; + var readTicket = serializer.Read(reader); + Assert.Equal(1, readTicket.Principal.Identities.Count()); + Assert.Equal("misc", readTicket.Principal.Identity.AuthenticationType); + + var readClaim = readTicket.Principal.FindFirst("type"); + Assert.NotNull(claim); + Assert.Equal(claim.Type, "type"); + Assert.Equal(claim.Value, "value"); + Assert.Equal(claim.ValueType, "valueType"); + Assert.Equal(claim.Issuer, "issuer"); + Assert.Equal(claim.OriginalIssuer, "original-issuer"); + + var property1 = readClaim.Properties["property-1"]; + Assert.Equal(property1, "property-value"); + + var property2 = readClaim.Properties["property-2"]; + Assert.Equal(property2, string.Empty); + } + } } }