diff --git a/src/Microsoft.AspNet.Security.DataProtection/DataProtectionExtensions.cs b/src/Microsoft.AspNet.Security.DataProtection/DataProtectionExtensions.cs
index 1e84b49be4..5178eb7bc1 100644
--- a/src/Microsoft.AspNet.Security.DataProtection/DataProtectionExtensions.cs
+++ b/src/Microsoft.AspNet.Security.DataProtection/DataProtectionExtensions.cs
@@ -2,6 +2,8 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
+using System.Diagnostics;
+using Microsoft.AspNet.Cryptography;
namespace Microsoft.AspNet.Security.DataProtection
{
@@ -21,6 +23,37 @@ namespace Microsoft.AspNet.Security.DataProtection
?? new TimeLimitedDataProtector(protector.CreateProtector(TimeLimitedDataProtector.PurposeString));
}
+ ///
+ /// Creates an IDataProtector given an array of purposes.
+ ///
+ /// The provider from which to generate the purpose chain.
+ ///
+ /// This is a convenience method used for chaining several purposes together
+ /// in a single call to CreateProtector. See the documentation of
+ /// IDataProtectionProvider.CreateProtector for more information.
+ ///
+ /// An IDataProtector tied to the provided purpose chain.
+ public static IDataProtector CreateProtector([NotNull] this IDataProtectionProvider provider, params string[] purposes)
+ {
+ if (purposes == null || purposes.Length == 0)
+ {
+ throw new ArgumentException(Resources.DataProtectionExtensions_NullPurposesArray, nameof(purposes));
+ }
+
+ IDataProtectionProvider retVal = provider;
+ foreach (string purpose in purposes)
+ {
+ if (String.IsNullOrEmpty(purpose))
+ {
+ throw new ArgumentException(Resources.DataProtectionExtensions_NullPurposesArray, nameof(purposes));
+ }
+ retVal = retVal.CreateProtector(purpose) ?? CryptoUtil.Fail("CreateProtector returned null.");
+ }
+
+ Debug.Assert(retVal is IDataProtector); // CreateProtector is supposed to return an instance of this interface
+ return (IDataProtector)retVal;
+ }
+
///
/// Cryptographically protects a piece of plaintext data.
///
diff --git a/src/Microsoft.AspNet.Security.DataProtection/Properties/Resources.Designer.cs b/src/Microsoft.AspNet.Security.DataProtection/Properties/Resources.Designer.cs
index 35f9a8dc33..ae6746ee91 100644
--- a/src/Microsoft.AspNet.Security.DataProtection/Properties/Resources.Designer.cs
+++ b/src/Microsoft.AspNet.Security.DataProtection/Properties/Resources.Designer.cs
@@ -186,6 +186,22 @@ namespace Microsoft.AspNet.Security.DataProtection
return string.Format(CultureInfo.CurrentCulture, GetString("TimeLimitedDataProtector_PayloadExpired"), p0);
}
+ ///
+ /// The purposes array cannot be null or empty and cannot contain null or empty elements.
+ ///
+ internal static string DataProtectionExtensions_NullPurposesArray
+ {
+ get { return GetString("DataProtectionExtensions_NullPurposesArray"); }
+ }
+
+ ///
+ /// The purposes array cannot be null or empty and cannot contain null or empty elements.
+ ///
+ internal static string FormatDataProtectionExtensions_NullPurposesArray()
+ {
+ return GetString("DataProtectionExtensions_NullPurposesArray");
+ }
+
private static string GetString(string name, params string[] formatterNames)
{
var value = _resourceManager.GetString(name);
diff --git a/src/Microsoft.AspNet.Security.DataProtection/Resources.resx b/src/Microsoft.AspNet.Security.DataProtection/Resources.resx
index 044df24e82..3db16f062c 100644
--- a/src/Microsoft.AspNet.Security.DataProtection/Resources.resx
+++ b/src/Microsoft.AspNet.Security.DataProtection/Resources.resx
@@ -150,4 +150,7 @@
The payload expired at {0}.
+
+ The purposes array cannot be null or empty and cannot contain null or empty elements.
+
\ No newline at end of file
diff --git a/test/Microsoft.AspNet.Security.DataProtection.Test/DataProtectionExtensionsTests.cs b/test/Microsoft.AspNet.Security.DataProtection.Test/DataProtectionExtensionsTests.cs
index 6993f4ab9b..bccbafeb38 100644
--- a/test/Microsoft.AspNet.Security.DataProtection.Test/DataProtectionExtensionsTests.cs
+++ b/test/Microsoft.AspNet.Security.DataProtection.Test/DataProtectionExtensionsTests.cs
@@ -39,6 +39,43 @@ namespace Microsoft.AspNet.Security.DataProtection.Test
Assert.Same(innerProtector, timeLimitedProtector.InnerProtector);
}
+ [Theory]
+ [InlineData(new object[] { null })]
+ [InlineData(new object[] { new string[0] })]
+ [InlineData(new object[] { new string[] { null } })]
+ [InlineData(new object[] { new string[] { "the next value is bad", "" } })]
+ public void CreateProtector_Chained_FailureCases(string[] purposes)
+ {
+ // Arrange
+ var mockProtector = new Mock();
+ mockProtector.Setup(o => o.CreateProtector(It.IsAny())).Returns(mockProtector.Object);
+ var provider = mockProtector.Object;
+
+ // Act & assert
+ var ex = Assert.Throws(() => provider.CreateProtector(purposes));
+ ex.AssertMessage("purposes", Resources.DataProtectionExtensions_NullPurposesArray);
+ }
+
+ [Fact]
+ public void CreateProtector_Chained_SuccessCase()
+ {
+ // Arrange
+ var finalExpectedProtector = new Mock().Object;
+
+ var thirdMock = new Mock();
+ thirdMock.Setup(o => o.CreateProtector("third")).Returns(finalExpectedProtector);
+ var secondMock = new Mock();
+ secondMock.Setup(o => o.CreateProtector("second")).Returns(thirdMock.Object);
+ var firstMock = new Mock();
+ firstMock.Setup(o => o.CreateProtector("first")).Returns(secondMock.Object);
+
+ // Act
+ var retVal = firstMock.Object.CreateProtector("first", "second", "third");
+
+ // Assert
+ Assert.Same(finalExpectedProtector, retVal);
+ }
+
[Fact]
public void Protect_InvalidUtf_Failure()
{
diff --git a/test/Microsoft.AspNet.Security.DataProtection.Test/ExceptionHelpers.cs b/test/Microsoft.AspNet.Security.DataProtection.Test/ExceptionHelpers.cs
new file mode 100644
index 0000000000..e4394cbc9b
--- /dev/null
+++ b/test/Microsoft.AspNet.Security.DataProtection.Test/ExceptionHelpers.cs
@@ -0,0 +1,20 @@
+// 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 Xunit;
+
+namespace Microsoft.AspNet.Security.DataProtection.Test
+{
+ internal static class ExceptionHelpers
+ {
+ public static void AssertMessage(this ArgumentException exception, string parameterName, string message)
+ {
+ Assert.Equal(parameterName, exception.ParamName);
+
+ // We'll let ArgumentException handle the message formatting for us and treat it as our control value
+ var controlException = new ArgumentException(message, parameterName);
+ Assert.Equal(controlException.Message, exception.Message);
+ }
+ }
+}