summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/DotNetOAuth.Test/DotNetOAuth.Test.csproj7
-rw-r--r--src/DotNetOAuth.Test/Messaging/ChannelTests.cs192
-rw-r--r--src/DotNetOAuth.Test/Messaging/MessagingTestBase.cs120
-rw-r--r--src/DotNetOAuth.Test/Messaging/StandardMessageExpirationBindingElementTests.cs38
-rw-r--r--src/DotNetOAuth.Test/Mocks/MockReplayProtectionBindingElement.cs45
-rw-r--r--src/DotNetOAuth.Test/Mocks/MockSigningBindingElement.cs41
-rw-r--r--src/DotNetOAuth.Test/Mocks/MockTransformationBindingElement.cs49
-rw-r--r--src/DotNetOAuth.Test/Mocks/TestChannel.cs4
-rw-r--r--src/DotNetOAuth.Test/Mocks/TestReplayProtectedChannel.cs35
-rw-r--r--src/DotNetOAuth.Test/Mocks/TestSigningChannel.cs29
-rw-r--r--src/DotNetOAuth.Test/TestBase.cs1
-rw-r--r--src/DotNetOAuth/DotNetOAuth.csproj3
-rw-r--r--src/DotNetOAuth/Messaging/Channel.cs219
-rw-r--r--src/DotNetOAuth/Messaging/ChannelProtection.cs41
-rw-r--r--src/DotNetOAuth/Messaging/ExpiredMessageException.cs2
-rw-r--r--src/DotNetOAuth/Messaging/IChannelBindingElement.cs36
-rw-r--r--src/DotNetOAuth/Messaging/InvalidSignatureException.cs2
-rw-r--r--src/DotNetOAuth/Messaging/MessagingStrings.Designer.cs18
-rw-r--r--src/DotNetOAuth/Messaging/MessagingStrings.resx8
-rw-r--r--src/DotNetOAuth/Messaging/MessagingUtilities.cs51
-rw-r--r--src/DotNetOAuth/Messaging/ReplayedMessageException.cs2
-rw-r--r--src/DotNetOAuth/Messaging/StandardMessageExpirationBindingElement.cs100
22 files changed, 699 insertions, 344 deletions
diff --git a/src/DotNetOAuth.Test/DotNetOAuth.Test.csproj b/src/DotNetOAuth.Test/DotNetOAuth.Test.csproj
index 85957c8..32e17aa 100644
--- a/src/DotNetOAuth.Test/DotNetOAuth.Test.csproj
+++ b/src/DotNetOAuth.Test/DotNetOAuth.Test.csproj
@@ -58,12 +58,15 @@
</Reference>
</ItemGroup>
<ItemGroup>
+ <Compile Include="Messaging\MessagingTestBase.cs" />
<Compile Include="Messaging\MessagingUtilitiesTests.cs" />
<Compile Include="Messaging\ChannelTests.cs" />
<Compile Include="Messaging\DictionaryXmlReaderTests.cs" />
<Compile Include="Messaging\HttpRequestInfoTests.cs" />
<Compile Include="Messaging\ProtocolExceptionTests.cs" />
- <Compile Include="Mocks\TestReplayProtectedChannel.cs" />
+ <Compile Include="Messaging\StandardMessageExpirationBindingElementTests.cs" />
+ <Compile Include="Mocks\MockTransformationBindingElement.cs" />
+ <Compile Include="Mocks\MockReplayProtectionBindingElement.cs" />
<Compile Include="Mocks\TestBaseMessage.cs" />
<Compile Include="Mocks\TestDerivedMessage.cs" />
<Compile Include="Mocks\TestReplayProtectedMessage.cs" />
@@ -71,7 +74,7 @@
<Compile Include="Mocks\TestBadChannel.cs" />
<Compile Include="Mocks\TestExpiringMessage.cs" />
<Compile Include="Mocks\TestSignedDirectedMessage.cs" />
- <Compile Include="Mocks\TestSigningChannel.cs" />
+ <Compile Include="Mocks\MockSigningBindingElement.cs" />
<Compile Include="Mocks\TestWebRequestHandler.cs" />
<Compile Include="OAuthChannelTests.cs" />
<Compile Include="Messaging\MessageSerializerTests.cs" />
diff --git a/src/DotNetOAuth.Test/Messaging/ChannelTests.cs b/src/DotNetOAuth.Test/Messaging/ChannelTests.cs
index ca80dba..6f58133 100644
--- a/src/DotNetOAuth.Test/Messaging/ChannelTests.cs
+++ b/src/DotNetOAuth.Test/Messaging/ChannelTests.cs
@@ -10,22 +10,12 @@ namespace DotNetOAuth.Test.Messaging {
using System.IO;
using System.Net;
using System.Web;
- using System.Xml;
using DotNetOAuth.Messaging;
using DotNetOAuth.Test.Mocks;
using Microsoft.VisualStudio.TestTools.UnitTesting;
[TestClass]
- public class ChannelTests : TestBase {
- private Channel channel;
-
- [TestInitialize]
- public override void SetUp() {
- base.SetUp();
-
- this.channel = new TestChannel();
- }
-
+ public class ChannelTests : MessagingTestBase {
[TestMethod, ExpectedException(typeof(ArgumentNullException))]
public void CtorNull() {
// This bad channel is deliberately constructed to pass null to
@@ -35,7 +25,7 @@ namespace DotNetOAuth.Test.Messaging {
[TestMethod]
public void DequeueIndirectOrResponseMessageReturnsNull() {
- Assert.IsNull(this.channel.DequeueIndirectOrResponseMessage());
+ Assert.IsNull(this.Channel.DequeueIndirectOrResponseMessage());
}
[TestMethod]
@@ -50,25 +40,25 @@ namespace DotNetOAuth.Test.Messaging {
[TestMethod, ExpectedException(typeof(ArgumentNullException))]
public void SendNull() {
- this.channel.Send(null);
+ this.Channel.Send(null);
}
[TestMethod, ExpectedException(typeof(ArgumentException))]
public void SendIndirectedUndirectedMessage() {
IProtocolMessage message = new TestMessage(MessageTransport.Indirect);
- this.channel.Send(message);
+ this.Channel.Send(message);
}
[TestMethod, ExpectedException(typeof(ArgumentException))]
public void SendDirectedNoRecipientMessage() {
IProtocolMessage message = new TestDirectedMessage(MessageTransport.Indirect);
- this.channel.Send(message);
+ this.Channel.Send(message);
}
[TestMethod, ExpectedException(typeof(ArgumentException))]
public void SendInvalidMessageTransport() {
IProtocolMessage message = new TestDirectedMessage((MessageTransport)100);
- this.channel.Send(message);
+ this.Channel.Send(message);
}
[TestMethod]
@@ -79,8 +69,8 @@ namespace DotNetOAuth.Test.Messaging {
Location = new Uri("http://host/path"),
Recipient = new Uri("http://provider/path"),
};
- this.channel.Send(message);
- Response response = this.channel.DequeueIndirectOrResponseMessage();
+ this.Channel.Send(message);
+ Response response = this.Channel.DequeueIndirectOrResponseMessage();
Assert.AreEqual(HttpStatusCode.Redirect, response.Status);
StringAssert.StartsWith(response.Headers[HttpResponseHeader.Location], "http://provider/path");
StringAssert.Contains(response.Headers[HttpResponseHeader.Location], "age=15");
@@ -120,8 +110,8 @@ namespace DotNetOAuth.Test.Messaging {
Location = new Uri("http://host/path"),
Recipient = new Uri("http://provider/path"),
};
- this.channel.Send(message);
- Response response = this.channel.DequeueIndirectOrResponseMessage();
+ this.Channel.Send(message);
+ Response response = this.Channel.DequeueIndirectOrResponseMessage();
Assert.AreEqual(HttpStatusCode.OK, response.Status, "A form redirect should be an HTTP successful response.");
Assert.IsNull(response.Headers[HttpResponseHeader.Location], "There should not be a redirection header in the response.");
string body = response.Body;
@@ -169,7 +159,7 @@ namespace DotNetOAuth.Test.Messaging {
Name = "Andrew",
Location = new Uri("http://host/path"),
};
- this.channel.Send(message);
+ this.Channel.Send(message);
}
[TestMethod, ExpectedException(typeof(ArgumentNullException))]
@@ -209,7 +199,7 @@ namespace DotNetOAuth.Test.Messaging {
// TODO: make this a request with a message in it.
HttpRequest request = new HttpRequest("somefile", "http://someurl", "age=15");
HttpContext.Current = new HttpContext(request, new HttpResponse(new StringWriter()));
- IProtocolMessage message = this.channel.ReadFromRequest();
+ IProtocolMessage message = this.Channel.ReadFromRequest();
Assert.IsNotNull(message);
Assert.IsInstanceOfType(message, typeof(TestMessage));
Assert.AreEqual(15, ((TestMessage)message).Age);
@@ -227,154 +217,72 @@ namespace DotNetOAuth.Test.Messaging {
badChannel.ReadFromRequest(null);
}
- [TestMethod, ExpectedException(typeof(NotSupportedException))]
- public void SendSigningMessagesNotSupported() {
- TestSignedDirectedMessage message = new TestSignedDirectedMessage(MessageTransport.Direct);
- message.Recipient = new Uri("http://localtest");
- this.channel.Send(message);
- }
-
- [TestMethod]
- public void SendSetsTimestamp() {
- TestExpiringMessage message = new TestExpiringMessage(MessageTransport.Indirect);
- message.Recipient = new Uri("http://localtest");
- ((IExpiringProtocolMessage)message).UtcCreationDate = DateTime.Parse("1/1/1990");
-
- Channel channel = new TestSigningChannel(true, false);
- channel.Send(message);
- Assert.IsTrue(DateTime.UtcNow - ((IExpiringProtocolMessage)message).UtcCreationDate < TimeSpan.FromSeconds(3), "The timestamp on the message was not set on send.");
- }
-
- [TestMethod, ExpectedException(typeof(NotSupportedException))]
- public void SendReplayProtectedMessageNotSupported() {
- TestReplayProtectedMessage message = new TestReplayProtectedMessage(MessageTransport.Indirect);
- message.Recipient = new Uri("http://localtest");
-
- Channel channel = new TestSigningChannel(true, false); // use this one to get passed signing NotSupportedException
- channel.Send(message);
- }
-
[TestMethod]
public void SendReplayProtectedMessageSetsNonce() {
TestReplayProtectedMessage message = new TestReplayProtectedMessage(MessageTransport.Indirect);
message.Recipient = new Uri("http://localtest");
- Channel channel = new TestReplayProtectedChannel();
- channel.Send(message);
+ this.Channel = CreateChannel(ChannelProtection.ReplayProtection, ChannelProtection.ReplayProtection);
+ this.Channel.Send(message);
Assert.IsNotNull(((IReplayProtectedProtocolMessage)message).Nonce);
}
- [TestMethod, ExpectedException(typeof(NotSupportedException))]
- public void ReceivedSignedMessagesNotSupported() {
- // Create a channel that doesn't support signed messages, but will recognize one.
- this.channel = new TestChannel(new TestMessageTypeProvider(true, false, false));
- this.ParameterizedReceiveTest("GET");
- }
-
[TestMethod, ExpectedException(typeof(InvalidSignatureException))]
public void ReceivedInvalidSignature() {
- this.channel = new TestSigningChannel(false, false);
+ this.Channel = CreateChannel(ChannelProtection.TamperProtection, ChannelProtection.TamperProtection);
this.ParameterizedReceiveProtectedTest(DateTime.UtcNow, true);
}
[TestMethod]
- public void VerifyGoodTimestampIsAccepted() {
- // Create a channel that supports and recognizes signed messages.
- this.channel = new TestSigningChannel(true, false);
- this.ParameterizedReceiveProtectedTest(DateTime.UtcNow, false);
- }
-
- [TestMethod, ExpectedException(typeof(ExpiredMessageException))]
- public void VerifyBadTimestampIsRejected() {
- // Create a channel that supports and recognizes signed messages.
- this.channel = new TestSigningChannel(true, false);
- this.ParameterizedReceiveProtectedTest(DateTime.UtcNow - this.channel.MaximumMessageAge - TimeSpan.FromSeconds(1), false);
- }
-
- [TestMethod]
public void ReceivedReplayProtectedMessageJustOnce() {
- this.channel = new TestReplayProtectedChannel();
+ this.Channel = CreateChannel(ChannelProtection.ReplayProtection, ChannelProtection.ReplayProtection);
this.ParameterizedReceiveProtectedTest(DateTime.UtcNow, false);
}
[TestMethod, ExpectedException(typeof(ReplayedMessageException))]
public void ReceivedReplayProtectedMessageTwice() {
- this.channel = new TestReplayProtectedChannel();
+ this.Channel = CreateChannel(ChannelProtection.ReplayProtection, ChannelProtection.ReplayProtection);
this.ParameterizedReceiveProtectedTest(DateTime.UtcNow, false);
this.ParameterizedReceiveProtectedTest(DateTime.UtcNow, false);
}
- [TestMethod, ExpectedException(typeof(NotSupportedException))]
- public void ReceivedReplayProtectedMessagesNotSupported() {
- // Create a channel that doesn't support replay protected messages, but will recognize one.
- this.channel = new TestSigningChannel(true, true);
- this.ParameterizedReceiveProtectedTest(DateTime.UtcNow, false);
+ [TestMethod, ExpectedException(typeof(ProtocolException))]
+ public void MessageExpirationWithoutTamperResistance() {
+ new TestChannel(
+ new TestMessageTypeProvider(),
+ new StandardMessageExpirationBindingElement());
}
- private static HttpRequestInfo CreateHttpRequestInfo(string method, IDictionary<string, string> fields) {
- string query = MessagingUtilities.CreateQueryString(fields);
- UriBuilder requestUri = new UriBuilder("http://localhost/path");
- WebHeaderCollection headers = new WebHeaderCollection();
- MemoryStream ms = new MemoryStream();
- if (method == "POST") {
- headers.Add(HttpRequestHeader.ContentType, "application/x-www-form-urlencoded");
- StreamWriter sw = new StreamWriter(ms);
- sw.Write(query);
- sw.Flush();
- ms.Position = 0;
- } else if (method == "GET") {
- requestUri.Query = query;
- } else {
- throw new ArgumentOutOfRangeException("method", method, "Expected POST or GET");
- }
- HttpRequestInfo request = new HttpRequestInfo {
- HttpMethod = method,
- Url = requestUri.Uri,
- Headers = headers,
- InputStream = ms,
- };
-
- return request;
+ [TestMethod, ExpectedException(typeof(ProtocolException))]
+ public void TooManyBindingElementsProvidingSameProtection() {
+ new TestChannel(
+ new TestMessageTypeProvider(),
+ new MockSigningBindingElement(),
+ new MockSigningBindingElement());
}
- private void ParameterizedReceiveTest(string method) {
- var fields = new Dictionary<string, string> {
- { "age", "15" },
- { "Name", "Andrew" },
- { "Location", "http://hostb/pathB" },
- };
- IProtocolMessage requestMessage = this.channel.ReadFromRequest(CreateHttpRequestInfo(method, fields));
- Assert.IsNotNull(requestMessage);
- Assert.IsInstanceOfType(requestMessage, typeof(TestMessage));
- TestMessage testMessage = (TestMessage)requestMessage;
- Assert.AreEqual(15, testMessage.Age);
- Assert.AreEqual("Andrew", testMessage.Name);
- Assert.AreEqual("http://hostb/pathB", testMessage.Location.AbsoluteUri);
- }
-
- private void ParameterizedReceiveProtectedTest(DateTime? utcCreatedDate, bool invalidSignature) {
- var fields = new Dictionary<string, string> {
- { "age", "15" },
- { "Name", "Andrew" },
- { "Location", "http://hostb/pathB" },
- { "Signature", invalidSignature ? "badsig" : TestSigningChannel.MessageSignature },
- { "Nonce", "someNonce" },
- };
- if (utcCreatedDate.HasValue) {
- utcCreatedDate = DateTime.Parse(utcCreatedDate.Value.ToUniversalTime().ToString()); // round off the milliseconds so comparisons work later
- fields.Add("created_on", XmlConvert.ToString(utcCreatedDate.Value, XmlDateTimeSerializationMode.Utc));
- }
- IProtocolMessage requestMessage = this.channel.ReadFromRequest(CreateHttpRequestInfo("GET", fields));
- Assert.IsNotNull(requestMessage);
- Assert.IsInstanceOfType(requestMessage, typeof(TestSignedDirectedMessage));
- TestSignedDirectedMessage testMessage = (TestSignedDirectedMessage)requestMessage;
- Assert.AreEqual(15, testMessage.Age);
- Assert.AreEqual("Andrew", testMessage.Name);
- Assert.AreEqual("http://hostb/pathB", testMessage.Location.AbsoluteUri);
- if (utcCreatedDate.HasValue) {
- IExpiringProtocolMessage expiringMessage = (IExpiringProtocolMessage)requestMessage;
- Assert.AreEqual(utcCreatedDate.Value, expiringMessage.UtcCreationDate);
- }
+ [TestMethod]
+ public void BindingElementsOrdering() {
+ IChannelBindingElement transformA = new MockTransformationBindingElement("a");
+ IChannelBindingElement transformB = new MockTransformationBindingElement("b");
+ IChannelBindingElement sign = new MockSigningBindingElement();
+ IChannelBindingElement replay = new MockReplayProtectionBindingElement();
+ IChannelBindingElement expire = new StandardMessageExpirationBindingElement();
+
+ Channel channel = new TestChannel(
+ new TestMessageTypeProvider(),
+ sign,
+ replay,
+ expire,
+ transformB,
+ transformA);
+
+ Assert.AreEqual(5, channel.BindingElements.Count);
+ Assert.AreSame(transformB, channel.BindingElements[0]);
+ Assert.AreSame(transformA, channel.BindingElements[1]);
+ Assert.AreSame(replay, channel.BindingElements[2]);
+ Assert.AreSame(expire, channel.BindingElements[3]);
+ Assert.AreSame(sign, channel.BindingElements[4]);
}
}
}
diff --git a/src/DotNetOAuth.Test/Messaging/MessagingTestBase.cs b/src/DotNetOAuth.Test/Messaging/MessagingTestBase.cs
new file mode 100644
index 0000000..61be6a7
--- /dev/null
+++ b/src/DotNetOAuth.Test/Messaging/MessagingTestBase.cs
@@ -0,0 +1,120 @@
+//-----------------------------------------------------------------------
+// <copyright file="MessagingTestBase.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOAuth.Test {
+ using System;
+ using System.Collections.Generic;
+ using System.IO;
+ using System.Net;
+ using System.Xml;
+ using DotNetOAuth.Messaging;
+ using DotNetOAuth.Test.Mocks;
+ using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+ /// <summary>
+ /// The base class that all messaging test classes inherit from.
+ /// </summary>
+ public class MessagingTestBase : TestBase {
+ internal Channel Channel { get; set; }
+
+ [TestInitialize]
+ public override void SetUp() {
+ base.SetUp();
+
+ this.Channel = new TestChannel();
+ }
+
+ internal static HttpRequestInfo CreateHttpRequestInfo(string method, IDictionary<string, string> fields) {
+ string query = MessagingUtilities.CreateQueryString(fields);
+ UriBuilder requestUri = new UriBuilder("http://localhost/path");
+ WebHeaderCollection headers = new WebHeaderCollection();
+ MemoryStream ms = new MemoryStream();
+ if (method == "POST") {
+ headers.Add(HttpRequestHeader.ContentType, "application/x-www-form-urlencoded");
+ StreamWriter sw = new StreamWriter(ms);
+ sw.Write(query);
+ sw.Flush();
+ ms.Position = 0;
+ } else if (method == "GET") {
+ requestUri.Query = query;
+ } else {
+ throw new ArgumentOutOfRangeException("method", method, "Expected POST or GET");
+ }
+ HttpRequestInfo request = new HttpRequestInfo {
+ HttpMethod = method,
+ Url = requestUri.Uri,
+ Headers = headers,
+ InputStream = ms,
+ };
+
+ return request;
+ }
+
+ internal static Channel CreateChannel(ChannelProtection capabilityAndRecognition) {
+ return CreateChannel(capabilityAndRecognition, capabilityAndRecognition);
+ }
+
+ internal static Channel CreateChannel(ChannelProtection capability, ChannelProtection recognition) {
+ bool signing = false, expiration = false, replay = false;
+ var bindingElements = new List<IChannelBindingElement>();
+ if (capability >= ChannelProtection.TamperProtection) {
+ bindingElements.Add(new MockSigningBindingElement());
+ signing = true;
+ }
+ if (capability >= ChannelProtection.Expiration) {
+ bindingElements.Add(new StandardMessageExpirationBindingElement());
+ expiration = true;
+ }
+ if (capability >= ChannelProtection.ReplayProtection) {
+ bindingElements.Add(new MockReplayProtectionBindingElement());
+ replay = true;
+ }
+
+ var typeProvider = new TestMessageTypeProvider(signing, expiration, replay);
+ return new TestChannel(typeProvider, bindingElements.ToArray());
+ }
+
+ internal void ParameterizedReceiveTest(string method) {
+ var fields = new Dictionary<string, string> {
+ { "age", "15" },
+ { "Name", "Andrew" },
+ { "Location", "http://hostb/pathB" },
+ };
+ IProtocolMessage requestMessage = this.Channel.ReadFromRequest(CreateHttpRequestInfo(method, fields));
+ Assert.IsNotNull(requestMessage);
+ Assert.IsInstanceOfType(requestMessage, typeof(TestMessage));
+ TestMessage testMessage = (TestMessage)requestMessage;
+ Assert.AreEqual(15, testMessage.Age);
+ Assert.AreEqual("Andrew", testMessage.Name);
+ Assert.AreEqual("http://hostb/pathB", testMessage.Location.AbsoluteUri);
+ }
+
+ internal void ParameterizedReceiveProtectedTest(DateTime? utcCreatedDate, bool invalidSignature) {
+ var fields = new Dictionary<string, string> {
+ { "age", "15" },
+ { "Name", "Andrew" },
+ { "Location", "http://hostb/pathB" },
+ { "Signature", invalidSignature ? "badsig" : MockSigningBindingElement.MessageSignature },
+ { "Nonce", "someNonce" },
+ };
+ if (utcCreatedDate.HasValue) {
+ utcCreatedDate = DateTime.Parse(utcCreatedDate.Value.ToUniversalTime().ToString()); // round off the milliseconds so comparisons work later
+ fields.Add("created_on", XmlConvert.ToString(utcCreatedDate.Value, XmlDateTimeSerializationMode.Utc));
+ }
+ IProtocolMessage requestMessage = this.Channel.ReadFromRequest(CreateHttpRequestInfo("GET", fields));
+ Assert.IsNotNull(requestMessage);
+ Assert.IsInstanceOfType(requestMessage, typeof(TestSignedDirectedMessage));
+ TestSignedDirectedMessage testMessage = (TestSignedDirectedMessage)requestMessage;
+ Assert.AreEqual(15, testMessage.Age);
+ Assert.AreEqual("Andrew", testMessage.Name);
+ Assert.AreEqual("http://hostb/pathB", testMessage.Location.AbsoluteUri);
+ if (utcCreatedDate.HasValue) {
+ IExpiringProtocolMessage expiringMessage = (IExpiringProtocolMessage)requestMessage;
+ Assert.AreEqual(utcCreatedDate.Value, expiringMessage.UtcCreationDate);
+ }
+ }
+ }
+}
diff --git a/src/DotNetOAuth.Test/Messaging/StandardMessageExpirationBindingElementTests.cs b/src/DotNetOAuth.Test/Messaging/StandardMessageExpirationBindingElementTests.cs
new file mode 100644
index 0000000..5a664a4
--- /dev/null
+++ b/src/DotNetOAuth.Test/Messaging/StandardMessageExpirationBindingElementTests.cs
@@ -0,0 +1,38 @@
+//-----------------------------------------------------------------------
+// <copyright file="StandardMessageExpirationBindingElementTests.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOAuth.Test.Messaging {
+ using System;
+ using DotNetOAuth.Messaging;
+ using DotNetOAuth.Test.Mocks;
+ using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+ [TestClass]
+ public class StandardMessageExpirationBindingElementTests : MessagingTestBase {
+ [TestMethod]
+ public void SendSetsTimestamp() {
+ TestExpiringMessage message = new TestExpiringMessage(MessageTransport.Indirect);
+ message.Recipient = new Uri("http://localtest");
+ ((IExpiringProtocolMessage)message).UtcCreationDate = DateTime.Parse("1/1/1990");
+
+ Channel channel = CreateChannel(ChannelProtection.Expiration, ChannelProtection.Expiration);
+ channel.Send(message);
+ Assert.IsTrue(DateTime.UtcNow - ((IExpiringProtocolMessage)message).UtcCreationDate < TimeSpan.FromSeconds(3), "The timestamp on the message was not set on send.");
+ }
+
+ [TestMethod]
+ public void VerifyGoodTimestampIsAccepted() {
+ this.Channel = CreateChannel(ChannelProtection.Expiration, ChannelProtection.Expiration);
+ this.ParameterizedReceiveProtectedTest(DateTime.UtcNow, false);
+ }
+
+ [TestMethod, ExpectedException(typeof(ExpiredMessageException))]
+ public void VerifyBadTimestampIsRejected() {
+ this.Channel = CreateChannel(ChannelProtection.Expiration, ChannelProtection.Expiration);
+ this.ParameterizedReceiveProtectedTest(DateTime.UtcNow - StandardMessageExpirationBindingElement.DefaultMaximumMessageAge - TimeSpan.FromSeconds(1), false);
+ }
+ }
+}
diff --git a/src/DotNetOAuth.Test/Mocks/MockReplayProtectionBindingElement.cs b/src/DotNetOAuth.Test/Mocks/MockReplayProtectionBindingElement.cs
new file mode 100644
index 0000000..ff1d709
--- /dev/null
+++ b/src/DotNetOAuth.Test/Mocks/MockReplayProtectionBindingElement.cs
@@ -0,0 +1,45 @@
+//-----------------------------------------------------------------------
+// <copyright file="MockReplayProtectionBindingElement.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOAuth.Test.Mocks {
+ using System;
+ using System.Collections.Generic;
+ using System.Linq;
+ using System.Text;
+ using DotNetOAuth.Messaging;
+ using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+ internal class MockReplayProtectionBindingElement : IChannelBindingElement {
+ private bool messageReceived;
+
+ #region IChannelBindingElement Members
+
+ ChannelProtection IChannelBindingElement.Protection {
+ get { return ChannelProtection.ReplayProtection; }
+ }
+
+ void IChannelBindingElement.PrepareMessageForSending(IProtocolMessage message) {
+ var replayMessage = message as IReplayProtectedProtocolMessage;
+ if (replayMessage != null) {
+ replayMessage.Nonce = "someNonce";
+ }
+ }
+
+ void IChannelBindingElement.PrepareMessageForReceiving(IProtocolMessage message) {
+ var replayMessage = message as IReplayProtectedProtocolMessage;
+ if (replayMessage != null) {
+ Assert.AreEqual("someNonce", replayMessage.Nonce, "The nonce didn't serialize correctly, or something");
+ // this mock implementation passes the first time and fails subsequent times.
+ if (this.messageReceived) {
+ throw new ReplayedMessageException(message);
+ }
+ this.messageReceived = true;
+ }
+ }
+
+ #endregion
+ }
+}
diff --git a/src/DotNetOAuth.Test/Mocks/MockSigningBindingElement.cs b/src/DotNetOAuth.Test/Mocks/MockSigningBindingElement.cs
new file mode 100644
index 0000000..5cf8be6
--- /dev/null
+++ b/src/DotNetOAuth.Test/Mocks/MockSigningBindingElement.cs
@@ -0,0 +1,41 @@
+//-----------------------------------------------------------------------
+// <copyright file="MockSigningBindingElement.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOAuth.Test.Mocks {
+ using System;
+ using System.Collections.Generic;
+ using System.Linq;
+ using System.Text;
+ using DotNetOAuth.Messaging;
+
+ internal class MockSigningBindingElement : IChannelBindingElement {
+ internal const string MessageSignature = "mocksignature";
+
+ #region IChannelBindingElement Members
+
+ ChannelProtection IChannelBindingElement.Protection {
+ get { return ChannelProtection.TamperProtection; }
+ }
+
+ void IChannelBindingElement.PrepareMessageForSending(IProtocolMessage message) {
+ ISignedProtocolMessage signedMessage = message as ISignedProtocolMessage;
+ if (signedMessage != null) {
+ signedMessage.Signature = MessageSignature;
+ }
+ }
+
+ void IChannelBindingElement.PrepareMessageForReceiving(IProtocolMessage message) {
+ ISignedProtocolMessage signedMessage = message as ISignedProtocolMessage;
+ if (signedMessage != null) {
+ if (signedMessage.Signature != MessageSignature) {
+ throw new InvalidSignatureException(message);
+ }
+ }
+ }
+
+ #endregion
+ }
+}
diff --git a/src/DotNetOAuth.Test/Mocks/MockTransformationBindingElement.cs b/src/DotNetOAuth.Test/Mocks/MockTransformationBindingElement.cs
new file mode 100644
index 0000000..7a1320b
--- /dev/null
+++ b/src/DotNetOAuth.Test/Mocks/MockTransformationBindingElement.cs
@@ -0,0 +1,49 @@
+//-----------------------------------------------------------------------
+// <copyright file="MockTransformationBindingElement.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOAuth.Test.Mocks {
+ using System;
+ using System.Collections.Generic;
+ using System.Linq;
+ using System.Text;
+ using DotNetOAuth.Messaging;
+ using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+ internal class MockTransformationBindingElement : IChannelBindingElement {
+ private string transform;
+
+ internal MockTransformationBindingElement(string transform) {
+ if (transform == null) {
+ throw new ArgumentNullException("transform");
+ }
+
+ this.transform = transform;
+ }
+
+ #region IChannelBindingElement Members
+
+ ChannelProtection IChannelBindingElement.Protection {
+ get { return ChannelProtection.None; }
+ }
+
+ void IChannelBindingElement.PrepareMessageForSending(IProtocolMessage message) {
+ var testMessage = message as TestMessage;
+ if (testMessage != null) {
+ testMessage.Name = this.transform + testMessage.Name;
+ }
+ }
+
+ void IChannelBindingElement.PrepareMessageForReceiving(IProtocolMessage message) {
+ var testMessage = message as TestMessage;
+ if (testMessage != null) {
+ StringAssert.StartsWith(testMessage.Name, this.transform);
+ testMessage.Name = testMessage.Name.Substring(this.transform.Length);
+ }
+ }
+
+ #endregion
+ }
+}
diff --git a/src/DotNetOAuth.Test/Mocks/TestChannel.cs b/src/DotNetOAuth.Test/Mocks/TestChannel.cs
index 2431af1..b69b756 100644
--- a/src/DotNetOAuth.Test/Mocks/TestChannel.cs
+++ b/src/DotNetOAuth.Test/Mocks/TestChannel.cs
@@ -16,8 +16,8 @@ namespace DotNetOAuth.Test.Mocks {
: this(new TestMessageTypeProvider()) {
}
- internal TestChannel(IMessageTypeProvider messageTypeProvider)
- : base(messageTypeProvider) {
+ internal TestChannel(IMessageTypeProvider messageTypeProvider, params IChannelBindingElement[] bindingElements)
+ : base(messageTypeProvider, bindingElements) {
}
protected override IProtocolMessage RequestInternal(IDirectedProtocolMessage request) {
diff --git a/src/DotNetOAuth.Test/Mocks/TestReplayProtectedChannel.cs b/src/DotNetOAuth.Test/Mocks/TestReplayProtectedChannel.cs
deleted file mode 100644
index d57b72c..0000000
--- a/src/DotNetOAuth.Test/Mocks/TestReplayProtectedChannel.cs
+++ /dev/null
@@ -1,35 +0,0 @@
-//-----------------------------------------------------------------------
-// <copyright file="TestReplayProtectedChannel.cs" company="Andrew Arnott">
-// Copyright (c) Andrew Arnott. All rights reserved.
-// </copyright>
-//-----------------------------------------------------------------------
-
-namespace DotNetOAuth.Test.Mocks {
- using System;
- using System.Collections.Generic;
- using System.Linq;
- using System.Text;
- using DotNetOAuth.Messaging;
- using Microsoft.VisualStudio.TestTools.UnitTesting;
-
- internal class TestReplayProtectedChannel : TestSigningChannel {
- private bool messageReceived;
-
- internal TestReplayProtectedChannel()
- : base(true, true) {
- }
-
- protected override bool IsMessageReplayed(DotNetOAuth.Messaging.IReplayProtectedProtocolMessage message) {
- Assert.AreEqual("someNonce", message.Nonce, "The nonce didn't serialize correctly, or something");
- // this mock implementation passes the first time and fails subsequent times.
- bool replay = this.messageReceived;
- this.messageReceived = true;
- return replay;
- }
-
- protected override void ApplyReplayProtection(IReplayProtectedProtocolMessage message) {
- message.Nonce = "someNonce";
- // no-op
- }
- }
-}
diff --git a/src/DotNetOAuth.Test/Mocks/TestSigningChannel.cs b/src/DotNetOAuth.Test/Mocks/TestSigningChannel.cs
deleted file mode 100644
index 60037ee..0000000
--- a/src/DotNetOAuth.Test/Mocks/TestSigningChannel.cs
+++ /dev/null
@@ -1,29 +0,0 @@
-//-----------------------------------------------------------------------
-// <copyright file="TestSigningChannel.cs" company="Andrew Arnott">
-// Copyright (c) Andrew Arnott. All rights reserved.
-// </copyright>
-//-----------------------------------------------------------------------
-
-namespace DotNetOAuth.Test.Mocks {
- using System;
- using System.Collections.Generic;
- using System.Linq;
- using System.Text;
- using DotNetOAuth.Messaging;
-
- internal class TestSigningChannel : TestChannel {
- internal const string MessageSignature = "mocksignature";
-
- internal TestSigningChannel(bool expiring, bool replay)
- : base(new TestMessageTypeProvider(true, expiring, replay)) {
- }
-
- protected override void Sign(ISignedProtocolMessage message) {
- message.Signature = MessageSignature;
- }
-
- protected override bool IsSignatureValid(ISignedProtocolMessage message) {
- return message.Signature == MessageSignature;
- }
- }
-}
diff --git a/src/DotNetOAuth.Test/TestBase.cs b/src/DotNetOAuth.Test/TestBase.cs
index e41b01c..9fce27c 100644
--- a/src/DotNetOAuth.Test/TestBase.cs
+++ b/src/DotNetOAuth.Test/TestBase.cs
@@ -3,6 +3,7 @@
// Copyright (c) Andrew Arnott. All rights reserved.
// </copyright>
//-----------------------------------------------------------------------
+
namespace DotNetOAuth.Test {
using System.Reflection;
using log4net;
diff --git a/src/DotNetOAuth/DotNetOAuth.csproj b/src/DotNetOAuth/DotNetOAuth.csproj
index fb41be1..253d4df 100644
--- a/src/DotNetOAuth/DotNetOAuth.csproj
+++ b/src/DotNetOAuth/DotNetOAuth.csproj
@@ -68,6 +68,8 @@
<ItemGroup>
<Compile Include="Consumer.cs" />
<Compile Include="IWebRequestHandler.cs" />
+ <Compile Include="Messaging\ChannelProtection.cs" />
+ <Compile Include="Messaging\IChannelBindingElement.cs" />
<Compile Include="Messaging\ReplayedMessageException.cs" />
<Compile Include="Messaging\ExpiredMessageException.cs" />
<Compile Include="Messaging\DataContractMemberComparer.cs" />
@@ -87,6 +89,7 @@
<DependentUpon>MessagingStrings.resx</DependentUpon>
</Compile>
<Compile Include="Messaging\MessagingUtilities.cs" />
+ <Compile Include="Messaging\StandardMessageExpirationBindingElement.cs" />
<Compile Include="OAuthChannel.cs" />
<Compile Include="Messaging\Response.cs" />
<Compile Include="Messaging\IProtocolMessage.cs" />
diff --git a/src/DotNetOAuth/Messaging/Channel.cs b/src/DotNetOAuth/Messaging/Channel.cs
index 4d644c9..5d52b23 100644
--- a/src/DotNetOAuth/Messaging/Channel.cs
+++ b/src/DotNetOAuth/Messaging/Channel.cs
@@ -7,9 +7,11 @@
namespace DotNetOAuth.Messaging {
using System;
using System.Collections.Generic;
+ using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Globalization;
using System.IO;
+ using System.Linq;
using System.Net;
using System.Text;
using System.Web;
@@ -57,36 +59,41 @@ namespace DotNetOAuth.Messaging {
private Response queuedIndirectOrResponseMessage;
/// <summary>
+ /// A list of binding elements in the order they must be applied to outgoing messages.
+ /// </summary>
+ /// <remarks>
+ /// Incoming messages should have the binding elements applied in reverse order.
+ /// </remarks>
+ private List<IChannelBindingElement> bindingElements = new List<IChannelBindingElement>();
+
+ /// <summary>
/// Initializes a new instance of the <see cref="Channel"/> class.
/// </summary>
/// <param name="messageTypeProvider">
/// A class prepared to analyze incoming messages and indicate what concrete
/// message types can deserialize from it.
/// </param>
- protected Channel(IMessageTypeProvider messageTypeProvider) {
+ /// <param name="bindingElements">The binding elements to use in sending and receiving messages.</param>
+ protected Channel(IMessageTypeProvider messageTypeProvider, params IChannelBindingElement[] bindingElements) {
if (messageTypeProvider == null) {
throw new ArgumentNullException("messageTypeProvider");
}
- this.MaximumMessageAge = TimeSpan.FromMinutes(13);
this.messageTypeProvider = messageTypeProvider;
+ this.bindingElements = new List<IChannelBindingElement>(ValidateAndPrepareBindingElements(bindingElements));
}
/// <summary>
- /// Gets or sets the maximum age a message implementing the
- /// <see cref="IExpiringProtocolMessage"/> interface can be before
- /// being discarded as too old.
+ /// Gets the binding elements used by this channel, in the order they are applied to outgoing messages.
/// </summary>
- /// <value>The default value is 13 minutes.</value>
/// <remarks>
- /// This time limit should take into account expected time skew for servers
- /// across the Internet. For example, if a server could conceivably have its
- /// clock d = 5 minutes off UTC time, then any two servers could have
- /// their clocks disagree by as much as 2*d = 10 minutes.
- /// If a message should live for at least t = 3 minutes,
- /// this property should be set to (2*d + t) = 13 minutes.
+ /// Incoming messages are processed by this binding elements in the reverse order.
/// </remarks>
- protected internal TimeSpan MaximumMessageAge { get; set; }
+ protected internal ReadOnlyCollection<IChannelBindingElement> BindingElements {
+ get {
+ return this.bindingElements.AsReadOnly();
+ }
+ }
/// <summary>
/// Gets a tool that can figure out what kind of message is being received
@@ -338,56 +345,6 @@ namespace DotNetOAuth.Messaging {
}
/// <summary>
- /// Signs a given message according to the rules of the channel.
- /// </summary>
- /// <param name="message">The message to sign.</param>
- protected virtual void Sign(ISignedProtocolMessage message) {
- Debug.Assert(message != null, "message == null");
- throw new NotSupportedException(MessagingStrings.SigningNotSupported);
- }
-
- /// <summary>
- /// Gets whether the signature of a signed message is valid or not
- /// according to the rules of the channel.
- /// </summary>
- /// <param name="message">The message whose signature should be verified.</param>
- /// <returns>True if the signature is valid. False otherwise.</returns>
- protected virtual bool IsSignatureValid(ISignedProtocolMessage message) {
- Debug.Assert(message != null, "message == null");
- throw new NotSupportedException(MessagingStrings.SigningNotSupported);
- }
-
- /// <summary>
- /// Applies replay protection on an outgoing message.
- /// </summary>
- /// <param name="message">The message to apply replay protection to.</param>
- /// <remarks>
- /// <para>Implementing this method typically involves generating and setting a nonce property
- /// on the message.</para>
- /// <para>
- /// At the time this method is called, the
- /// <see cref="IExpiringProtocolMessage.UtcCreationDate"/> property will already be
- /// set on the <paramref name="message"/>.</para>
- /// </remarks>
- protected virtual void ApplyReplayProtection(IReplayProtectedProtocolMessage message) {
- throw new NotSupportedException(MessagingStrings.ReplayProtectionNotSupported);
- }
-
- /// <summary>
- /// Gets whether this message has already been processed based on the
- /// replay protection applied by <see cref="ApplyReplayProtection"/>.
- /// </summary>
- /// <param name="message">The message to be checked against the list of recently received messages.</param>
- /// <returns>True if the message has already been processed. False otherwise.</returns>
- /// <remarks>
- /// An exception should NOT be thrown by this method in case of a message replay.
- /// The caller will be responsible to handle the replay attack.
- /// </remarks>
- protected virtual bool IsMessageReplayed(IReplayProtectedProtocolMessage message) {
- throw new NotSupportedException(MessagingStrings.ReplayProtectionNotSupported);
- }
-
- /// <summary>
/// Gets the protocol message that may be in the given HTTP response stream.
/// </summary>
/// <param name="responseStream">The response that is anticipated to contain an OAuth message.</param>
@@ -446,93 +403,95 @@ namespace DotNetOAuth.Messaging {
}
/// <summary>
- /// Prepares a message for transmit by applying signatures, nonces, etc.
+ /// Ensures a consistent and secure set of binding elements and
+ /// sorts them as necessary for a valid sequence of operations.
/// </summary>
- /// <param name="message">The message to prepare for sending.</param>
- private void PrepareMessageForSending(IProtocolMessage message) {
- // The order of operations here is important.
- ISignedProtocolMessage signedMessage = message as ISignedProtocolMessage;
- if (signedMessage != null) {
- IExpiringProtocolMessage expiringMessage = message as IExpiringProtocolMessage;
- if (expiringMessage != null) {
- IReplayProtectedProtocolMessage nonceMessage = message as IReplayProtectedProtocolMessage;
- if (nonceMessage != null) {
- this.ApplyReplayProtection(nonceMessage);
- }
+ /// <param name="elements">The binding elements provided to the channel.</param>
+ /// <returns>The properly ordered list of elements.</returns>
+ /// <exception cref="ProtocolException">Thrown when the binding elements are incomplete or inconsistent with each other.</exception>
+ private static IEnumerable<IChannelBindingElement> ValidateAndPrepareBindingElements(IEnumerable<IChannelBindingElement> elements) {
+ // Filter the elements between the mere transforming ones and the protection ones.
+ var transformationElements = new List<IChannelBindingElement>(
+ elements.Where(element => element.Protection == ChannelProtection.None));
+ var protectionElements = new List<IChannelBindingElement>(
+ elements.Where(element => element.Protection != ChannelProtection.None));
- expiringMessage.UtcCreationDate = DateTime.UtcNow;
+ bool wasLastProtectionPresent = true;
+ foreach (ChannelProtection protectionKind in Enum.GetValues(typeof(ChannelProtection))) {
+ if (protectionKind == ChannelProtection.None) {
+ continue;
}
- this.Sign(signedMessage);
- }
- }
+ int countProtectionsOfThisKind = protectionElements.Count(element => (element.Protection & protectionKind) == protectionKind);
+
+ // Each protection binding element is backed by the presence of its dependent protection(s).
+ if (countProtectionsOfThisKind > 0 && !wasLastProtectionPresent) {
+ throw new ProtocolException(
+ string.Format(
+ CultureInfo.CurrentCulture,
+ MessagingStrings.RequiredProtectionMissing,
+ protectionKind));
+ }
- /// <summary>
- /// Verifies the integrity and applicability of an incoming message.
- /// </summary>
- /// <param name="message">The message just received.</param>
- /// <exception cref="ProtocolException">
- /// Thrown when the message is somehow invalid.
- /// This can be due to tampering, replay attack or expiration, among other things.
- /// </exception>
- private void VerifyMessageAfterReceiving(IProtocolMessage message) {
- // The order of operations is important.
- ISignedProtocolMessage signedMessage = message as ISignedProtocolMessage;
- if (signedMessage != null) {
- this.VerifyMessageSignature(signedMessage);
-
- IExpiringProtocolMessage expiringMessage = message as IExpiringProtocolMessage;
- if (expiringMessage != null) {
- this.VerifyMessageHasNotExpired(expiringMessage);
-
- IReplayProtectedProtocolMessage nonceMessage = message as IReplayProtectedProtocolMessage;
- if (nonceMessage != null) {
- this.VerifyMessageReplayProtection(nonceMessage);
- }
+ // At most one binding element for each protection type.
+ if (countProtectionsOfThisKind > 1) {
+ throw new ProtocolException(
+ string.Format(
+ CultureInfo.CurrentCulture,
+ MessagingStrings.TooManyBindingsOfferingSameProtection,
+ protectionKind,
+ countProtectionsOfThisKind));
}
+ wasLastProtectionPresent = countProtectionsOfThisKind > 0;
}
+
+ // Put the binding elements in order so they are correctly applied to outgoing messages.
+ // Start with the transforming (non-protecting) binding elements first and preserve their original order.
+ var orderedList = new List<IChannelBindingElement>(transformationElements);
+
+ // Now sort the protection binding elements among themselves and add them to the list.
+ orderedList.AddRange(protectionElements.OrderBy(element => element.Protection, BindingElementOutgoingMessageApplicationOrder));
+ return orderedList;
}
/// <summary>
- /// Verifies that a message signature is valid.
+ /// Puts binding elements in their correct outgoing message processing order.
/// </summary>
- /// <param name="signedMessage">The message whose signature is to be verified.</param>
- /// <exception cref="ProtocolException">Thrown if the signature is invalid.</exception>
- private void VerifyMessageSignature(ISignedProtocolMessage signedMessage) {
- Debug.Assert(signedMessage != null, "signedMessage == null");
-
- if (!this.IsSignatureValid(signedMessage)) {
- // TODO: add inResponseTo and remoteReceiver where applicable
- throw new InvalidSignatureException(signedMessage);
- }
+ /// <param name="protection1">The first protection type to compare.</param>
+ /// <param name="protection2">The second protection type to compare.</param>
+ /// <returns>
+ /// -1 if <paramref name="element1"/> should be applied to an outgoing message before <paramref name="element2"/>.
+ /// 1 if <paramref name="element2"/> should be applied to an outgoing message before <paramref name="element1"/>.
+ /// 0 if it doesn't matter.
+ /// </returns>
+ private static int BindingElementOutgoingMessageApplicationOrder(ChannelProtection protection1, ChannelProtection protection2) {
+ Debug.Assert(protection1 != ChannelProtection.None && protection2 != ChannelProtection.None, "This comparison function should only be used to compare protection binding elements. Otherwise we change the order of user-defined message transformations.");
+
+ // Now put the protection ones in the right order.
+ return -((int)protection1).CompareTo((int)protection2); // descending flag ordinal order
}
/// <summary>
- /// Verifies that a given message has not grown too old to process.
+ /// Prepares a message for transmit by applying signatures, nonces, etc.
/// </summary>
- /// <param name="expiringMessage">The message to ensure has not expired.</param>
- /// <exception cref="ProtocolException">Thrown if the message has already expired.</exception>
- private void VerifyMessageHasNotExpired(IExpiringProtocolMessage expiringMessage) {
- Debug.Assert(expiringMessage != null, "expiringMessage == null");
-
- // Yes the UtcCreationDate is supposed to always be in UTC already,
- // but just in case a given message failed to guarantee that, we do it here.
- DateTime expirationDate = expiringMessage.UtcCreationDate.ToUniversalTime() + this.MaximumMessageAge;
- if (expirationDate < DateTime.UtcNow) {
- throw new ExpiredMessageException(expirationDate, expiringMessage);
+ /// <param name="message">The message to prepare for sending.</param>
+ private void PrepareMessageForSending(IProtocolMessage message) {
+ foreach (IChannelBindingElement bindingElement in this.bindingElements) {
+ bindingElement.PrepareMessageForSending(message);
}
}
/// <summary>
- /// Verifies that a message has not already been processed.
+ /// Verifies the integrity and applicability of an incoming message.
/// </summary>
- /// <param name="message">The message to verify.</param>
- /// <exception cref="ProtocolException">Thrown if the message has already been processed.</exception>
- private void VerifyMessageReplayProtection(IReplayProtectedProtocolMessage message) {
- Debug.Assert(message != null, "message == null");
-
- if (this.IsMessageReplayed(message)) {
- throw new ReplayedMessageException(message);
+ /// <param name="message">The message just received.</param>
+ /// <exception cref="ProtocolException">
+ /// Thrown when the message is somehow invalid.
+ /// This can be due to tampering, replay attack or expiration, among other things.
+ /// </exception>
+ private void VerifyMessageAfterReceiving(IProtocolMessage message) {
+ foreach (IChannelBindingElement bindingElement in this.bindingElements.Reverse<IChannelBindingElement>()) {
+ bindingElement.PrepareMessageForReceiving(message);
}
}
}
diff --git a/src/DotNetOAuth/Messaging/ChannelProtection.cs b/src/DotNetOAuth/Messaging/ChannelProtection.cs
new file mode 100644
index 0000000..bbc619c
--- /dev/null
+++ b/src/DotNetOAuth/Messaging/ChannelProtection.cs
@@ -0,0 +1,41 @@
+//-----------------------------------------------------------------------
+// <copyright file="ChannelProtection.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOAuth.Messaging {
+ using System;
+
+ /// <summary>
+ /// Categorizes the various types of channel binding elements so they can be properly ordered.
+ /// </summary>
+ /// <remarks>
+ /// The order of these enum values is significant.
+ /// Each successive value requires the protection offered by all the previous values
+ /// in order to be reliable. For example, message expiration is meaningless without
+ /// tamper protection to prevent a user from changing the timestamp on a message.
+ /// </remarks>
+ [Flags]
+ internal enum ChannelProtection {
+ /// <summary>
+ /// No protection.
+ /// </summary>
+ None = 0x0,
+
+ /// <summary>
+ /// A binding element that signs a message before sending and validates its signature upon receiving.
+ /// </summary>
+ TamperProtection = 0x1,
+
+ /// <summary>
+ /// A binding element that enforces a maximum message age between sending and processing on the receiving side.
+ /// </summary>
+ Expiration = 0x2,
+
+ /// <summary>
+ /// A binding element that prepares messages for replay detection and detects replayed messages on the receiving side.
+ /// </summary>
+ ReplayProtection = 0x4,
+ }
+} \ No newline at end of file
diff --git a/src/DotNetOAuth/Messaging/ExpiredMessageException.cs b/src/DotNetOAuth/Messaging/ExpiredMessageException.cs
index 3d4ded3..fe37b07 100644
--- a/src/DotNetOAuth/Messaging/ExpiredMessageException.cs
+++ b/src/DotNetOAuth/Messaging/ExpiredMessageException.cs
@@ -17,7 +17,7 @@ namespace DotNetOAuth.Messaging {
/// </summary>
/// <param name="utcExpirationDate">The date the message expired.</param>
/// <param name="faultedMessage">The expired message.</param>
- public ExpiredMessageException(DateTime utcExpirationDate, IExpiringProtocolMessage faultedMessage)
+ public ExpiredMessageException(DateTime utcExpirationDate, IProtocolMessage faultedMessage)
: base(string.Format(MessagingStrings.ExpiredMessage, utcExpirationDate.ToUniversalTime(), DateTime.UtcNow), faultedMessage) {
}
diff --git a/src/DotNetOAuth/Messaging/IChannelBindingElement.cs b/src/DotNetOAuth/Messaging/IChannelBindingElement.cs
new file mode 100644
index 0000000..5e72d36
--- /dev/null
+++ b/src/DotNetOAuth/Messaging/IChannelBindingElement.cs
@@ -0,0 +1,36 @@
+//-----------------------------------------------------------------------
+// <copyright file="IChannelBindingElement.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOAuth.Messaging {
+ using System;
+ using System.Collections.Generic;
+ using System.Linq;
+ using System.Text;
+
+ /// <summary>
+ /// An interface that must be implemented by message transforms/validators in order
+ /// to be included in the channel stack.
+ /// </summary>
+ internal interface IChannelBindingElement {
+ /// <summary>
+ /// Gets the protection offered (if any) by this binding element.
+ /// </summary>
+ ChannelProtection Protection { get; }
+
+ /// <summary>
+ /// Prepares a message for sending based on the rules of this channel binding element.
+ /// </summary>
+ /// <param name="message">The message to prepare for sending.</param>
+ void PrepareMessageForSending(IProtocolMessage message);
+
+ /// <summary>
+ /// Performs any transformation on an incoming message that may be necessary and/or
+ /// validates an incoming message based on the rules of this channel binding element.
+ /// </summary>
+ /// <param name="message">The incoming message to process.</param>
+ void PrepareMessageForReceiving(IProtocolMessage message);
+ }
+}
diff --git a/src/DotNetOAuth/Messaging/InvalidSignatureException.cs b/src/DotNetOAuth/Messaging/InvalidSignatureException.cs
index 1b66a3d..7b65f2d 100644
--- a/src/DotNetOAuth/Messaging/InvalidSignatureException.cs
+++ b/src/DotNetOAuth/Messaging/InvalidSignatureException.cs
@@ -16,7 +16,7 @@ namespace DotNetOAuth.Messaging {
/// Initializes a new instance of the <see cref="InvalidSignatureException"/> class.
/// </summary>
/// <param name="faultedMessage">The message with the invalid signature.</param>
- public InvalidSignatureException(ISignedProtocolMessage faultedMessage)
+ public InvalidSignatureException(IProtocolMessage faultedMessage)
: base(MessagingStrings.SignatureInvalid, faultedMessage) { }
/// <summary>
diff --git a/src/DotNetOAuth/Messaging/MessagingStrings.Designer.cs b/src/DotNetOAuth/Messaging/MessagingStrings.Designer.cs
index 2d3985a..702a5be 100644
--- a/src/DotNetOAuth/Messaging/MessagingStrings.Designer.cs
+++ b/src/DotNetOAuth/Messaging/MessagingStrings.Designer.cs
@@ -169,6 +169,15 @@ namespace DotNetOAuth.Messaging {
}
/// <summary>
+ /// Looks up a localized string similar to The binding element offering the {0} protection requires other protection that is not provided..
+ /// </summary>
+ internal static string RequiredProtectionMissing {
+ get {
+ return ResourceManager.GetString("RequiredProtectionMissing", resourceCulture);
+ }
+ }
+
+ /// <summary>
/// Looks up a localized string similar to Message signature was incorrect..
/// </summary>
internal static string SignatureInvalid {
@@ -187,6 +196,15 @@ namespace DotNetOAuth.Messaging {
}
/// <summary>
+ /// Looks up a localized string similar to Expected at most 1 binding element offering the {0} protection, but found {1}..
+ /// </summary>
+ internal static string TooManyBindingsOfferingSameProtection {
+ get {
+ return ResourceManager.GetString("TooManyBindingsOfferingSameProtection", resourceCulture);
+ }
+ }
+
+ /// <summary>
/// Looks up a localized string similar to The type {0} or a derived type was expected, but {1} was given..
/// </summary>
internal static string UnexpectedType {
diff --git a/src/DotNetOAuth/Messaging/MessagingStrings.resx b/src/DotNetOAuth/Messaging/MessagingStrings.resx
index be0b864..a7dd42f 100644
--- a/src/DotNetOAuth/Messaging/MessagingStrings.resx
+++ b/src/DotNetOAuth/Messaging/MessagingStrings.resx
@@ -153,16 +153,22 @@
<data name="ReplayProtectionNotSupported" xml:space="preserve">
<value>This channel does not support replay protection.</value>
</data>
+ <data name="RequiredProtectionMissing" xml:space="preserve">
+ <value>The binding element offering the {0} protection requires other protection that is not provided.</value>
+ </data>
<data name="SignatureInvalid" xml:space="preserve">
<value>Message signature was incorrect.</value>
</data>
<data name="SigningNotSupported" xml:space="preserve">
<value>This channel does not support signing messages. To support signing messages, a derived Channel type must override the Sign and IsSignatureValid methods.</value>
</data>
+ <data name="TooManyBindingsOfferingSameProtection" xml:space="preserve">
+ <value>Expected at most 1 binding element offering the {0} protection, but found {1}.</value>
+ </data>
<data name="UnexpectedType" xml:space="preserve">
<value>The type {0} or a derived type was expected, but {1} was given.</value>
</data>
<data name="UnrecognizedEnumValue" xml:space="preserve">
<value>{0} property has unrecognized value {1}.</value>
</data>
-</root>
+</root> \ No newline at end of file
diff --git a/src/DotNetOAuth/Messaging/MessagingUtilities.cs b/src/DotNetOAuth/Messaging/MessagingUtilities.cs
index e8069f6..7c95696 100644
--- a/src/DotNetOAuth/Messaging/MessagingUtilities.cs
+++ b/src/DotNetOAuth/Messaging/MessagingUtilities.cs
@@ -8,6 +8,7 @@ namespace DotNetOAuth.Messaging {
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
+ using System.Linq;
using System.Net;
using System.Text;
using System.Web;
@@ -115,5 +116,55 @@ namespace DotNetOAuth.Messaging {
return dictionary;
}
+
+ /// <summary>
+ /// Sorts the elements of a sequence in ascending order by using a specified comparer.
+ /// </summary>
+ /// <typeparam name="TSource">The type of the elements of source.</typeparam>
+ /// <typeparam name="TKey">The type of the key returned by keySelector.</typeparam>
+ /// <param name="source">A sequence of values to order.</param>
+ /// <param name="keySelector">A function to extract a key from an element.</param>
+ /// <param name="comparer">A comparison function to compare keys.</param>
+ /// <returns>An System.Linq.IOrderedEnumerable&lt;TElement&gt; whose elements are sorted according to a key.</returns>
+ internal static IOrderedEnumerable<TSource> OrderBy<TSource, TKey>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector, Comparison<TKey> comparer) {
+ return System.Linq.Enumerable.OrderBy<TSource, TKey>(source, keySelector, new ComparisonHelper<TKey>(comparer));
+ }
+
+ /// <summary>
+ /// A class to convert a <see cref="Comparison&lt;T&gt;"/> into an <see cref="IComparer&lt;T&gt;"/>.
+ /// </summary>
+ /// <typeparam name="T">The type of objects being compared.</typeparam>
+ private class ComparisonHelper<T> : IComparer<T> {
+ /// <summary>
+ /// The comparison method to use.
+ /// </summary>
+ private Comparison<T> comparison;
+
+ /// <summary>
+ /// Initializes a new instance of the ComparisonHelper class.
+ /// </summary>
+ /// <param name="comparison">The comparison method to use.</param>
+ internal ComparisonHelper(Comparison<T> comparison) {
+ if (comparison == null) {
+ throw new ArgumentNullException("comparison");
+ }
+
+ this.comparison = comparison;
+ }
+
+ #region IComparer<T> Members
+
+ /// <summary>
+ /// Compares two instances of <typeparamref name="T"/>.
+ /// </summary>
+ /// <param name="x">The first object to compare.</param>
+ /// <param name="y">The second object to compare.</param>
+ /// <returns>Any of -1, 0, or 1 according to standard comparison rules.</returns>
+ public int Compare(T x, T y) {
+ return this.comparison(x, y);
+ }
+
+ #endregion
+ }
}
}
diff --git a/src/DotNetOAuth/Messaging/ReplayedMessageException.cs b/src/DotNetOAuth/Messaging/ReplayedMessageException.cs
index 1df0a4b..2c4e5cb 100644
--- a/src/DotNetOAuth/Messaging/ReplayedMessageException.cs
+++ b/src/DotNetOAuth/Messaging/ReplayedMessageException.cs
@@ -17,7 +17,7 @@ namespace DotNetOAuth.Messaging {
/// Initializes a new instance of the <see cref="ReplayedMessageException"/> class.
/// </summary>
/// <param name="faultedMessage">The replayed message.</param>
- public ReplayedMessageException(IReplayProtectedProtocolMessage faultedMessage) : base(MessagingStrings.ReplayAttackDetected, faultedMessage) { }
+ public ReplayedMessageException(IProtocolMessage faultedMessage) : base(MessagingStrings.ReplayAttackDetected, faultedMessage) { }
/// <summary>
/// Initializes a new instance of the <see cref="ReplayedMessageException"/> class.
diff --git a/src/DotNetOAuth/Messaging/StandardMessageExpirationBindingElement.cs b/src/DotNetOAuth/Messaging/StandardMessageExpirationBindingElement.cs
new file mode 100644
index 0000000..cfa0d31
--- /dev/null
+++ b/src/DotNetOAuth/Messaging/StandardMessageExpirationBindingElement.cs
@@ -0,0 +1,100 @@
+//-----------------------------------------------------------------------
+// <copyright file="StandardMessageExpirationBindingElement.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOAuth.Messaging {
+ using System;
+
+ /// <summary>
+ /// A message expiration enforcing binding element that supports messages
+ /// implementing the <see cref="IExpiringProtocolMessage"/> interface.
+ /// </summary>
+ internal class StandardMessageExpirationBindingElement : IChannelBindingElement {
+ /// <summary>
+ /// The default maximum message age to use if the default constructor is called.
+ /// </summary>
+ internal static readonly TimeSpan DefaultMaximumMessageAge = TimeSpan.FromMinutes(13);
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="StandardMessageExpirationBindingElement"/> class
+ /// with a default maximum message lifetime of 13 minutes.
+ /// </summary>
+ internal StandardMessageExpirationBindingElement()
+ : this(DefaultMaximumMessageAge) {
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="StandardMessageExpirationBindingElement"/> class.
+ /// </summary>
+ /// <param name="maximumAge">
+ /// <para>The maximum age a message implementing the
+ /// <see cref="IExpiringProtocolMessage"/> interface can be before
+ /// being discarded as too old.</para>
+ /// <para>This time limit should take into account expected time skew for servers
+ /// across the Internet. For example, if a server could conceivably have its
+ /// clock d = 5 minutes off UTC time, then any two servers could have
+ /// their clocks disagree by as much as 2*d = 10 minutes.
+ /// If a message should live for at least t = 3 minutes,
+ /// this property should be set to (2*d + t) = 13 minutes.</para>
+ /// </param>
+ internal StandardMessageExpirationBindingElement(TimeSpan maximumAge) {
+ this.MaximumMessageAge = maximumAge;
+ }
+
+ #region IChannelBindingElement Properties
+
+ /// <summary>
+ /// Gets the protection offered by this binding element.
+ /// </summary>
+ /// <value><see cref="ChannelProtection.Expiration"/></value>
+ ChannelProtection IChannelBindingElement.Protection {
+ get { return ChannelProtection.Expiration; }
+ }
+
+ #endregion
+
+ /// <summary>
+ /// Gets the maximum age a message implementing the
+ /// <see cref="IExpiringProtocolMessage"/> interface can be before
+ /// being discarded as too old.
+ /// </summary>
+ protected internal TimeSpan MaximumMessageAge {
+ get;
+ private set;
+ }
+
+ #region IChannelBindingElement Methods
+
+ /// <summary>
+ /// Sets the timestamp on an outgoing message.
+ /// </summary>
+ /// <param name="message">The outgoing message.</param>
+ void IChannelBindingElement.PrepareMessageForSending(IProtocolMessage message) {
+ IExpiringProtocolMessage expiringMessage = message as IExpiringProtocolMessage;
+ if (expiringMessage != null) {
+ expiringMessage.UtcCreationDate = DateTime.UtcNow;
+ }
+ }
+
+ /// <summary>
+ /// Reads the timestamp on a message and throws an exception if the message is too old.
+ /// </summary>
+ /// <param name="message">The incoming message.</param>
+ /// <exception cref="ExpiredMessageException">Thrown if the given message has already expired.</exception>
+ void IChannelBindingElement.PrepareMessageForReceiving(IProtocolMessage message) {
+ IExpiringProtocolMessage expiringMessage = message as IExpiringProtocolMessage;
+ if (expiringMessage != null) {
+ // Yes the UtcCreationDate is supposed to always be in UTC already,
+ // but just in case a given message failed to guarantee that, we do it here.
+ DateTime expirationDate = expiringMessage.UtcCreationDate.ToUniversalTime() + this.MaximumMessageAge;
+ if (expirationDate < DateTime.UtcNow) {
+ throw new ExpiredMessageException(expirationDate, expiringMessage);
+ }
+ }
+ }
+
+ #endregion
+ }
+}