diff options
7 files changed, 168 insertions, 95 deletions
diff --git a/src/DotNetOpenAuth.Core/Messaging/HttpRequestInfo.cs b/src/DotNetOpenAuth.Core/Messaging/HttpRequestInfo.cs index 4b4a3fe..d0f6f63 100644 --- a/src/DotNetOpenAuth.Core/Messaging/HttpRequestInfo.cs +++ b/src/DotNetOpenAuth.Core/Messaging/HttpRequestInfo.cs @@ -61,6 +61,11 @@ namespace DotNetOpenAuth.Messaging { private readonly NameValueCollection serverVariables; /// <summary> + /// The backing field for the <see cref="Cookies"/> property. + /// </summary> + private readonly HttpCookieCollection cookies; + + /// <summary> /// Initializes a new instance of the <see cref="HttpRequestInfo"/> class. /// </summary> /// <param name="request">The request.</param> @@ -75,6 +80,7 @@ namespace DotNetOpenAuth.Messaging { this.form = new NameValueCollection(); this.queryString = HttpUtility.ParseQueryString(requestUri.Query); this.serverVariables = new NameValueCollection(); + this.cookies = new HttpCookieCollection(); Reporting.RecordRequestStatistics(this); } @@ -86,7 +92,8 @@ namespace DotNetOpenAuth.Messaging { /// <param name="requestUri">The request URI.</param> /// <param name="form">The form variables.</param> /// <param name="headers">The HTTP headers.</param> - internal HttpRequestInfo(string httpMethod, Uri requestUri, NameValueCollection form = null, NameValueCollection headers = null) { + /// <param name="cookies">The cookies in the request.</param> + internal HttpRequestInfo(string httpMethod, Uri requestUri, NameValueCollection form = null, NameValueCollection headers = null, HttpCookieCollection cookies = null) { Requires.NotNullOrEmpty(httpMethod, "httpMethod"); Requires.NotNull(requestUri, "requestUri"); @@ -96,6 +103,7 @@ namespace DotNetOpenAuth.Messaging { this.queryString = HttpUtility.ParseQueryString(requestUri.Query); this.headers = headers ?? new WebHeaderCollection(); this.serverVariables = new NameValueCollection(); + this.cookies = cookies ?? new HttpCookieCollection(); } /// <summary> @@ -111,6 +119,7 @@ namespace DotNetOpenAuth.Messaging { this.headers = listenerRequest.Headers; this.form = ParseFormData(listenerRequest.HttpMethod, listenerRequest.Headers, () => listenerRequest.InputStream); this.serverVariables = new NameValueCollection(); + this.cookies = new HttpCookieCollection(); Reporting.RecordRequestStatistics(this); } @@ -131,6 +140,7 @@ namespace DotNetOpenAuth.Messaging { AddHeaders(this.headers, request.Content.Headers); this.form = ParseFormData(this.httpMethod, this.headers, () => request.Content.ReadAsStreamAsync().Result); this.serverVariables = new NameValueCollection(); + this.cookies = new HttpCookieCollection(); Reporting.RecordRequestStatistics(this); } @@ -153,6 +163,7 @@ namespace DotNetOpenAuth.Messaging { this.queryString = HttpUtility.ParseQueryString(requestUri.Query); this.form = ParseFormData(httpMethod, headers, () => inputStream); this.serverVariables = new NameValueCollection(); + this.cookies = new HttpCookieCollection(); Reporting.RecordRequestStatistics(this); } @@ -207,6 +218,14 @@ namespace DotNetOpenAuth.Messaging { } /// <summary> + /// Gets the collection of cookies that were sent by the client. + /// </summary> + /// <returns>The client's cookies.</returns> + public override HttpCookieCollection Cookies { + get { return this.cookies; } + } + + /// <summary> /// Creates an <see cref="HttpRequestBase"/> instance that describes the specified HTTP request. /// </summary> /// <param name="request">The request.</param> diff --git a/src/DotNetOpenAuth.Core/Messaging/MessagingUtilities.cs b/src/DotNetOpenAuth.Core/Messaging/MessagingUtilities.cs index 2e72c97..bbe28ab 100644 --- a/src/DotNetOpenAuth.Core/Messaging/MessagingUtilities.cs +++ b/src/DotNetOpenAuth.Core/Messaging/MessagingUtilities.cs @@ -22,6 +22,7 @@ namespace DotNetOpenAuth.Messaging { using System.Runtime.Serialization.Json; using System.Security; using System.Security.Cryptography; + using System.Threading; using System.Text; using System.Web; using System.Web.Mvc; @@ -41,9 +42,11 @@ namespace DotNetOpenAuth.Messaging { internal static readonly RandomNumberGenerator CryptoRandomDataGenerator = new RNGCryptoServiceProvider(); /// <summary> - /// A pseudo-random data generator (NOT cryptographically strong random data) + /// Gets a random number generator for use on the current thread only. /// </summary> - internal static readonly Random NonCryptoRandomDataGenerator = new Random(); + internal static Random NonCryptoRandomDataGenerator { + get { return ThreadSafeRandom.RandomNumberGenerator; } + } /// <summary> /// The uppercase alphabet. @@ -367,63 +370,63 @@ namespace DotNetOpenAuth.Messaging { } return result == 0; - }
-
- /// <summary>
- /// Gets the public facing URL for the given incoming HTTP request.
- /// </summary>
- /// <param name="request">The incoming request. Cannot be <c>null</c>.</param>
- /// <param name="serverVariables">The server variables to consider part of the request. Cannot be <c>null</c>.</param>
- /// <returns>
- /// The URI that the outside world used to create this request.
- /// </returns>
- /// <remarks>
- /// Although the <paramref name="serverVariables"/> value can be obtained from
- /// <see cref="HttpRequest.ServerVariables"/>, it's useful to be able to pass them
- /// in so we can simulate injected values from our unit tests since the actual property
- /// is a read-only kind of <see cref="NameValueCollection"/>.
- /// </remarks>
- public static Uri GetPublicFacingUrl(this HttpRequestBase request, NameValueCollection serverVariables) {
- Requires.NotNull(request, "request");
- Requires.NotNull(serverVariables, "serverVariables");
-
- // Due to URL rewriting, cloud computing (i.e. Azure)
- // and web farms, etc., we have to be VERY careful about what
- // we consider the incoming URL. We want to see the URL as it would
- // appear on the public-facing side of the hosting web site.
- // HttpRequest.Url gives us the internal URL in a cloud environment,
- // So we use a variable that (at least from what I can tell) gives us
- // the public URL:
- if (serverVariables["HTTP_HOST"] != null) {
- ErrorUtilities.VerifySupported(request.Url.Scheme == Uri.UriSchemeHttps || request.Url.Scheme == Uri.UriSchemeHttp, "Only HTTP and HTTPS are supported protocols.");
- string scheme = serverVariables["HTTP_X_FORWARDED_PROTO"] ?? request.Url.Scheme;
- Uri hostAndPort = new Uri(scheme + Uri.SchemeDelimiter + serverVariables["HTTP_HOST"]);
- UriBuilder publicRequestUri = new UriBuilder(request.Url);
- publicRequestUri.Scheme = scheme;
- publicRequestUri.Host = hostAndPort.Host;
- publicRequestUri.Port = hostAndPort.Port; // CC missing Uri.Port contract that's on UriBuilder.Port
- return publicRequestUri.Uri;
- } else {
- // Failover to the method that works for non-web farm enviroments.
- // We use Request.Url for the full path to the server, and modify it
- // with Request.RawUrl to capture both the cookieless session "directory" if it exists
- // and the original path in case URL rewriting is going on. We don't want to be
- // fooled by URL rewriting because we're comparing the actual URL with what's in
- // the return_to parameter in some cases.
- // Response.ApplyAppPathModifier(builder.Path) would have worked for the cookieless
- // session, but not the URL rewriting problem.
- return new Uri(request.Url, request.RawUrl);
- }
- }
-
- /// <summary>
- /// Gets the public facing URL for the given incoming HTTP request.
- /// </summary>
- /// <param name="request">The incoming request. Cannot be <c>null</c>. Server variables are read from this request.</param>
- /// <returns>The URI that the outside world used to create this request.</returns>
- public static Uri GetPublicFacingUrl(this HttpRequestBase request) {
- Requires.NotNull(request, "request");
- return GetPublicFacingUrl(request, request.ServerVariables);
+ } + + /// <summary> + /// Gets the public facing URL for the given incoming HTTP request. + /// </summary> + /// <param name="request">The incoming request. Cannot be <c>null</c>.</param> + /// <param name="serverVariables">The server variables to consider part of the request. Cannot be <c>null</c>.</param> + /// <returns> + /// The URI that the outside world used to create this request. + /// </returns> + /// <remarks> + /// Although the <paramref name="serverVariables"/> value can be obtained from + /// <see cref="HttpRequest.ServerVariables"/>, it's useful to be able to pass them + /// in so we can simulate injected values from our unit tests since the actual property + /// is a read-only kind of <see cref="NameValueCollection"/>. + /// </remarks> + public static Uri GetPublicFacingUrl(this HttpRequestBase request, NameValueCollection serverVariables) { + Requires.NotNull(request, "request"); + Requires.NotNull(serverVariables, "serverVariables"); + + // Due to URL rewriting, cloud computing (i.e. Azure) + // and web farms, etc., we have to be VERY careful about what + // we consider the incoming URL. We want to see the URL as it would + // appear on the public-facing side of the hosting web site. + // HttpRequest.Url gives us the internal URL in a cloud environment, + // So we use a variable that (at least from what I can tell) gives us + // the public URL: + if (serverVariables["HTTP_HOST"] != null) { + ErrorUtilities.VerifySupported(request.Url.Scheme == Uri.UriSchemeHttps || request.Url.Scheme == Uri.UriSchemeHttp, "Only HTTP and HTTPS are supported protocols."); + string scheme = serverVariables["HTTP_X_FORWARDED_PROTO"] ?? request.Url.Scheme; + Uri hostAndPort = new Uri(scheme + Uri.SchemeDelimiter + serverVariables["HTTP_HOST"]); + UriBuilder publicRequestUri = new UriBuilder(request.Url); + publicRequestUri.Scheme = scheme; + publicRequestUri.Host = hostAndPort.Host; + publicRequestUri.Port = hostAndPort.Port; // CC missing Uri.Port contract that's on UriBuilder.Port + return publicRequestUri.Uri; + } else { + // Failover to the method that works for non-web farm enviroments. + // We use Request.Url for the full path to the server, and modify it + // with Request.RawUrl to capture both the cookieless session "directory" if it exists + // and the original path in case URL rewriting is going on. We don't want to be + // fooled by URL rewriting because we're comparing the actual URL with what's in + // the return_to parameter in some cases. + // Response.ApplyAppPathModifier(builder.Path) would have worked for the cookieless + // session, but not the URL rewriting problem. + return new Uri(request.Url, request.RawUrl); + } + } + + /// <summary> + /// Gets the public facing URL for the given incoming HTTP request. + /// </summary> + /// <param name="request">The incoming request. Cannot be <c>null</c>. Server variables are read from this request.</param> + /// <returns>The URI that the outside world used to create this request.</returns> + public static Uri GetPublicFacingUrl(this HttpRequestBase request) { + Requires.NotNull(request, "request"); + return GetPublicFacingUrl(request, request.ServerVariables); } /// <summary> @@ -662,7 +665,7 @@ namespace DotNetOpenAuth.Messaging { } /// <summary> - /// Gets a cryptographically strong random sequence of values. + /// Gets a cryptographically strong random string of base64 characters. /// </summary> /// <param name="binaryLength">The length of the byte sequence to generate.</param> /// <returns>A base64 encoding of the generated random data, @@ -674,6 +677,18 @@ namespace DotNetOpenAuth.Messaging { } /// <summary> + /// Gets a NON-cryptographically strong random string of base64 characters. + /// </summary> + /// <param name="binaryLength">The length of the byte sequence to generate.</param> + /// <returns>A base64 encoding of the generated random data, + /// whose length in characters will likely be greater than <paramref name="binaryLength"/>.</returns> + internal static string GetNonCryptoRandomDataAsBase64(int binaryLength) { + byte[] uniq_bytes = GetNonCryptoRandomData(binaryLength); + string uniq = Convert.ToBase64String(uniq_bytes); + return uniq; + } + + /// <summary> /// Gets a random string made up of a given set of allowable characters. /// </summary> /// <param name="length">The length of the desired random string.</param> @@ -684,8 +699,9 @@ namespace DotNetOpenAuth.Messaging { Requires.True(allowableCharacters != null && allowableCharacters.Length >= 2, "allowableCharacters"); char[] randomString = new char[length]; + var random = NonCryptoRandomDataGenerator; for (int i = 0; i < length; i++) { - randomString[i] = allowableCharacters[NonCryptoRandomDataGenerator.Next(allowableCharacters.Length)]; + randomString[i] = allowableCharacters[random.Next(allowableCharacters.Length)]; } return new string(randomString); @@ -1545,9 +1561,9 @@ namespace DotNetOpenAuth.Messaging { return HttpDeliveryMethods.DeleteRequest; } else if (httpVerb == "HEAD") { return HttpDeliveryMethods.HeadRequest; - } else if (httpVerb == "PATCH") {
+ } else if (httpVerb == "PATCH") { return HttpDeliveryMethods.PatchRequest; - } else if (httpVerb == "OPTIONS") {
+ } else if (httpVerb == "OPTIONS") { return HttpDeliveryMethods.OptionsRequest; } else { throw ErrorUtilities.ThrowArgumentNamed("httpVerb", MessagingStrings.UnsupportedHttpVerb, httpVerb); @@ -1570,9 +1586,9 @@ namespace DotNetOpenAuth.Messaging { return "DELETE"; } else if ((httpMethod & HttpDeliveryMethods.HttpVerbMask) == HttpDeliveryMethods.HeadRequest) { return "HEAD"; - } else if ((httpMethod & HttpDeliveryMethods.HttpVerbMask) == HttpDeliveryMethods.PatchRequest) {
+ } else if ((httpMethod & HttpDeliveryMethods.HttpVerbMask) == HttpDeliveryMethods.PatchRequest) { return "PATCH"; - } else if ((httpMethod & HttpDeliveryMethods.HttpVerbMask) == HttpDeliveryMethods.OptionsRequest) {
+ } else if ((httpMethod & HttpDeliveryMethods.HttpVerbMask) == HttpDeliveryMethods.OptionsRequest) { return "OPTIONS"; } else if ((httpMethod & HttpDeliveryMethods.AuthorizationHeaderRequest) != 0) { return "GET"; // if AuthorizationHeaderRequest is specified without an explicit HTTP verb, assume GET. @@ -2032,5 +2048,31 @@ namespace DotNetOpenAuth.Messaging { #endregion } + + /// <summary> + /// A thread-safe, non-crypto random number generator. + /// </summary> + private static class ThreadSafeRandom { + /// <summary> + /// The initializer of all new <see cref="Random"/> instances. + /// </summary> + private static readonly Random threadRandomInitializer = new Random(); + + /// <summary> + /// A thread-local instance of <see cref="Random"/> + /// </summary> + private static readonly ThreadLocal<Random> threadRandom = new ThreadLocal<Random>(delegate { + lock (threadRandomInitializer) { + return new Random(threadRandomInitializer.Next()); + } + }); + + /// <summary> + /// Gets a random number generator for use on the current thread only. + /// </summary> + public static Random RandomNumberGenerator { + get { return threadRandom.Value; } + } + } } } diff --git a/src/DotNetOpenAuth.OAuth2.Client/OAuth2/WebServerClient.cs b/src/DotNetOpenAuth.OAuth2.Client/OAuth2/WebServerClient.cs index 4fc8687..1fdd372 100644 --- a/src/DotNetOpenAuth.OAuth2.Client/OAuth2/WebServerClient.cs +++ b/src/DotNetOpenAuth.OAuth2.Client/OAuth2/WebServerClient.cs @@ -113,7 +113,7 @@ namespace DotNetOpenAuth.OAuth2 { if (this.AuthorizationTracker == null) { var context = this.Channel.GetHttpContext(); - string xsrfKey = (new Random()).Next().ToString(CultureInfo.InvariantCulture); + string xsrfKey = MessagingUtilities.GetNonCryptoRandomDataAsBase64(16); cookie = new HttpCookie(XsrfCookieName, xsrfKey) { HttpOnly = true, Secure = FormsAuthentication.RequireSSL, diff --git a/src/DotNetOpenAuth.Test/Mocks/CoordinatingChannel.cs b/src/DotNetOpenAuth.Test/Mocks/CoordinatingChannel.cs index 2e09943..50eff97 100644 --- a/src/DotNetOpenAuth.Test/Mocks/CoordinatingChannel.cs +++ b/src/DotNetOpenAuth.Test/Mocks/CoordinatingChannel.cs @@ -88,6 +88,11 @@ namespace DotNetOpenAuth.Test.Mocks { private Action<IProtocolMessage> outgoingMessageFilter; /// <summary> + /// The simulated clients cookies. + /// </summary> + private HttpCookieCollection cookies = new HttpCookieCollection(); + + /// <summary> /// Initializes a new instance of the <see cref="CoordinatingChannel"/> class. /// </summary> /// <param name="wrappedChannel">The wrapped channel. Must not be null.</param> @@ -158,15 +163,23 @@ namespace DotNetOpenAuth.Test.Mocks { this.incomingMessageSignal.Set(); } + internal void SaveCookies(HttpCookieCollection cookies) { + Requires.NotNull(cookies, "cookies"); + foreach (string cookieName in cookies) { + var cookie = cookies[cookieName]; + this.cookies.Set(cookie); + } + } + protected internal override HttpRequestBase GetRequestFromContext() { MessageReceivingEndpoint recipient; WebHeaderCollection headers; var messageData = this.AwaitIncomingMessage(out recipient, out headers); CoordinatingHttpRequestInfo result; if (messageData != null) { - result = new CoordinatingHttpRequestInfo(this, this.MessageFactory, messageData, recipient); + result = new CoordinatingHttpRequestInfo(this, this.MessageFactory, messageData, recipient, this.cookies); } else { - result = new CoordinatingHttpRequestInfo(recipient); + result = new CoordinatingHttpRequestInfo(recipient, this.cookies); } if (headers != null) { @@ -207,7 +220,7 @@ namespace DotNetOpenAuth.Test.Mocks { protected override OutgoingWebResponse PrepareDirectResponse(IProtocolMessage response) { this.ProcessMessageFilter(response, true); - return new CoordinatingOutgoingWebResponse(response, this.RemoteChannel); + return new CoordinatingOutgoingWebResponse(response, this.RemoteChannel, this); } protected override OutgoingWebResponse PrepareIndirectResponse(IDirectedProtocolMessage message) { diff --git a/src/DotNetOpenAuth.Test/Mocks/CoordinatingHttpRequestInfo.cs b/src/DotNetOpenAuth.Test/Mocks/CoordinatingHttpRequestInfo.cs index a1f5cf5..d53d8c9 100644 --- a/src/DotNetOpenAuth.Test/Mocks/CoordinatingHttpRequestInfo.cs +++ b/src/DotNetOpenAuth.Test/Mocks/CoordinatingHttpRequestInfo.cs @@ -6,10 +6,12 @@ namespace DotNetOpenAuth.Test.Mocks { using System; -using System.Collections.Generic; -using System.Diagnostics.Contracts; -using System.Net; -using DotNetOpenAuth.Messaging; + using System.Collections.Generic; + using System.Diagnostics.Contracts; + using System.Net; + using System.Web; + + using DotNetOpenAuth.Messaging; internal class CoordinatingHttpRequestInfo : HttpRequestInfo { private readonly Channel channel; @@ -34,8 +36,9 @@ using DotNetOpenAuth.Messaging; Channel channel, IMessageFactory messageFactory, IDictionary<string, string> messageData, - MessageReceivingEndpoint recipient) - : this(recipient) { + MessageReceivingEndpoint recipient, + HttpCookieCollection cookies) + : this(recipient, cookies) { Contract.Requires(channel != null); Contract.Requires(messageFactory != null); Contract.Requires(messageData != null); @@ -49,8 +52,8 @@ using DotNetOpenAuth.Messaging; /// that will not generate any message. /// </summary> /// <param name="recipient">The recipient.</param> - internal CoordinatingHttpRequestInfo(MessageReceivingEndpoint recipient) - : base(GetHttpVerb(recipient), recipient != null ? recipient.Location : new Uri("http://host/path")) { + internal CoordinatingHttpRequestInfo(MessageReceivingEndpoint recipient, HttpCookieCollection cookies) + : base(GetHttpVerb(recipient), recipient != null ? recipient.Location : new Uri("http://host/path"), cookies: cookies) { this.recipient = recipient; } diff --git a/src/DotNetOpenAuth.Test/Mocks/CoordinatingOutgoingWebResponse.cs b/src/DotNetOpenAuth.Test/Mocks/CoordinatingOutgoingWebResponse.cs index 8d2c1e7..326cc74 100644 --- a/src/DotNetOpenAuth.Test/Mocks/CoordinatingOutgoingWebResponse.cs +++ b/src/DotNetOpenAuth.Test/Mocks/CoordinatingOutgoingWebResponse.cs @@ -16,16 +16,20 @@ namespace DotNetOpenAuth.Test.Mocks { internal class CoordinatingOutgoingWebResponse : OutgoingWebResponse { private CoordinatingChannel receivingChannel; + private CoordinatingChannel sendingChannel; + /// <summary> /// Initializes a new instance of the <see cref="CoordinatingOutgoingWebResponse"/> class. /// </summary> /// <param name="message">The direct response message to send to the remote channel. This message will be cloned.</param> /// <param name="receivingChannel">The receiving channel.</param> - internal CoordinatingOutgoingWebResponse(IProtocolMessage message, CoordinatingChannel receivingChannel) { + internal CoordinatingOutgoingWebResponse(IProtocolMessage message, CoordinatingChannel receivingChannel, CoordinatingChannel sendingChannel) { Requires.NotNull(message, "message"); Requires.NotNull(receivingChannel, "receivingChannel"); + Requires.NotNull(sendingChannel, "sendingChannel"); this.receivingChannel = receivingChannel; + this.sendingChannel = sendingChannel; this.OriginalMessage = message; } @@ -35,6 +39,7 @@ namespace DotNetOpenAuth.Test.Mocks { } public override void Respond() { + this.sendingChannel.SaveCookies(this.Cookies); this.receivingChannel.PostMessage(this.OriginalMessage); } } diff --git a/src/DotNetOpenAuth.Test/OAuth2/AuthorizationServerTests.cs b/src/DotNetOpenAuth.Test/OAuth2/AuthorizationServerTests.cs index e1af8a5..e8f7172 100644 --- a/src/DotNetOpenAuth.Test/OAuth2/AuthorizationServerTests.cs +++ b/src/DotNetOpenAuth.Test/OAuth2/AuthorizationServerTests.cs @@ -168,33 +168,24 @@ namespace DotNetOpenAuth.Test.OAuth2 { Assert.That(req.User, Is.EqualTo(ResourceOwnerUsername)); return true; }); - var refreshTokenSource = new TaskCompletionSource<string>(); var coordinator = new OAuth2Coordinator<WebServerClient>( AuthorizationServerDescription, authServerMock.Object, new WebServerClient(AuthorizationServerDescription), client => { - try { - var authState = new AuthorizationState(TestScopes) { - Callback = ClientCallback, - }; - client.PrepareRequestUserAuthorization(authState).Respond(); - var result = client.ProcessUserAuthorization(); - Assert.That(result.AccessToken, Is.Not.Null.And.Not.Empty); - Assert.That(result.RefreshToken, Is.Not.Null.And.Not.Empty); - refreshTokenSource.SetResult(result.RefreshToken); - } catch { - refreshTokenSource.TrySetCanceled(); - } + var authState = new AuthorizationState(TestScopes) { + Callback = ClientCallback, + }; + client.PrepareRequestUserAuthorization(authState).Respond(); + var result = client.ProcessUserAuthorization(); + Assert.That(result.AccessToken, Is.Not.Null.And.Not.Empty); + Assert.That(result.RefreshToken, Is.Not.Null.And.Not.Empty); }, server => { var request = server.ReadAuthorizationRequest(); Assert.That(request, Is.Not.Null); server.ApproveAuthorizationRequest(request, ResourceOwnerUsername); server.HandleTokenRequest().Respond(); - var authorization = server.DecodeRefreshToken(refreshTokenSource.Task.Result); - Assert.That(authorization, Is.Not.Null); - Assert.That(authorization.User, Is.EqualTo(ResourceOwnerUsername)); }); coordinator.Run(); } |