diff options
Diffstat (limited to 'src')
52 files changed, 932 insertions, 189 deletions
diff --git a/src/DotNetOpenAuth.AspNet/Clients/OAuth2/MicrosoftClient.cs b/src/DotNetOpenAuth.AspNet/Clients/OAuth2/MicrosoftClient.cs index faea868..653a0b0 100644 --- a/src/DotNetOpenAuth.AspNet/Clients/OAuth2/MicrosoftClient.cs +++ b/src/DotNetOpenAuth.AspNet/Clients/OAuth2/MicrosoftClient.cs @@ -71,6 +71,13 @@ namespace DotNetOpenAuth.AspNet.Clients { #endregion + /// <summary> + /// Gets the identifier for this client as it is registered with Microsoft. + /// </summary> + protected string AppId { + get { return this.appId; } + } + #region Methods /// <summary> diff --git a/src/DotNetOpenAuth.Core/Configuration/DotNetOpenAuth.xsd b/src/DotNetOpenAuth.Core/Configuration/DotNetOpenAuth.xsd index 8a970f4..0fbdd7f 100644 --- a/src/DotNetOpenAuth.Core/Configuration/DotNetOpenAuth.xsd +++ b/src/DotNetOpenAuth.Core/Configuration/DotNetOpenAuth.xsd @@ -945,6 +945,14 @@ <xs:complexType> <xs:choice minOccurs="0" maxOccurs="unbounded"> </xs:choice> + <xs:attribute name="maxAuthorizationTime" type="xs:string"> + <xs:annotation> + <xs:documentation> + The maximum time a user can take at the authorization server before the client considers + the authorization lost. + </xs:documentation> + </xs:annotation> + </xs:attribute> </xs:complexType> </xs:element> <xs:element name="authorizationServer"> diff --git a/src/DotNetOpenAuth.Core/DotNetOpenAuth.Core.csproj b/src/DotNetOpenAuth.Core/DotNetOpenAuth.Core.csproj index eb38711..dc47259 100644 --- a/src/DotNetOpenAuth.Core/DotNetOpenAuth.Core.csproj +++ b/src/DotNetOpenAuth.Core/DotNetOpenAuth.Core.csproj @@ -59,6 +59,7 @@ <Compile Include="Messaging\OutgoingWebResponseActionResult.cs" /> <Compile Include="Messaging\ProtocolFaultResponseException.cs" /> <Compile Include="Messaging\Reflection\IMessagePartEncoder.cs" /> + <Compile Include="Messaging\Reflection\IMessagePartFormattingEncoder.cs" /> <Compile Include="Messaging\Reflection\IMessagePartNullEncoder.cs" /> <Compile Include="Messaging\Reflection\IMessagePartOriginalEncoder.cs" /> <Compile Include="Messaging\Reflection\MessageDescriptionCollection.cs" /> diff --git a/src/DotNetOpenAuth.Core/Messaging/Bindings/ExpiredMessageException.cs b/src/DotNetOpenAuth.Core/Messaging/Bindings/ExpiredMessageException.cs index 5126897..88b8fed 100644 --- a/src/DotNetOpenAuth.Core/Messaging/Bindings/ExpiredMessageException.cs +++ b/src/DotNetOpenAuth.Core/Messaging/Bindings/ExpiredMessageException.cs @@ -22,7 +22,6 @@ namespace DotNetOpenAuth.Messaging.Bindings { public ExpiredMessageException(DateTime utcExpirationDate, IProtocolMessage faultedMessage) : base(string.Format(CultureInfo.CurrentCulture, MessagingStrings.ExpiredMessage, utcExpirationDate.ToLocalTime(), DateTime.Now), faultedMessage) { Requires.True(utcExpirationDate.Kind == DateTimeKind.Utc, "utcExpirationDate"); - Requires.NotNull(faultedMessage, "faultedMessage"); } /// <summary> diff --git a/src/DotNetOpenAuth.Core/Messaging/Channel.cs b/src/DotNetOpenAuth.Core/Messaging/Channel.cs index 672a942..f8ac6a1 100644 --- a/src/DotNetOpenAuth.Core/Messaging/Channel.cs +++ b/src/DotNetOpenAuth.Core/Messaging/Channel.cs @@ -650,10 +650,12 @@ namespace DotNetOpenAuth.Messaging { protected static bool HttpMethodHasEntity(string httpMethod) { if (string.Equals(httpMethod, "GET", StringComparison.Ordinal) || string.Equals(httpMethod, "HEAD", StringComparison.Ordinal) || - string.Equals(httpMethod, "DELETE", StringComparison.Ordinal)) { + string.Equals(httpMethod, "DELETE", StringComparison.Ordinal) || + string.Equals(httpMethod, "OPTIONS", StringComparison.Ordinal)) { return false; } else if (string.Equals(httpMethod, "POST", StringComparison.Ordinal) || - string.Equals(httpMethod, "PUT", StringComparison.Ordinal)) { + string.Equals(httpMethod, "PUT", StringComparison.Ordinal) || + string.Equals(httpMethod, "PATCH", StringComparison.Ordinal)) { return true; } else { throw ErrorUtilities.ThrowArgumentNamed("httpMethod", MessagingStrings.UnsupportedHttpVerb, httpMethod); diff --git a/src/DotNetOpenAuth.Core/Messaging/DataBagFormatterBase.cs b/src/DotNetOpenAuth.Core/Messaging/DataBagFormatterBase.cs index 2452502..e7ac254 100644 --- a/src/DotNetOpenAuth.Core/Messaging/DataBagFormatterBase.cs +++ b/src/DotNetOpenAuth.Core/Messaging/DataBagFormatterBase.cs @@ -193,20 +193,18 @@ namespace DotNetOpenAuth.Messaging { /// Deserializes a <see cref="DataBag"/>, including decompression, decryption, signature and nonce validation where applicable. /// </summary> /// <param name="message">The instance to initialize with deserialized data.</param> - /// <param name="containingMessage">The message that contains the <see cref="DataBag"/> serialized value. Must not be null.</param> /// <param name="value">The serialized form of the <see cref="DataBag"/> to deserialize. Must not be null or empty.</param> - /// <param name="messagePartName">The name of the parameter whose value is to be deserialized. Used for error message generation.</param> + /// <param name="containingMessage">The message that contains the <see cref="DataBag"/> serialized value. May be null if no carrying message is applicable.</param> + /// <param name="messagePartName">The name of the parameter whose value is to be deserialized. Used for error message generation, but may be <c>null</c>.</param> [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "No apparent problem. False positive?")] - public void Deserialize(T message, IProtocolMessage containingMessage, string value, string messagePartName) { + public void Deserialize(T message, string value, IProtocolMessage containingMessage, string messagePartName) { Requires.NotNull(message, "message"); - Requires.NotNull(containingMessage, "containingMessage"); Requires.NotNullOrEmpty(value, "value"); - Requires.NotNullOrEmpty(messagePartName, "messagePartName"); string symmetricSecretHandle = null; if (this.encrypted && this.cryptoKeyStore != null) { string valueWithoutHandle; - MessagingUtilities.ExtractKeyHandleAndPayload(containingMessage, messagePartName, value, out symmetricSecretHandle, out valueWithoutHandle); + MessagingUtilities.ExtractKeyHandleAndPayload(messagePartName, value, out symmetricSecretHandle, out valueWithoutHandle); value = valueWithoutHandle; } diff --git a/src/DotNetOpenAuth.Core/Messaging/HttpDeliveryMethods.cs b/src/DotNetOpenAuth.Core/Messaging/HttpDeliveryMethods.cs index c92b9de..dbd3855 100644 --- a/src/DotNetOpenAuth.Core/Messaging/HttpDeliveryMethods.cs +++ b/src/DotNetOpenAuth.Core/Messaging/HttpDeliveryMethods.cs @@ -48,11 +48,21 @@ namespace DotNetOpenAuth.Messaging { /// <summary> /// Added to the URLs in the query part (as defined by [RFC3986] (Berners-Lee, T., “Uniform Resource Identifiers (URI): Generic Syntax,” .) section 3). /// </summary> - HeadRequest = 0x20, + HeadRequest = 0x20,
+
+ /// <summary>
+ /// Added to the URLs in the query part (as defined by [RFC3986] (Berners-Lee, T., “Uniform Resource Identifiers (URI): Generic Syntax,” .) section 3).
+ /// </summary> + PatchRequest = 0x40,
+
+ /// <summary>
+ /// Added to the URLs in the query part (as defined by [RFC3986] (Berners-Lee, T., “Uniform Resource Identifiers (URI): Generic Syntax,” .) section 3).
+ /// </summary> + OptionsRequest = 0x80, /// <summary> /// The flags that control HTTP verbs. - /// </summary> - HttpVerbMask = PostRequest | GetRequest | PutRequest | DeleteRequest | HeadRequest, + /// </summary>
+ HttpVerbMask = PostRequest | GetRequest | PutRequest | DeleteRequest | HeadRequest | PatchRequest | OptionsRequest, } } diff --git a/src/DotNetOpenAuth.Core/Messaging/HttpRequestInfo.cs b/src/DotNetOpenAuth.Core/Messaging/HttpRequestInfo.cs index 4b4a3fe..75da833 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/IDataBagFormatter.cs b/src/DotNetOpenAuth.Core/Messaging/IDataBagFormatter.cs index 923773e..0d1ab03 100644 --- a/src/DotNetOpenAuth.Core/Messaging/IDataBagFormatter.cs +++ b/src/DotNetOpenAuth.Core/Messaging/IDataBagFormatter.cs @@ -25,10 +25,10 @@ namespace DotNetOpenAuth.Messaging { /// Deserializes a <see cref="DataBag"/>. /// </summary> /// <param name="message">The instance to deserialize into</param> - /// <param name="containingMessage">The message that contains the <see cref="DataBag"/> serialized value. Must not be null.</param> /// <param name="data">The serialized form of the <see cref="DataBag"/> to deserialize. Must not be null or empty.</param> - /// <param name="messagePartName">The name of the parameter whose value is to be deserialized. Used for error message generation.</param> - void Deserialize(T message, IProtocolMessage containingMessage, string data, string messagePartName); + /// <param name="containingMessage">The message that contains the <see cref="DataBag"/> serialized value. May be null if no carrying message is applicable.</param> + /// <param name="messagePartName">The name of the parameter whose value is to be deserialized. Used for error message generation, but may be <c>null</c>.</param> + void Deserialize(T message, string data, IProtocolMessage containingMessage = null, string messagePartName = null); } /// <summary> @@ -61,10 +61,10 @@ namespace DotNetOpenAuth.Messaging { /// Deserializes a <see cref="DataBag"/>. /// </summary> /// <param name="message">The instance to deserialize into</param> - /// <param name="containingMessage">The message that contains the <see cref="DataBag"/> serialized value. Must not be nulll.</param> /// <param name="data">The serialized form of the <see cref="DataBag"/> to deserialize. Must not be null or empty.</param> + /// <param name="containingMessage">The message that contains the <see cref="DataBag"/> serialized value. Must not be nulll.</param> /// <param name="messagePartName">Name of the message part whose value is to be deserialized. Used for exception messages.</param> - void IDataBagFormatter<T>.Deserialize(T message, IProtocolMessage containingMessage, string data, string messagePartName) { + void IDataBagFormatter<T>.Deserialize(T message, string data, IProtocolMessage containingMessage, string messagePartName) { Requires.NotNull(message, "message"); Requires.NotNull(containingMessage, "containingMessage"); Requires.NotNullOrEmpty(data, "data"); diff --git a/src/DotNetOpenAuth.Core/Messaging/MessageSerializer.cs b/src/DotNetOpenAuth.Core/Messaging/MessageSerializer.cs index 15df48a..7391867 100644 --- a/src/DotNetOpenAuth.Core/Messaging/MessageSerializer.cs +++ b/src/DotNetOpenAuth.Core/Messaging/MessageSerializer.cs @@ -97,9 +97,10 @@ namespace DotNetOpenAuth.Messaging { Contract.Assume(partDescription != null); if (partDescription.IsRequired || partDescription.IsNondefaultValueSet(messageDictionary.Message)) { include = true; - if (IsNumeric(partDescription.MemberDeclaredType)) { + Type formattingType = partDescription.PreferredFormattingType; + if (IsNumeric(formattingType)) { type = "number"; - } else if (partDescription.MemberDeclaredType.IsAssignableFrom(typeof(bool))) { + } else if (formattingType.IsAssignableFrom(typeof(bool))) { type = "boolean"; } } diff --git a/src/DotNetOpenAuth.Core/Messaging/MessagingUtilities.cs b/src/DotNetOpenAuth.Core/Messaging/MessagingUtilities.cs index 7aa4469..80703c1 100644 --- a/src/DotNetOpenAuth.Core/Messaging/MessagingUtilities.cs +++ b/src/DotNetOpenAuth.Core/Messaging/MessagingUtilities.cs @@ -23,6 +23,7 @@ namespace DotNetOpenAuth.Messaging { using System.Security; using System.Security.Cryptography; using System.Text; + using System.Threading; using System.Web; using System.Web.Mvc; using System.Xml; @@ -41,11 +42,6 @@ namespace DotNetOpenAuth.Messaging { internal static readonly RandomNumberGenerator CryptoRandomDataGenerator = new RNGCryptoServiceProvider(); /// <summary> - /// A pseudo-random data generator (NOT cryptographically strong random data) - /// </summary> - internal static readonly Random NonCryptoRandomDataGenerator = new Random(); - - /// <summary> /// The uppercase alphabet. /// </summary> internal const string UppercaseLetters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; @@ -155,6 +151,13 @@ namespace DotNetOpenAuth.Messaging { } /// <summary> + /// Gets a random number generator for use on the current thread only. + /// </summary> + internal static Random NonCryptoRandomDataGenerator { + get { return ThreadSafeRandom.RandomNumberGenerator; } + } + + /// <summary> /// Transforms an OutgoingWebResponse to an MVC-friendly ActionResult. /// </summary> /// <param name="response">The response to send to the user agent.</param> @@ -370,6 +373,64 @@ namespace DotNetOpenAuth.Messaging { } /// <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"] ?? + (string.Equals(serverVariables["HTTP_FRONT_END_HTTPS"], "on", StringComparison.OrdinalIgnoreCase) ? Uri.UriSchemeHttps : 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 URL to the root of a web site, which may include a virtual directory path. /// </summary> /// <returns>An absolute URI.</returns> @@ -569,18 +630,15 @@ namespace DotNetOpenAuth.Messaging { /// <summary> /// Extracts the key handle and encrypted blob from a string previously returned from <see cref="CombineKeyHandleAndPayload"/>. /// </summary> - /// <param name="containingMessage">The containing message.</param> - /// <param name="messagePart">The message part.</param> + /// <param name="messagePart">The message part. May be null if not applicable.</param> /// <param name="keyHandleAndBlob">The value previously returned from <see cref="CombineKeyHandleAndPayload"/>.</param> /// <param name="handle">The crypto key handle.</param> /// <param name="dataBlob">The encrypted/signed data.</param> - internal static void ExtractKeyHandleAndPayload(IProtocolMessage containingMessage, string messagePart, string keyHandleAndBlob, out string handle, out string dataBlob) { - Requires.NotNull(containingMessage, "containingMessage"); - Requires.NotNullOrEmpty(messagePart, "messagePart"); + internal static void ExtractKeyHandleAndPayload(string messagePart, string keyHandleAndBlob, out string handle, out string dataBlob) { Requires.NotNullOrEmpty(keyHandleAndBlob, "keyHandleAndBlob"); int privateHandleIndex = keyHandleAndBlob.IndexOf('!'); - ErrorUtilities.VerifyProtocol(privateHandleIndex > 0, MessagingStrings.UnexpectedMessagePartValue, messagePart, keyHandleAndBlob); + ErrorUtilities.VerifyProtocol(privateHandleIndex > 0, MessagingStrings.UnexpectedMessagePartValue, messagePart ?? "<unknown>", keyHandleAndBlob); handle = keyHandleAndBlob.Substring(0, privateHandleIndex); dataBlob = keyHandleAndBlob.Substring(privateHandleIndex + 1); } @@ -608,7 +666,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, @@ -620,6 +678,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> @@ -630,8 +700,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); @@ -1491,6 +1562,10 @@ namespace DotNetOpenAuth.Messaging { return HttpDeliveryMethods.DeleteRequest; } else if (httpVerb == "HEAD") { return HttpDeliveryMethods.HeadRequest; + } else if (httpVerb == "PATCH") { + return HttpDeliveryMethods.PatchRequest; + } else if (httpVerb == "OPTIONS") { + return HttpDeliveryMethods.OptionsRequest; } else { throw ErrorUtilities.ThrowArgumentNamed("httpVerb", MessagingStrings.UnsupportedHttpVerb, httpVerb); } @@ -1500,7 +1575,7 @@ namespace DotNetOpenAuth.Messaging { /// Gets the HTTP verb to use for a given <see cref="HttpDeliveryMethods"/> enum value. /// </summary> /// <param name="httpMethod">The HTTP method.</param> - /// <returns>An HTTP verb, such as GET, POST, PUT, or DELETE.</returns> + /// <returns>An HTTP verb, such as GET, POST, PUT, DELETE, PATCH, or OPTION.</returns> internal static string GetHttpVerb(HttpDeliveryMethods httpMethod) { if ((httpMethod & HttpDeliveryMethods.HttpVerbMask) == HttpDeliveryMethods.GetRequest) { return "GET"; @@ -1512,6 +1587,10 @@ namespace DotNetOpenAuth.Messaging { return "DELETE"; } else if ((httpMethod & HttpDeliveryMethods.HttpVerbMask) == HttpDeliveryMethods.HeadRequest) { return "HEAD"; + } else if ((httpMethod & HttpDeliveryMethods.HttpVerbMask) == HttpDeliveryMethods.PatchRequest) { + return "PATCH"; + } 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. } else { @@ -1901,64 +1980,6 @@ namespace DotNetOpenAuth.Messaging { } /// <summary> - /// Gets the public facing URL for the given incoming HTTP request. - /// </summary> - /// <param name="request">The request.</param> - /// <param name="serverVariables">The server variables to consider part of the request.</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> - internal 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"] ?? - (string.Equals(serverVariables["HTTP_FRONT_END_HTTPS"], "on", StringComparison.OrdinalIgnoreCase) ? Uri.UriSchemeHttps : 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 request.</param> - /// <returns>The URI that the outside world used to create this request.</returns> - internal static Uri GetPublicFacingUrl(this HttpRequestBase request) { - Requires.NotNull(request, "request"); - return GetPublicFacingUrl(request, request.ServerVariables); - } - - /// <summary> /// Gets the query or form data from the original request (before any URL rewriting has occurred.) /// </summary> /// <param name="request">The request.</param> @@ -1995,6 +2016,37 @@ namespace DotNetOpenAuth.Messaging { } /// <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> + [ThreadStatic] + private static Random threadRandom; + + /// <summary> + /// Gets a random number generator for use on the current thread only. + /// </summary> + public static Random RandomNumberGenerator { + get { + if (threadRandom == null) { + lock (threadRandomInitializer) { + threadRandom = new Random(threadRandomInitializer.Next()); + } + } + + return threadRandom; + } + } + } + + /// <summary> /// A class to convert a <see cref="Comparison<T>"/> into an <see cref="IComparer<T>"/>. /// </summary> /// <typeparam name="T">The type of objects being compared.</typeparam> diff --git a/src/DotNetOpenAuth.Core/Messaging/OutgoingWebResponse.cs b/src/DotNetOpenAuth.Core/Messaging/OutgoingWebResponse.cs index 9ef89e9..e1e9d53 100644 --- a/src/DotNetOpenAuth.Core/Messaging/OutgoingWebResponse.cs +++ b/src/DotNetOpenAuth.Core/Messaging/OutgoingWebResponse.cs @@ -43,6 +43,7 @@ namespace DotNetOpenAuth.Messaging { internal OutgoingWebResponse() { this.Status = HttpStatusCode.OK; this.Headers = new WebHeaderCollection(); + this.Cookies = new HttpCookieCollection(); } /// <summary> @@ -56,6 +57,7 @@ namespace DotNetOpenAuth.Messaging { this.Status = response.StatusCode; this.Headers = response.Headers; + this.Cookies = new HttpCookieCollection(); this.ResponseStream = new MemoryStream(response.ContentLength < 0 ? 4 * 1024 : (int)response.ContentLength); using (Stream responseStream = response.GetResponseStream()) { // BUGBUG: strictly speaking, is the response were exactly the limit, we'd report it as truncated here. @@ -86,6 +88,11 @@ namespace DotNetOpenAuth.Messaging { public bool IsResponseTruncated { get; internal set; } /// <summary> + /// Gets the cookies collection to add as headers to the HTTP response. + /// </summary> + public HttpCookieCollection Cookies { get; internal set; } + + /// <summary> /// Gets or sets the body of the response as a string. /// </summary> public string Body { @@ -239,6 +246,17 @@ namespace DotNetOpenAuth.Messaging { response.StatusCode = (int)this.Status; MessagingUtilities.ApplyHeadersToResponse(this.Headers, response); + foreach (HttpCookie httpCookie in this.Cookies) { + var cookie = new Cookie(httpCookie.Name, httpCookie.Value) { + Expires = httpCookie.Expires, + Path = httpCookie.Path, + HttpOnly = httpCookie.HttpOnly, + Secure = httpCookie.Secure, + Domain = httpCookie.Domain, + }; + response.AppendCookie(cookie); + } + if (this.ResponseStream != null) { response.ContentLength64 = this.ResponseStream.Length; this.ResponseStream.CopyTo(response.OutputStream); @@ -346,6 +364,11 @@ namespace DotNetOpenAuth.Messaging { } } + foreach (string cookieName in this.Cookies) { + var cookie = this.Cookies[cookieName]; + context.Response.AppendCookie(cookie); + } + if (endRequest) { // This approach throws an exception in order that // no more code is executed in the calling page. diff --git a/src/DotNetOpenAuth.Core/Messaging/Reflection/IMessagePartFormattingEncoder.cs b/src/DotNetOpenAuth.Core/Messaging/Reflection/IMessagePartFormattingEncoder.cs new file mode 100644 index 0000000..66b7f3c --- /dev/null +++ b/src/DotNetOpenAuth.Core/Messaging/Reflection/IMessagePartFormattingEncoder.cs @@ -0,0 +1,25 @@ +//----------------------------------------------------------------------- +// <copyright file="IMessagePartFormattingEncoder.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.Messaging.Reflection { + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + + /// <summary> + /// An interface describing how various objects can be serialized and deserialized between their object and string forms. + /// </summary> + /// <remarks> + /// Implementations of this interface must include a default constructor and must be thread-safe. + /// </remarks> + public interface IMessagePartFormattingEncoder : IMessagePartEncoder { + /// <summary> + /// Gets the type of the encoded values produced by this encoder, as they would appear in their preferred form. + /// </summary> + Type FormattingType { get; } + } +} diff --git a/src/DotNetOpenAuth.Core/Messaging/Reflection/MessagePart.cs b/src/DotNetOpenAuth.Core/Messaging/Reflection/MessagePart.cs index 0f140d6..a6e8da2 100644 --- a/src/DotNetOpenAuth.Core/Messaging/Reflection/MessagePart.cs +++ b/src/DotNetOpenAuth.Core/Messaging/Reflection/MessagePart.cs @@ -216,6 +216,20 @@ namespace DotNetOpenAuth.Messaging.Reflection { } /// <summary> + /// Gets the type of the encoded values produced by this encoder, as they would appear in their preferred form. + /// </summary> + internal Type PreferredFormattingType { + get { + var formattingEncoder = this.converter.Encoder as IMessagePartFormattingEncoder; + if (formattingEncoder != null) { + return formattingEncoder.FormattingType; + } + + return this.MemberDeclaredType; + } + } + + /// <summary> /// Sets the member of a given message to some given value. /// Used in deserialization. /// </summary> diff --git a/src/DotNetOpenAuth.Core/Messaging/Reflection/ValueMapping.cs b/src/DotNetOpenAuth.Core/Messaging/Reflection/ValueMapping.cs index bc12f5d..4139f52 100644 --- a/src/DotNetOpenAuth.Core/Messaging/Reflection/ValueMapping.cs +++ b/src/DotNetOpenAuth.Core/Messaging/Reflection/ValueMapping.cs @@ -35,7 +35,8 @@ namespace DotNetOpenAuth.Messaging.Reflection { /// <param name="toString">The mapping function that converts some custom value to a string.</param> /// <param name="toOriginalString">The mapping function that converts some custom value to its original (non-normalized) string. May be null if the same as the <paramref name="toString"/> function.</param> /// <param name="toValue">The mapping function that converts a string to some custom value.</param> - internal ValueMapping(Func<object, string> toString, Func<object, string> toOriginalString, Func<string, object> toValue) { + internal ValueMapping(Func<object, string> toString, Func<object, string> toOriginalString, Func<string, object> toValue) + : this() { Requires.NotNull(toString, "toString"); Requires.NotNull(toValue, "toValue"); @@ -48,8 +49,11 @@ namespace DotNetOpenAuth.Messaging.Reflection { /// Initializes a new instance of the <see cref="ValueMapping"/> struct. /// </summary> /// <param name="encoder">The encoder.</param> - internal ValueMapping(IMessagePartEncoder encoder) { + internal ValueMapping(IMessagePartEncoder encoder) + : this() { Requires.NotNull(encoder, "encoder"); + + this.Encoder = encoder; var nullEncoder = encoder as IMessagePartNullEncoder; string nullString = nullEncoder != null ? nullEncoder.EncodedNullValue : null; @@ -63,5 +67,10 @@ namespace DotNetOpenAuth.Messaging.Reflection { this.StringToValue = str => (str != null) ? encoder.Decode(str) : null; this.ValueToOriginalString = obj => (obj != null) ? originalStringEncode(obj) : nullString; } + + /// <summary> + /// Gets the encoder. + /// </summary> + internal IMessagePartEncoder Encoder { get; private set; } } } diff --git a/src/DotNetOpenAuth.Core/Messaging/StandardWebRequestHandler.cs b/src/DotNetOpenAuth.Core/Messaging/StandardWebRequestHandler.cs index 114c191..adca925 100644 --- a/src/DotNetOpenAuth.Core/Messaging/StandardWebRequestHandler.cs +++ b/src/DotNetOpenAuth.Core/Messaging/StandardWebRequestHandler.cs @@ -151,14 +151,25 @@ namespace DotNetOpenAuth.Messaging { return new NetworkDirectWebResponse(request.RequestUri, response); } - if (Logger.Http.IsErrorEnabled) { - if (response != null) { + if (response != null) { + Logger.Http.ErrorFormat( + "{0} returned {1} {2}: {3}", + response.ResponseUri, + (int)response.StatusCode, + response.StatusCode, + response.StatusDescription); + + if (Logger.Http.IsDebugEnabled) { using (var reader = new StreamReader(ex.Response.GetResponseStream())) { - Logger.Http.ErrorFormat("WebException from {0}: {1}{2}", ex.Response.ResponseUri, Environment.NewLine, reader.ReadToEnd()); + Logger.Http.DebugFormat( + "WebException from {0}: {1}{2}", ex.Response.ResponseUri, Environment.NewLine, reader.ReadToEnd()); } - } else { - Logger.Http.ErrorFormat("WebException {1} from {0}, no response available.", request.RequestUri, ex.Status); } + } else { + Logger.Http.ErrorFormat( + "{0} connecting to {0}", + ex.Status, + request.RequestUri); } // Be sure to close the response stream to conserve resources and avoid diff --git a/src/DotNetOpenAuth.Core/Messaging/TimespanSecondsEncoder.cs b/src/DotNetOpenAuth.Core/Messaging/TimespanSecondsEncoder.cs index 842176a..abce20a 100644 --- a/src/DotNetOpenAuth.Core/Messaging/TimespanSecondsEncoder.cs +++ b/src/DotNetOpenAuth.Core/Messaging/TimespanSecondsEncoder.cs @@ -12,7 +12,7 @@ namespace DotNetOpenAuth.Messaging { /// <summary> /// Encodes and decodes the <see cref="TimeSpan"/> as an integer of total seconds. /// </summary> - internal class TimespanSecondsEncoder : IMessagePartEncoder { + internal class TimespanSecondsEncoder : IMessagePartEncoder, IMessagePartFormattingEncoder { /// <summary> /// Initializes a new instance of the <see cref="TimespanSecondsEncoder"/> class. /// </summary> @@ -20,6 +20,17 @@ namespace DotNetOpenAuth.Messaging { // Note that this constructor is public so it can be instantiated via Activator. } + #region IMessagePartFormattingEncoder members + + /// <summary> + /// Gets the type of the encoded values produced by this encoder, as they would appear in their preferred form. + /// </summary> + public Type FormattingType { + get { return typeof(int); } + } + + #endregion + #region IMessagePartEncoder Members /// <summary> diff --git a/src/DotNetOpenAuth.OAuth2.AuthorizationServer/DotNetOpenAuth.OAuth2.AuthorizationServer.csproj b/src/DotNetOpenAuth.OAuth2.AuthorizationServer/DotNetOpenAuth.OAuth2.AuthorizationServer.csproj index 34d59ee..73c563b 100644 --- a/src/DotNetOpenAuth.OAuth2.AuthorizationServer/DotNetOpenAuth.OAuth2.AuthorizationServer.csproj +++ b/src/DotNetOpenAuth.OAuth2.AuthorizationServer/DotNetOpenAuth.OAuth2.AuthorizationServer.csproj @@ -27,6 +27,8 @@ <DependentUpon>AuthServerStrings.resx</DependentUpon> </Compile> <Compile Include="OAuth2\AuthServerUtilities.cs" /> + <Compile Include="OAuth2\AutomatedAuthorizationCheckResponse.cs" /> + <Compile Include="OAuth2\AutomatedUserAuthorizationCheckResponse.cs" /> <Compile Include="OAuth2\ChannelElements\AggregatingClientCredentialReader.cs" /> <Compile Include="OAuth2\ChannelElements\ClientCredentialHttpBasicReader.cs" /> <Compile Include="OAuth2\ChannelElements\ClientCredentialMessagePartReader.cs" /> @@ -42,6 +44,7 @@ <Compile Include="OAuth2\IAuthorizationServerHost.cs" /> <Compile Include="OAuth2\Messages\AccessTokenAuthorizationCodeRequestAS.cs" /> <Compile Include="OAuth2\Messages\AccessTokenRefreshRequestAS.cs" /> + <Compile Include="OAuth2\Messages\AccessTokenResult.cs" /> <Compile Include="OAuth2\Messages\EndUserAuthorizationSuccessAuthCodeResponseAS.cs" /> <Compile Include="OAuth2\Messages\IAuthorizationCodeCarryingRequest.cs" /> <Compile Include="OAuth2\Messages\IRefreshTokenCarryingRequest.cs" /> diff --git a/src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/AuthorizationServer.cs b/src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/AuthorizationServer.cs index 1e404e7..050a4ab 100644 --- a/src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/AuthorizationServer.cs +++ b/src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/AuthorizationServer.cs @@ -265,6 +265,25 @@ namespace DotNetOpenAuth.OAuth2 { } /// <summary> + /// Decodes a refresh token into its authorization details. + /// </summary> + /// <param name="refreshToken">The encoded refresh token as it would appear to the client.</param> + /// <returns>A description of the authorization represented by the refresh token.</returns> + /// <exception cref="ProtocolException">Thrown if the refresh token is not valid due to expiration, corruption or not being authentic.</exception> + /// <remarks> + /// This can be useful if the authorization server supports the client revoking its own access (on uninstall, for example). + /// Outside the scope of the OAuth 2 spec, the client may contact the authorization server host requesting that its refresh + /// token be revoked. The authorization server would need to decode the refresh token so it knows which authorization in + /// the database to delete. + /// </remarks> + public IAuthorizationDescription DecodeRefreshToken(string refreshToken) { + var refreshTokenFormatter = RefreshToken.CreateFormatter(this.AuthorizationServerServices.CryptoKeyStore); + var token = new RefreshToken(); + refreshTokenFormatter.Deserialize(token, refreshToken); + return token; + } + + /// <summary> /// Gets the redirect URL to use for a particular authorization request. /// </summary> /// <param name="authorizationRequest">The authorization request.</param> diff --git a/src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/AutomatedAuthorizationCheckResponse.cs b/src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/AutomatedAuthorizationCheckResponse.cs new file mode 100644 index 0000000..0179d05 --- /dev/null +++ b/src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/AutomatedAuthorizationCheckResponse.cs @@ -0,0 +1,40 @@ +//----------------------------------------------------------------------- +// <copyright file="AutomatedAuthorizationCheckResponse.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OAuth2 { + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + using DotNetOpenAuth.OAuth2.Messages; + + /// <summary> + /// Describes the result of an automated authorization check, such as for client credential or resource owner password grants. + /// </summary> + public class AutomatedAuthorizationCheckResponse { + /// <summary> + /// Initializes a new instance of the <see cref="AutomatedAuthorizationCheckResponse" /> class. + /// </summary> + /// <param name="accessRequest">The access token request.</param> + /// <param name="approved">A value indicating whether the authorization should be approved.</param> + public AutomatedAuthorizationCheckResponse(IAccessTokenRequest accessRequest, bool approved) { + Requires.NotNull(accessRequest, "accessRequest"); + + this.IsApproved = approved; + this.ApprovedScope = new HashSet<string>(accessRequest.Scope); + } + + /// <summary> + /// Gets a value indicating whether the authorization should be approved. + /// </summary> + public bool IsApproved { get; private set; } + + /// <summary> + /// Gets the scope to be granted. + /// </summary> + public HashSet<string> ApprovedScope { get; private set; } + } +} diff --git a/src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/AutomatedUserAuthorizationCheckResponse.cs b/src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/AutomatedUserAuthorizationCheckResponse.cs new file mode 100644 index 0000000..b62807c --- /dev/null +++ b/src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/AutomatedUserAuthorizationCheckResponse.cs @@ -0,0 +1,42 @@ +//----------------------------------------------------------------------- +// <copyright file="AutomatedUserAuthorizationCheckResponse.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OAuth2 { + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + + using DotNetOpenAuth.OAuth2.Messages; + + /// <summary> + /// Describes the result of an automated authorization check for resource owner grants. + /// </summary> + public class AutomatedUserAuthorizationCheckResponse : AutomatedAuthorizationCheckResponse { + /// <summary> + /// Initializes a new instance of the <see cref="AutomatedUserAuthorizationCheckResponse" /> class. + /// </summary> + /// <param name="accessRequest">The access token request.</param> + /// <param name="approved">A value indicating whether the authorization should be approved.</param> + /// <param name="canonicalUserName"> + /// Canonical username of the authorizing user (resource owner), as the resource server would recognize it. + /// Ignored if <paramref name="approved"/> is false. + /// </param> + public AutomatedUserAuthorizationCheckResponse(IAccessTokenRequest accessRequest, bool approved, string canonicalUserName) + : base(accessRequest, approved) { + if (approved) { + Requires.NotNullOrEmpty(canonicalUserName, "canonicalUserName"); + } + + this.CanonicalUserName = canonicalUserName; + } + + /// <summary> + /// Gets the canonical username of the authorizing user (resource owner). + /// </summary> + public string CanonicalUserName { get; private set; } + } +} diff --git a/src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/ChannelElements/MessageValidationBindingElement.cs b/src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/ChannelElements/MessageValidationBindingElement.cs index 27b71db..500b91d 100644 --- a/src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/ChannelElements/MessageValidationBindingElement.cs +++ b/src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/ChannelElements/MessageValidationBindingElement.cs @@ -120,15 +120,17 @@ namespace DotNetOpenAuth.OAuth2.ChannelElements { // Check that any resource owner password credential is correct. if (resourceOwnerPasswordCarrier != null) { try { - string canonicalUserName; - if (this.AuthorizationServer.TryAuthorizeResourceOwnerCredentialGrant(resourceOwnerPasswordCarrier.UserName, resourceOwnerPasswordCarrier.Password, resourceOwnerPasswordCarrier, out canonicalUserName)) { - ErrorUtilities.VerifyHost(!string.IsNullOrEmpty(canonicalUserName), "TryAuthorizeResourceOwnerCredentialGrant did not initialize out parameter."); + var authorizeResult = + this.AuthorizationServer.CheckAuthorizeResourceOwnerCredentialGrant( + resourceOwnerPasswordCarrier.RequestingUserName, resourceOwnerPasswordCarrier.Password, resourceOwnerPasswordCarrier); + if (authorizeResult.IsApproved) { resourceOwnerPasswordCarrier.CredentialsValidated = true; - resourceOwnerPasswordCarrier.UserName = canonicalUserName; + resourceOwnerPasswordCarrier.RequestingUserName = authorizeResult.CanonicalUserName; + resourceOwnerPasswordCarrier.Scope.ResetContents(authorizeResult.ApprovedScope); } else { Logger.OAuth.ErrorFormat( "Resource owner password credential for user \"{0}\" rejected by authorization server host.", - resourceOwnerPasswordCarrier.UserName); + resourceOwnerPasswordCarrier.RequestingUserName); throw new TokenEndpointProtocolException(accessTokenRequest, Protocol.AccessTokenRequestErrorCodes.InvalidGrant, AuthServerStrings.InvalidResourceOwnerPasswordCredential); } } catch (NotSupportedException) { @@ -140,12 +142,15 @@ namespace DotNetOpenAuth.OAuth2.ChannelElements { applied = true; } else if (clientCredentialOnly != null) { try { - if (!this.AuthorizationServer.TryAuthorizeClientCredentialsGrant(clientCredentialOnly)) { + var authorizeResult = this.AuthorizationServer.CheckAuthorizeClientCredentialsGrant(clientCredentialOnly); + if (!authorizeResult.IsApproved) { Logger.OAuth.ErrorFormat( "Client credentials grant access request for client \"{0}\" rejected by authorization server host.", clientCredentialOnly.ClientIdentifier); throw new TokenEndpointProtocolException(accessTokenRequest, Protocol.AccessTokenRequestErrorCodes.UnauthorizedClient); } + + clientCredentialOnly.Scope.ResetContents(authorizeResult.ApprovedScope); } catch (NotSupportedException) { throw new TokenEndpointProtocolException(accessTokenRequest, Protocol.AccessTokenRequestErrorCodes.UnsupportedGrantType); } catch (NotImplementedException) { diff --git a/src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/ChannelElements/TokenCodeSerializationBindingElement.cs b/src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/ChannelElements/TokenCodeSerializationBindingElement.cs index 494a10b..5a1dbae 100644 --- a/src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/ChannelElements/TokenCodeSerializationBindingElement.cs +++ b/src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/ChannelElements/TokenCodeSerializationBindingElement.cs @@ -103,7 +103,7 @@ namespace DotNetOpenAuth.OAuth2.ChannelElements { if (authCodeCarrier != null) { var authorizationCodeFormatter = AuthorizationCode.CreateFormatter(this.AuthorizationServer); var authorizationCode = new AuthorizationCode(); - authorizationCodeFormatter.Deserialize(authorizationCode, message, authCodeCarrier.Code, Protocol.code); + authorizationCodeFormatter.Deserialize(authorizationCode, authCodeCarrier.Code, message, Protocol.code); authCodeCarrier.AuthorizationDescription = authorizationCode; } @@ -111,7 +111,7 @@ namespace DotNetOpenAuth.OAuth2.ChannelElements { if (refreshTokenCarrier != null) { var refreshTokenFormatter = RefreshToken.CreateFormatter(this.AuthorizationServer.CryptoKeyStore); var refreshToken = new RefreshToken(); - refreshTokenFormatter.Deserialize(refreshToken, message, refreshTokenCarrier.RefreshToken, Protocol.refresh_token); + refreshTokenFormatter.Deserialize(refreshToken, refreshTokenCarrier.RefreshToken, message, Protocol.refresh_token); refreshTokenCarrier.AuthorizationDescription = refreshToken; } diff --git a/src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/IAuthorizationServerHost.cs b/src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/IAuthorizationServerHost.cs index b75cb29..b9b5725 100644 --- a/src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/IAuthorizationServerHost.cs +++ b/src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/IAuthorizationServerHost.cs @@ -91,17 +91,11 @@ namespace DotNetOpenAuth.OAuth2 { /// The access request the credentials came with. /// This may be useful if the authorization server wishes to apply some policy based on the client that is making the request. /// </param> - /// <param name="canonicalUserName"> - /// Receives the canonical username (normalized for the resource server) of the user, for valid credentials; - /// Or <c>null</c> if the return value is false. - /// </param> - /// <returns> - /// <c>true</c> if the given credentials are valid and the authorization granted; otherwise, <c>false</c>. - /// </returns> + /// <returns>A value that describes the result of the authorization check.</returns> /// <exception cref="NotSupportedException"> /// May be thrown if the authorization server does not support the resource owner password credential grant type. /// </exception> - bool TryAuthorizeResourceOwnerCredentialGrant(string userName, string password, IAccessTokenRequest accessRequest, out string canonicalUserName); + AutomatedUserAuthorizationCheckResponse CheckAuthorizeResourceOwnerCredentialGrant(string userName, string password, IAccessTokenRequest accessRequest); /// <summary> /// Determines whether an access token request given a client credential grant should be authorized @@ -112,17 +106,15 @@ namespace DotNetOpenAuth.OAuth2 { /// The access request the credentials came with. /// This may be useful if the authorization server wishes to apply some policy based on the client that is making the request. /// </param> - /// <returns> - /// <c>true</c> if the given credentials are valid and the authorization granted; otherwise, <c>false</c>. - /// </returns> + /// <returns>A value that describes the result of the authorization check.</returns> /// <exception cref="NotSupportedException"> /// May be thrown if the authorization server does not support the client credential grant type. /// </exception> - bool TryAuthorizeClientCredentialsGrant(IAccessTokenRequest accessRequest); + AutomatedAuthorizationCheckResponse CheckAuthorizeClientCredentialsGrant(IAccessTokenRequest accessRequest); } /// <summary> - /// Code Contract for the <see cref="IAuthorizationServerHost"/> interface. + /// Code Contract for the <see cref="IAuthorizationServerHost" /> interface. /// </summary> [ContractClassFor(typeof(IAuthorizationServerHost))] internal abstract class IAuthorizationServerHostContract : IAuthorizationServerHost { @@ -203,40 +195,34 @@ namespace DotNetOpenAuth.OAuth2 { /// The access request the credentials came with. /// This may be useful if the authorization server wishes to apply some policy based on the client that is making the request. /// </param> - /// <param name="canonicalUserName"> - /// Receives the canonical username (normalized for the resource server) of the user, for valid credentials; - /// Or <c>null</c> if the return value is false. - /// </param> /// <returns> - /// <c>true</c> if the given credentials are valid and the authorization granted; otherwise, <c>false</c>. + /// A value that describes the result of the authorization check. /// </returns> /// <exception cref="NotSupportedException"> /// May be thrown if the authorization server does not support the resource owner password credential grant type. /// </exception> - bool IAuthorizationServerHost.TryAuthorizeResourceOwnerCredentialGrant(string userName, string password, IAccessTokenRequest accessRequest, out string canonicalUserName) { + AutomatedUserAuthorizationCheckResponse IAuthorizationServerHost.CheckAuthorizeResourceOwnerCredentialGrant(string userName, string password, IAccessTokenRequest accessRequest) { Contract.Requires(!string.IsNullOrEmpty(userName)); Contract.Requires(password != null); Contract.Requires(accessRequest != null); - Contract.Ensures(!Contract.Result<bool>() || !string.IsNullOrEmpty(Contract.ValueAtReturn<string>(out canonicalUserName))); + Contract.Ensures(Contract.Result<AutomatedUserAuthorizationCheckResponse>() != null); throw new NotImplementedException(); } /// <summary> /// Determines whether an access token request given a client credential grant should be authorized - /// and if so records an authorization entry such that subsequent calls to <see cref="IAuthorizationServerHost.IsAuthorizationValid"/> would + /// and if so records an authorization entry such that subsequent calls to <see cref="IAuthorizationServerHost.IsAuthorizationValid" /> would /// return <c>true</c>. /// </summary> - /// <param name="accessRequest"> - /// The access request the credentials came with. - /// This may be useful if the authorization server wishes to apply some policy based on the client that is making the request. - /// </param> + /// <param name="accessRequest">The access request the credentials came with. + /// This may be useful if the authorization server wishes to apply some policy based on the client that is making the request.</param> /// <returns> - /// <c>true</c> if the given credentials are valid and the authorization granted; otherwise, <c>false</c>. + /// A value that describes the result of the authorization check. /// </returns> - /// <exception cref="NotSupportedException"> - /// May be thrown if the authorization server does not support the client credential grant type. - /// </exception> - bool IAuthorizationServerHost.TryAuthorizeClientCredentialsGrant(IAccessTokenRequest accessRequest) { + /// <exception cref="NotSupportedException">May be thrown if the authorization server does not support the client credential grant type.</exception> + AutomatedAuthorizationCheckResponse IAuthorizationServerHost.CheckAuthorizeClientCredentialsGrant(IAccessTokenRequest accessRequest) { + Contract.Requires(accessRequest != null); + Contract.Ensures(Contract.Result<AutomatedAuthorizationCheckResponse>() != null); throw new NotImplementedException(); } diff --git a/src/DotNetOpenAuth.OAuth2.ClientAuthorization/OAuth2/Messages/AccessTokenResult.cs b/src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/Messages/AccessTokenResult.cs index 11e486b..4297165 100644 --- a/src/DotNetOpenAuth.OAuth2.ClientAuthorization/OAuth2/Messages/AccessTokenResult.cs +++ b/src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/Messages/AccessTokenResult.cs @@ -14,12 +14,12 @@ namespace DotNetOpenAuth.OAuth2 { /// <summary> /// Describes the parameters to be fed into creating a response to an access token request. /// </summary> - public class AccessTokenResult { + public class AccessTokenResult : IAccessTokenResult { /// <summary> /// Initializes a new instance of the <see cref="AccessTokenResult"/> class. /// </summary> /// <param name="accessToken">The access token to include in this result.</param> - public AccessTokenResult(AccessToken accessToken) { + public AccessTokenResult(AuthorizationServerAccessToken accessToken) { Requires.NotNull(accessToken, "accessToken"); this.AllowRefreshToken = true; this.AccessToken = accessToken; @@ -38,6 +38,13 @@ namespace DotNetOpenAuth.OAuth2 { /// <summary> /// Gets the access token. /// </summary> - public AccessToken AccessToken { get; private set; } + public AuthorizationServerAccessToken AccessToken { get; private set; } + + /// <summary> + /// Gets the access token. + /// </summary> + AccessToken IAccessTokenResult.AccessToken { + get { return this.AccessToken; } + } } } diff --git a/src/DotNetOpenAuth.OAuth2.Client/Configuration/OAuth2ClientSection.cs b/src/DotNetOpenAuth.OAuth2.Client/Configuration/OAuth2ClientSection.cs index 1ee5aa5..3bb6ffc 100644 --- a/src/DotNetOpenAuth.OAuth2.Client/Configuration/OAuth2ClientSection.cs +++ b/src/DotNetOpenAuth.OAuth2.Client/Configuration/OAuth2ClientSection.cs @@ -5,6 +5,7 @@ //----------------------------------------------------------------------- namespace DotNetOpenAuth.Configuration { + using System; using System.Configuration; using System.Diagnostics.Contracts; @@ -18,6 +19,11 @@ namespace DotNetOpenAuth.Configuration { private const string SectionName = OAuth2SectionGroup.SectionName + "/client"; /// <summary> + /// The name of the @maxAuthorizationTime attribute. + /// </summary> + private const string MaxAuthorizationTimePropertyName = "maxAuthorizationTime"; + + /// <summary> /// Initializes a new instance of the <see cref="OAuth2ClientSection"/> class. /// </summary> internal OAuth2ClientSection() { @@ -32,5 +38,24 @@ namespace DotNetOpenAuth.Configuration { return (OAuth2ClientSection)ConfigurationManager.GetSection(SectionName) ?? new OAuth2ClientSection(); } } + + /// <summary> + /// Gets or sets the maximum time a user can take to complete authentication. + /// </summary> + [ConfigurationProperty(MaxAuthorizationTimePropertyName, DefaultValue = "0:15")] // 15 minutes + [PositiveTimeSpanValidator] + internal TimeSpan MaxAuthorizationTime { + get { + Contract.Ensures(Contract.Result<TimeSpan>() > TimeSpan.Zero); + TimeSpan result = (TimeSpan)this[MaxAuthorizationTimePropertyName]; + Contract.Assume(result > TimeSpan.Zero); // our PositiveTimeSpanValidator should take care of this + return result; + } + + set { + Requires.InRange(value > TimeSpan.Zero, "value"); + this[MaxAuthorizationTimePropertyName] = value; + } + } } } diff --git a/src/DotNetOpenAuth.OAuth2.Client/OAuth2/ChannelElements/IOAuth2ChannelWithClient.cs b/src/DotNetOpenAuth.OAuth2.Client/OAuth2/ChannelElements/IOAuth2ChannelWithClient.cs index c802be6..db8b9f8 100644 --- a/src/DotNetOpenAuth.OAuth2.Client/OAuth2/ChannelElements/IOAuth2ChannelWithClient.cs +++ b/src/DotNetOpenAuth.OAuth2.Client/OAuth2/ChannelElements/IOAuth2ChannelWithClient.cs @@ -9,6 +9,7 @@ namespace DotNetOpenAuth.OAuth2.ChannelElements { using System.Collections.Generic; using System.Linq; using System.Text; + using System.Xml; /// <summary> /// An interface that defines the OAuth2 client specific channel additions. @@ -23,5 +24,10 @@ namespace DotNetOpenAuth.OAuth2.ChannelElements { /// Gets or sets the client credentials applicator extension to use. /// </summary> ClientCredentialApplicator ClientCredentialApplicator { get; set; } + + /// <summary> + /// Gets quotas used when deserializing JSON. + /// </summary> + XmlDictionaryReaderQuotas JsonReaderQuotas { get; } } } diff --git a/src/DotNetOpenAuth.OAuth2.Client/OAuth2/ChannelElements/OAuth2ClientChannel.cs b/src/DotNetOpenAuth.OAuth2.Client/OAuth2/ChannelElements/OAuth2ClientChannel.cs index 8ad2ed9..b0cdb4b 100644 --- a/src/DotNetOpenAuth.OAuth2.Client/OAuth2/ChannelElements/OAuth2ClientChannel.cs +++ b/src/DotNetOpenAuth.OAuth2.Client/OAuth2/ChannelElements/OAuth2ClientChannel.cs @@ -11,6 +11,7 @@ namespace DotNetOpenAuth.OAuth2.ChannelElements { using System.Diagnostics.Contracts; using System.Net; using System.Web; + using System.Xml; using DotNetOpenAuth.Messaging; using DotNetOpenAuth.OAuth2.Messages; @@ -50,6 +51,13 @@ namespace DotNetOpenAuth.OAuth2.ChannelElements { public ClientCredentialApplicator ClientCredentialApplicator { get; set; } /// <summary> + /// Gets quotas used when deserializing JSON. + /// </summary> + public XmlDictionaryReaderQuotas JsonReaderQuotas { + get { return this.XmlDictionaryReaderQuotas; } + } + + /// <summary> /// Prepares an HTTP request that carries a given message. /// </summary> /// <param name="request">The message to send.</param> diff --git a/src/DotNetOpenAuth.OAuth2.Client/OAuth2/ClientBase.cs b/src/DotNetOpenAuth.OAuth2.Client/OAuth2/ClientBase.cs index 49d0732..e9c952d 100644 --- a/src/DotNetOpenAuth.OAuth2.Client/OAuth2/ClientBase.cs +++ b/src/DotNetOpenAuth.OAuth2.Client/OAuth2/ClientBase.cs @@ -16,6 +16,8 @@ namespace DotNetOpenAuth.OAuth2 { #endif using System.Security; using System.Text; + using System.Xml; + using DotNetOpenAuth.Messaging; using DotNetOpenAuth.OAuth2.ChannelElements; using DotNetOpenAuth.OAuth2.Messages; @@ -71,6 +73,13 @@ namespace DotNetOpenAuth.OAuth2 { } /// <summary> + /// Gets quotas used when deserializing JSON. + /// </summary> + public XmlDictionaryReaderQuotas JsonReaderQuotas { + get { return this.OAuthChannel.JsonReaderQuotas; } + } + + /// <summary> /// Gets the OAuth client channel. /// </summary> internal IOAuth2ChannelWithClient OAuthChannel { @@ -87,7 +96,20 @@ namespace DotNetOpenAuth.OAuth2 { Requires.NotNull(request, "request"); Requires.NotNullOrEmpty(accessToken, "accessToken"); - OAuthUtilities.AuthorizeWithBearerToken(request, accessToken); + AuthorizeRequest(request.Headers, accessToken); + } + + /// <summary> + /// Adds the necessary HTTP Authorization header to an HTTP request for protected resources + /// so that the Service Provider will allow the request through. + /// </summary> + /// <param name="requestHeaders">The headers on the request for protected resources from the service provider.</param> + /// <param name="accessToken">The access token previously obtained from the Authorization Server.</param> + public static void AuthorizeRequest(WebHeaderCollection requestHeaders, string accessToken) { + Requires.NotNull(requestHeaders, "requestHeaders"); + Requires.NotNullOrEmpty(accessToken, "accessToken"); + + OAuthUtilities.AuthorizeWithBearerToken(requestHeaders, accessToken); } /// <summary> @@ -99,6 +121,19 @@ namespace DotNetOpenAuth.OAuth2 { public void AuthorizeRequest(HttpWebRequest request, IAuthorizationState authorization) { Requires.NotNull(request, "request"); Requires.NotNull(authorization, "authorization"); + + this.AuthorizeRequest(request.Headers, authorization); + } + + /// <summary> + /// Adds the OAuth authorization token to an outgoing HTTP request, renewing a + /// (nearly) expired access token if necessary. + /// </summary> + /// <param name="requestHeaders">The headers on the request for protected resources from the service provider.</param> + /// <param name="authorization">The authorization for this request previously obtained via OAuth.</param> + public void AuthorizeRequest(WebHeaderCollection requestHeaders, IAuthorizationState authorization) { + Requires.NotNull(requestHeaders, "requestHeaders"); + Requires.NotNull(authorization, "authorization"); Requires.True(!string.IsNullOrEmpty(authorization.AccessToken), "authorization"); ErrorUtilities.VerifyProtocol(!authorization.AccessTokenExpirationUtc.HasValue || authorization.AccessTokenExpirationUtc >= DateTime.UtcNow || authorization.RefreshToken != null, ClientStrings.AuthorizationExpired); @@ -107,7 +142,7 @@ namespace DotNetOpenAuth.OAuth2 { this.RefreshAuthorization(authorization); } - AuthorizeRequest(request, authorization.AccessToken); + AuthorizeRequest(requestHeaders, authorization.AccessToken); } #if CLR4 @@ -216,7 +251,7 @@ namespace DotNetOpenAuth.OAuth2 { Requires.NotNull(password, "password"); var request = new AccessTokenResourceOwnerPasswordCredentialsRequest(this.AuthorizationServer.TokenEndpoint, this.AuthorizationServer.Version) { - UserName = userName, + RequestingUserName = userName, Password = password, }; @@ -372,6 +407,7 @@ namespace DotNetOpenAuth.OAuth2 { var failure = response as AccessTokenFailedResponse; ErrorUtilities.VerifyProtocol(success != null || failure != null, MessagingStrings.UnexpectedMessageReceivedOfMany); if (success != null) { + authorizationState.Scope.Clear(); // clear the scope we requested so that the response will repopulate it. UpdateAuthorizationWithResponse(authorizationState, success); } else { // failure Logger.OAuth.Info("Credentials rejected by the Authorization Server."); diff --git a/src/DotNetOpenAuth.OAuth2.Client/OAuth2/WebServerClient.cs b/src/DotNetOpenAuth.OAuth2.Client/OAuth2/WebServerClient.cs index 939d1df..879e4e3 100644 --- a/src/DotNetOpenAuth.OAuth2.Client/OAuth2/WebServerClient.cs +++ b/src/DotNetOpenAuth.OAuth2.Client/OAuth2/WebServerClient.cs @@ -8,10 +8,14 @@ namespace DotNetOpenAuth.OAuth2 { using System; using System.Collections.Generic; using System.Diagnostics.Contracts; + using System.Globalization; using System.Linq; using System.Net; using System.Text; using System.Web; + using System.Web.Security; + + using DotNetOpenAuth.Configuration; using DotNetOpenAuth.Messaging; using DotNetOpenAuth.OAuth2.Messages; @@ -20,6 +24,11 @@ namespace DotNetOpenAuth.OAuth2 { /// </summary> public class WebServerClient : ClientBase { /// <summary> + /// The cookie name for XSRF mitigation during authorization code grant flows. + /// </summary> + private const string XsrfCookieName = "DotNetOpenAuth.WebServerClient.XSRF-Session"; + + /// <summary> /// Initializes a new instance of the <see cref="WebServerClient"/> class. /// </summary> /// <param name="authorizationServer">The authorization server.</param> @@ -100,16 +109,25 @@ namespace DotNetOpenAuth.OAuth2 { // Mitigate XSRF attacks by including a state value that would be unpredictable between users, but // verifiable for the same user/session. // If the host is implementing the authorization tracker though, they're handling this protection themselves. + HttpCookie cookie = null; if (this.AuthorizationTracker == null) { var context = this.Channel.GetHttpContext(); - if (context.Session != null) { - request.ClientState = context.Session.SessionID; - } else { - Logger.OAuth.WarnFormat("No request context discovered, so no client state parameter could be set to mitigate XSRF attacks."); - } + + string xsrfKey = MessagingUtilities.GetNonCryptoRandomDataAsBase64(16); + cookie = new HttpCookie(XsrfCookieName, xsrfKey) { + HttpOnly = true, + Secure = FormsAuthentication.RequireSSL, + ////Expires = DateTime.Now.Add(OAuth2ClientSection.Configuration.MaxAuthorizationTime), // we prefer session cookies to persistent ones + }; + request.ClientState = xsrfKey; + } + + var response = this.Channel.PrepareResponse(request); + if (cookie != null) { + response.Cookies.Add(cookie); } - return this.Channel.PrepareResponse(request); + return response; } /// <summary> @@ -134,12 +152,9 @@ namespace DotNetOpenAuth.OAuth2 { ErrorUtilities.VerifyProtocol(authorizationState != null, ClientStrings.AuthorizationResponseUnexpectedMismatch); } else { var context = this.Channel.GetHttpContext(); - if (context.Session != null) { - ErrorUtilities.VerifyProtocol(string.Equals(response.ClientState, context.Session.SessionID, StringComparison.Ordinal), ClientStrings.AuthorizationResponseUnexpectedMismatch); - } else { - Logger.OAuth.WarnFormat("No request context discovered, so no client state parameter could be checked to mitigate XSRF attacks."); - } + HttpCookie cookie = request.Cookies[XsrfCookieName]; + ErrorUtilities.VerifyProtocol(cookie != null && string.Equals(response.ClientState, cookie.Value, StringComparison.Ordinal), ClientStrings.AuthorizationResponseUnexpectedMismatch); authorizationState = new AuthorizationState { Callback = callback }; } var success = response as EndUserAuthorizationSuccessAuthCodeResponse; diff --git a/src/DotNetOpenAuth.OAuth2.ClientAuthorization/DotNetOpenAuth.OAuth2.ClientAuthorization.csproj b/src/DotNetOpenAuth.OAuth2.ClientAuthorization/DotNetOpenAuth.OAuth2.ClientAuthorization.csproj index 2a08dbf..c3ed569 100644 --- a/src/DotNetOpenAuth.OAuth2.ClientAuthorization/DotNetOpenAuth.OAuth2.ClientAuthorization.csproj +++ b/src/DotNetOpenAuth.OAuth2.ClientAuthorization/DotNetOpenAuth.OAuth2.ClientAuthorization.csproj @@ -34,7 +34,7 @@ <Compile Include="OAuth2\Messages\AccessTokenRefreshRequest.cs" /> <Compile Include="OAuth2\Messages\AccessTokenRequestBase.cs" /> <Compile Include="OAuth2\Messages\AccessTokenResourceOwnerPasswordCredentialsRequest.cs" /> - <Compile Include="OAuth2\Messages\AccessTokenResult.cs" /> + <Compile Include="OAuth2\Messages\IAccessTokenResult.cs" /> <Compile Include="OAuth2\Messages\AccessTokenSuccessResponse.cs" /> <Compile Include="OAuth2\Messages\AuthenticatedClientRequestBase.cs" /> <Compile Include="OAuth2\Messages\EndUserAuthorizationFailedResponse.cs" /> diff --git a/src/DotNetOpenAuth.OAuth2.ClientAuthorization/OAuth2/Messages/AccessTokenRequestBase.cs b/src/DotNetOpenAuth.OAuth2.ClientAuthorization/OAuth2/Messages/AccessTokenRequestBase.cs index e6bbc34..e405a85 100644 --- a/src/DotNetOpenAuth.OAuth2.ClientAuthorization/OAuth2/Messages/AccessTokenRequestBase.cs +++ b/src/DotNetOpenAuth.OAuth2.ClientAuthorization/OAuth2/Messages/AccessTokenRequestBase.cs @@ -45,7 +45,24 @@ namespace DotNetOpenAuth.OAuth2.Messages { /// <summary> /// Gets or sets the result of calling the authorization server host's access token creation method. /// </summary> - AccessTokenResult IAccessTokenRequestInternal.AccessTokenResult { get; set; } + IAccessTokenResult IAccessTokenRequestInternal.AccessTokenResult { get; set; } + + /// <summary> + /// Gets the username of the authorizing user, when applicable. + /// </summary> + /// <value> + /// A non-empty string; or <c>null</c> when no user has authorized this access token. + /// </value> + public virtual string UserName { + get { + IAccessTokenRequestInternal request = this; + if (request.AccessTokenResult != null && request.AccessTokenResult.AccessToken != null) { + return request.AccessTokenResult.AccessToken.User; + } + + return null; + } + } /// <summary> /// Gets the type of the grant. diff --git a/src/DotNetOpenAuth.OAuth2.ClientAuthorization/OAuth2/Messages/AccessTokenResourceOwnerPasswordCredentialsRequest.cs b/src/DotNetOpenAuth.OAuth2.ClientAuthorization/OAuth2/Messages/AccessTokenResourceOwnerPasswordCredentialsRequest.cs index a5d958a..a2cbe90 100644 --- a/src/DotNetOpenAuth.OAuth2.ClientAuthorization/OAuth2/Messages/AccessTokenResourceOwnerPasswordCredentialsRequest.cs +++ b/src/DotNetOpenAuth.OAuth2.ClientAuthorization/OAuth2/Messages/AccessTokenResourceOwnerPasswordCredentialsRequest.cs @@ -51,7 +51,7 @@ namespace DotNetOpenAuth.OAuth2.Messages { /// Gets the name on the account whose data on the resource server is accessible using this authorization. /// </summary> string IAuthorizationDescription.User { - get { return this.UserName; } + get { return this.RequestingUserName; } } /// <summary> @@ -64,6 +64,16 @@ namespace DotNetOpenAuth.OAuth2.Messages { #endregion /// <summary> + /// Gets the username of the authorizing user, when applicable. + /// </summary> + /// <value> + /// A non-empty string; or <c>null</c> when no user has authorized this access token. + /// </value> + public override string UserName { + get { return base.UserName ?? this.RequestingUserName; } + } + + /// <summary> /// Gets the type of the grant. /// </summary> /// <value>The type of the grant.</value> @@ -76,7 +86,7 @@ namespace DotNetOpenAuth.OAuth2.Messages { /// </summary> /// <value>The username on the user's account.</value> [MessagePart(Protocol.username, IsRequired = true)] - internal string UserName { get; set; } + internal string RequestingUserName { get; set; } /// <summary> /// Gets or sets the user's password. diff --git a/src/DotNetOpenAuth.OAuth2.ClientAuthorization/OAuth2/Messages/EndUserAuthorizationImplicitRequest.cs b/src/DotNetOpenAuth.OAuth2.ClientAuthorization/OAuth2/Messages/EndUserAuthorizationImplicitRequest.cs index 4b662cd..6f7ba7d 100644 --- a/src/DotNetOpenAuth.OAuth2.ClientAuthorization/OAuth2/Messages/EndUserAuthorizationImplicitRequest.cs +++ b/src/DotNetOpenAuth.OAuth2.ClientAuthorization/OAuth2/Messages/EndUserAuthorizationImplicitRequest.cs @@ -45,7 +45,24 @@ namespace DotNetOpenAuth.OAuth2.Messages { /// <summary> /// Gets or sets the result of calling the authorization server host's access token creation method. /// </summary> - AccessTokenResult IAccessTokenRequestInternal.AccessTokenResult { get; set; } + IAccessTokenResult IAccessTokenRequestInternal.AccessTokenResult { get; set; } + + /// <summary> + /// Gets the username of the authorizing user, when applicable. + /// </summary> + /// <value> + /// A non-empty string; or <c>null</c> when no user has authorized this access token. + /// </value> + string IAccessTokenRequest.UserName { + get { + IAccessTokenRequestInternal request = this; + if (request.AccessTokenResult != null && request.AccessTokenResult.AccessToken != null) { + return request.AccessTokenResult.AccessToken.User; + } + + return null; + } + } /// <summary> /// Gets a value indicating whether the client requesting the access token has authenticated itself. diff --git a/src/DotNetOpenAuth.OAuth2.ClientAuthorization/OAuth2/Messages/IAccessTokenRequest.cs b/src/DotNetOpenAuth.OAuth2.ClientAuthorization/OAuth2/Messages/IAccessTokenRequest.cs index 65378f9..81acb77 100644 --- a/src/DotNetOpenAuth.OAuth2.ClientAuthorization/OAuth2/Messages/IAccessTokenRequest.cs +++ b/src/DotNetOpenAuth.OAuth2.ClientAuthorization/OAuth2/Messages/IAccessTokenRequest.cs @@ -30,6 +30,12 @@ namespace DotNetOpenAuth.OAuth2.Messages { string ClientIdentifier { get; } /// <summary> + /// Gets the username of the authorizing user, when applicable. + /// </summary> + /// <value>A non-empty string; or <c>null</c> when no user has authorized this access token.</value> + string UserName { get; } + + /// <summary> /// Gets the scope of operations the client is allowed to invoke. /// </summary> HashSet<string> Scope { get; } diff --git a/src/DotNetOpenAuth.OAuth2.ClientAuthorization/OAuth2/Messages/IAccessTokenRequestInternal.cs b/src/DotNetOpenAuth.OAuth2.ClientAuthorization/OAuth2/Messages/IAccessTokenRequestInternal.cs index 44af074..b79d566 100644 --- a/src/DotNetOpenAuth.OAuth2.ClientAuthorization/OAuth2/Messages/IAccessTokenRequestInternal.cs +++ b/src/DotNetOpenAuth.OAuth2.ClientAuthorization/OAuth2/Messages/IAccessTokenRequestInternal.cs @@ -17,6 +17,6 @@ namespace DotNetOpenAuth.OAuth2.Messages { /// <summary> /// Gets or sets the result of calling the authorization server host's access token creation method. /// </summary> - AccessTokenResult AccessTokenResult { get; set; } + IAccessTokenResult AccessTokenResult { get; set; } } } diff --git a/src/DotNetOpenAuth.OAuth2.ClientAuthorization/OAuth2/Messages/IAccessTokenResult.cs b/src/DotNetOpenAuth.OAuth2.ClientAuthorization/OAuth2/Messages/IAccessTokenResult.cs new file mode 100644 index 0000000..1719984 --- /dev/null +++ b/src/DotNetOpenAuth.OAuth2.ClientAuthorization/OAuth2/Messages/IAccessTokenResult.cs @@ -0,0 +1,33 @@ +//----------------------------------------------------------------------- +// <copyright file="IAccessTokenResult.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OAuth2 { + using System; + using System.Collections.Generic; + using System.Linq; + using System.Security.Cryptography; + using System.Text; + + /// <summary> + /// Describes the parameters to be fed into creating a response to an access token request. + /// </summary> + public interface IAccessTokenResult { + /// <summary> + /// Gets or sets a value indicating whether to provide the client with a refresh token, when applicable. + /// </summary> + /// <value>The default value is <c>true</c>.</value> + /// <remarks>> + /// The refresh token will never be provided when this value is false. + /// The refresh token <em>may</em> be provided when this value is true. + /// </remarks> + bool AllowRefreshToken { get; set; } + + /// <summary> + /// Gets the access token. + /// </summary> + AccessToken AccessToken { get; } + } +} diff --git a/src/DotNetOpenAuth.OAuth2.ResourceServer/OAuth2/StandardAccessTokenAnalyzer.cs b/src/DotNetOpenAuth.OAuth2.ResourceServer/OAuth2/StandardAccessTokenAnalyzer.cs index 33b99f9..62436ec 100644 --- a/src/DotNetOpenAuth.OAuth2.ResourceServer/OAuth2/StandardAccessTokenAnalyzer.cs +++ b/src/DotNetOpenAuth.OAuth2.ResourceServer/OAuth2/StandardAccessTokenAnalyzer.cs @@ -53,7 +53,7 @@ namespace DotNetOpenAuth.OAuth2 { var accessTokenFormatter = AccessToken.CreateFormatter(this.AuthorizationServerPublicSigningKey, this.ResourceServerPrivateEncryptionKey); var token = new AccessToken(); try { - accessTokenFormatter.Deserialize(token, message, accessToken, Protocol.access_token); + accessTokenFormatter.Deserialize(token, accessToken, message, Protocol.access_token); } catch (IOException ex) { throw new ProtocolException(ResourceServerStrings.InvalidAccessToken, ex); } diff --git a/src/DotNetOpenAuth.OAuth2/OAuth2/OAuthUtilities.cs b/src/DotNetOpenAuth.OAuth2/OAuth2/OAuthUtilities.cs index 5a4a0d3..f28518f 100644 --- a/src/DotNetOpenAuth.OAuth2/OAuth2/OAuthUtilities.cs +++ b/src/DotNetOpenAuth.OAuth2/OAuth2/OAuthUtilities.cs @@ -136,7 +136,20 @@ namespace DotNetOpenAuth.OAuth2 { Requires.NotNullOrEmpty(accessToken, "accessToken"); ErrorUtilities.VerifyProtocol(accessToken.All(ch => accessTokenAuthorizationHeaderAllowedCharacters.IndexOf(ch) >= 0), OAuthStrings.AccessTokenInvalidForHttpAuthorizationHeader); - request.Headers[HttpRequestHeader.Authorization] = string.Format( + AuthorizeWithBearerToken(request.Headers, accessToken); + } + + /// <summary> + /// Authorizes an HTTP request using an OAuth 2.0 access token in an HTTP Authorization header. + /// </summary> + /// <param name="requestHeaders">The headers on the request for protected resources from the service provider.</param> + /// <param name="accessToken">The access token previously obtained from the Authorization Server.</param> + internal static void AuthorizeWithBearerToken(WebHeaderCollection requestHeaders, string accessToken) { + Requires.NotNull(requestHeaders, "requestHeaders"); + Requires.NotNullOrEmpty(accessToken, "accessToken"); + ErrorUtilities.VerifyProtocol(accessToken.All(ch => accessTokenAuthorizationHeaderAllowedCharacters.IndexOf(ch) >= 0), OAuthStrings.AccessTokenInvalidForHttpAuthorizationHeader); + + requestHeaders[HttpRequestHeader.Authorization] = string.Format( CultureInfo.InvariantCulture, Protocol.BearerHttpAuthorizationHeaderFormat, accessToken); diff --git a/src/DotNetOpenAuth.OpenId.Provider/OpenId/Provider/ProviderAssociationHandleEncoder.cs b/src/DotNetOpenAuth.OpenId.Provider/OpenId/Provider/ProviderAssociationHandleEncoder.cs index 5c39c5e..897b4f8 100644 --- a/src/DotNetOpenAuth.OpenId.Provider/OpenId/Provider/ProviderAssociationHandleEncoder.cs +++ b/src/DotNetOpenAuth.OpenId.Provider/OpenId/Provider/ProviderAssociationHandleEncoder.cs @@ -70,7 +70,7 @@ namespace DotNetOpenAuth.OpenId.Provider { var formatter = AssociationDataBag.CreateFormatter(this.cryptoKeyStore, AssociationHandleEncodingSecretBucket); AssociationDataBag bag = new AssociationDataBag(); try { - formatter.Deserialize(bag, containingMessage, handle, Protocol.Default.openid.assoc_handle); + formatter.Deserialize(bag, handle, containingMessage, Protocol.Default.openid.assoc_handle); } catch (ProtocolException ex) { Logger.OpenId.Error("Rejecting an association because deserialization of the encoded handle failed.", ex); return null; diff --git a/src/DotNetOpenAuth.OpenId.UI/XrdsPublisher.cs b/src/DotNetOpenAuth.OpenId.UI/XrdsPublisher.cs index 5aa3064..87e3a8b 100644 --- a/src/DotNetOpenAuth.OpenId.UI/XrdsPublisher.cs +++ b/src/DotNetOpenAuth.OpenId.UI/XrdsPublisher.cs @@ -198,6 +198,7 @@ namespace DotNetOpenAuth { // client since it isn't interested anyway. // We do NOT simply send a 301 redirect here because that would // alter the Claimed Identifier. + Logger.Yadis.InfoFormat("Transferring request from {0} to {1} to respond to XRDS discovery request.", this.Page.Request.Url.AbsoluteUri, this.XrdsUrl); this.Page.Server.Transfer(this.XrdsUrl); } } diff --git a/src/DotNetOpenAuth.Test/Messaging/MessagingUtilitiesTests.cs b/src/DotNetOpenAuth.Test/Messaging/MessagingUtilitiesTests.cs index 8620b93..cf0f9ca 100644 --- a/src/DotNetOpenAuth.Test/Messaging/MessagingUtilitiesTests.cs +++ b/src/DotNetOpenAuth.Test/Messaging/MessagingUtilitiesTests.cs @@ -220,13 +220,17 @@ namespace DotNetOpenAuth.Test.Messaging { Assert.AreEqual("POST", MessagingUtilities.GetHttpVerb(HttpDeliveryMethods.PostRequest)); Assert.AreEqual("HEAD", MessagingUtilities.GetHttpVerb(HttpDeliveryMethods.HeadRequest)); Assert.AreEqual("DELETE", MessagingUtilities.GetHttpVerb(HttpDeliveryMethods.DeleteRequest)); - Assert.AreEqual("PUT", MessagingUtilities.GetHttpVerb(HttpDeliveryMethods.PutRequest)); + Assert.AreEqual("PUT", MessagingUtilities.GetHttpVerb(HttpDeliveryMethods.PutRequest));
+ Assert.AreEqual("PATCH", MessagingUtilities.GetHttpVerb(HttpDeliveryMethods.PatchRequest));
+ Assert.AreEqual("OPTIONS", MessagingUtilities.GetHttpVerb(HttpDeliveryMethods.OptionsRequest)); Assert.AreEqual("GET", MessagingUtilities.GetHttpVerb(HttpDeliveryMethods.GetRequest | HttpDeliveryMethods.AuthorizationHeaderRequest)); Assert.AreEqual("POST", MessagingUtilities.GetHttpVerb(HttpDeliveryMethods.PostRequest | HttpDeliveryMethods.AuthorizationHeaderRequest)); Assert.AreEqual("HEAD", MessagingUtilities.GetHttpVerb(HttpDeliveryMethods.HeadRequest | HttpDeliveryMethods.AuthorizationHeaderRequest)); Assert.AreEqual("DELETE", MessagingUtilities.GetHttpVerb(HttpDeliveryMethods.DeleteRequest | HttpDeliveryMethods.AuthorizationHeaderRequest)); - Assert.AreEqual("PUT", MessagingUtilities.GetHttpVerb(HttpDeliveryMethods.PutRequest | HttpDeliveryMethods.AuthorizationHeaderRequest)); + Assert.AreEqual("PUT", MessagingUtilities.GetHttpVerb(HttpDeliveryMethods.PutRequest | HttpDeliveryMethods.AuthorizationHeaderRequest));
+ Assert.AreEqual("PATCH", MessagingUtilities.GetHttpVerb(HttpDeliveryMethods.PatchRequest | HttpDeliveryMethods.AuthorizationHeaderRequest));
+ Assert.AreEqual("OPTIONS", MessagingUtilities.GetHttpVerb(HttpDeliveryMethods.OptionsRequest | HttpDeliveryMethods.AuthorizationHeaderRequest)); } /// <summary> @@ -246,7 +250,9 @@ namespace DotNetOpenAuth.Test.Messaging { Assert.AreEqual(HttpDeliveryMethods.PostRequest, MessagingUtilities.GetHttpDeliveryMethod("POST")); Assert.AreEqual(HttpDeliveryMethods.HeadRequest, MessagingUtilities.GetHttpDeliveryMethod("HEAD")); Assert.AreEqual(HttpDeliveryMethods.PutRequest, MessagingUtilities.GetHttpDeliveryMethod("PUT")); - Assert.AreEqual(HttpDeliveryMethods.DeleteRequest, MessagingUtilities.GetHttpDeliveryMethod("DELETE")); + Assert.AreEqual(HttpDeliveryMethods.DeleteRequest, MessagingUtilities.GetHttpDeliveryMethod("DELETE"));
+ Assert.AreEqual(HttpDeliveryMethods.PatchRequest, MessagingUtilities.GetHttpDeliveryMethod("PATCH"));
+ Assert.AreEqual(HttpDeliveryMethods.OptionsRequest, MessagingUtilities.GetHttpDeliveryMethod("OPTIONS")); } /// <summary> 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..2713765 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; @@ -30,12 +32,14 @@ using DotNetOpenAuth.Messaging; /// <param name="messageFactory">The message factory.</param> /// <param name="messageData">The message data.</param> /// <param name="recipient">The recipient.</param> + /// <param name="cookies">Cookies included in the incoming request.</param> internal CoordinatingHttpRequestInfo( 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 +53,9 @@ 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")) { + /// <param name="cookies">Cookies included in the incoming request.</param> + 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/CoordinatingOAuth2ClientChannel.cs b/src/DotNetOpenAuth.Test/Mocks/CoordinatingOAuth2ClientChannel.cs index 52f381d..96091ac 100644 --- a/src/DotNetOpenAuth.Test/Mocks/CoordinatingOAuth2ClientChannel.cs +++ b/src/DotNetOpenAuth.Test/Mocks/CoordinatingOAuth2ClientChannel.cs @@ -29,5 +29,9 @@ namespace DotNetOpenAuth.Test.Mocks { get { return this.wrappedChannel.ClientCredentialApplicator; } set { this.wrappedChannel.ClientCredentialApplicator = value; } } + + public System.Xml.XmlDictionaryReaderQuotas JsonReaderQuotas { + get { return this.XmlDictionaryReaderQuotas; } + } } }
\ No newline at end of file diff --git a/src/DotNetOpenAuth.Test/Mocks/CoordinatingOutgoingWebResponse.cs b/src/DotNetOpenAuth.Test/Mocks/CoordinatingOutgoingWebResponse.cs index 8d2c1e7..90dbd7d 100644 --- a/src/DotNetOpenAuth.Test/Mocks/CoordinatingOutgoingWebResponse.cs +++ b/src/DotNetOpenAuth.Test/Mocks/CoordinatingOutgoingWebResponse.cs @@ -16,16 +16,21 @@ 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) { + /// <param name="sendingChannel">The sending channel.</param> + 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 +40,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/OAuth/ChannelElements/HmacSha1SigningBindingElementTests.cs b/src/DotNetOpenAuth.Test/OAuth/ChannelElements/HmacSha1SigningBindingElementTests.cs index 487ce56..49260eb 100644 --- a/src/DotNetOpenAuth.Test/OAuth/ChannelElements/HmacSha1SigningBindingElementTests.cs +++ b/src/DotNetOpenAuth.Test/OAuth/ChannelElements/HmacSha1SigningBindingElementTests.cs @@ -5,6 +5,9 @@ //----------------------------------------------------------------------- namespace DotNetOpenAuth.Test.OAuth.ChannelElements { + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.Messaging.Reflection; + using DotNetOpenAuth.OAuth; using DotNetOpenAuth.OAuth.ChannelElements; using DotNetOpenAuth.OAuth.Messages; using DotNetOpenAuth.Test.Mocks; @@ -20,5 +23,25 @@ namespace DotNetOpenAuth.Test.OAuth.ChannelElements { hmac.Channel = new TestChannel(this.MessageDescriptions); Assert.AreEqual("kR0LhH8UqylaLfR/esXVVlP4sQI=", hmac.GetSignatureTestHook(message)); } + + [Test] + public void LinkedInInteropTest() { + var endpoint = new MessageReceivingEndpoint("https://api.linkedin.com/v1/people/~:(id,first-name,last-name,headline,industry,summary)", HttpDeliveryMethods.AuthorizationHeaderRequest | HttpDeliveryMethods.GetRequest); + var message = new AccessProtectedResourceRequest(endpoint, Protocol.V10.Version); + message.ConsumerKey = "ub78frzrn0yf"; + message.AccessToken = "852863fd-05da-4d80-a93d-50f64f966de4"; + ((ITamperResistantOAuthMessage)message).ConsumerSecret = "ExJXsYl7Or8OfK98"; + ((ITamperResistantOAuthMessage)message).TokenSecret = "b197333b-470a-43b3-bcd7-49d6d2229c4c"; + var signedMessage = (ITamperResistantOAuthMessage)message; + signedMessage.HttpMethod = "GET"; + signedMessage.SignatureMethod = "HMAC-SHA1"; + MessageDictionary dictionary = this.MessageDescriptions.GetAccessor(message); + dictionary["oauth_timestamp"] = "1353545248"; + dictionary["oauth_nonce"] = "ugEB4bst"; + + var hmac = new HmacSha1SigningBindingElement(); + hmac.Channel = new TestChannel(this.MessageDescriptions); + Assert.That(hmac.GetSignatureTestHook(message), Is.EqualTo("l09yeD9cr4+h1eoUF4WBoGEHrlk=")); + } } } diff --git a/src/DotNetOpenAuth.Test/OAuth2/AuthorizationServerTests.cs b/src/DotNetOpenAuth.Test/OAuth2/AuthorizationServerTests.cs index 3791e28..e8f7172 100644 --- a/src/DotNetOpenAuth.Test/OAuth2/AuthorizationServerTests.cs +++ b/src/DotNetOpenAuth.Test/OAuth2/AuthorizationServerTests.cs @@ -9,8 +9,11 @@ namespace DotNetOpenAuth.Test.OAuth2 { using System.Collections.Generic; using System.Linq; using System.Text; + using System.Threading.Tasks; using DotNetOpenAuth.OAuth2; + using DotNetOpenAuth.OAuth2.ChannelElements; using DotNetOpenAuth.OAuth2.Messages; + using Moq; using NUnit.Framework; /// <summary> @@ -28,8 +31,7 @@ namespace DotNetOpenAuth.Test.OAuth2 { AuthorizationServerMock, new UserAgentClient(AuthorizationServerDescription), client => { - var request = new AccessTokenAuthorizationCodeRequestC(AuthorizationServerDescription) - { ClientIdentifier = ClientId, ClientSecret = ClientSecret, AuthorizationCode = "foo" }; + var request = new AccessTokenAuthorizationCodeRequestC(AuthorizationServerDescription) { ClientIdentifier = ClientId, ClientSecret = ClientSecret, AuthorizationCode = "foo" }; var response = client.Channel.Request<AccessTokenFailedResponse>(request); Assert.That(response.Error, Is.Not.Null.And.Not.Empty); @@ -40,5 +42,182 @@ namespace DotNetOpenAuth.Test.OAuth2 { }); coordinator.Run(); } + + [Test] + public void DecodeRefreshToken() { + var refreshTokenSource = new TaskCompletionSource<string>(); + var coordinator = new OAuth2Coordinator<WebServerClient>( + AuthorizationServerDescription, + AuthorizationServerMock, + 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(); + } + }, + 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(); + } + + [Test] + public void ResourceOwnerScopeOverride() { + var clientRequestedScopes = new[] { "scope1", "scope2" }; + var serverOverriddenScopes = new[] { "scope1", "differentScope" }; + var authServerMock = CreateAuthorizationServerMock(); + authServerMock + .Setup(a => a.CheckAuthorizeResourceOwnerCredentialGrant(ResourceOwnerUsername, ResourceOwnerPassword, It.IsAny<IAccessTokenRequest>())) + .Returns<string, string, IAccessTokenRequest>((un, pw, req) => { + var response = new AutomatedUserAuthorizationCheckResponse(req, true, ResourceOwnerUsername); + response.ApprovedScope.Clear(); + response.ApprovedScope.UnionWith(serverOverriddenScopes); + return response; + }); + var coordinator = new OAuth2Coordinator<WebServerClient>( + AuthorizationServerDescription, + authServerMock.Object, + new WebServerClient(AuthorizationServerDescription), + client => { + var authState = new AuthorizationState(TestScopes) { + Callback = ClientCallback, + }; + var result = client.ExchangeUserCredentialForToken(ResourceOwnerUsername, ResourceOwnerPassword, clientRequestedScopes); + Assert.That(result.Scope, Is.EquivalentTo(serverOverriddenScopes)); + }, + server => { + server.HandleTokenRequest().Respond(); + }); + coordinator.Run(); + } + + [Test] + public void CreateAccessTokenSeesAuthorizingUserResourceOwnerGrant() { + var authServerMock = CreateAuthorizationServerMock(); + authServerMock + .Setup(a => a.CheckAuthorizeResourceOwnerCredentialGrant(ResourceOwnerUsername, ResourceOwnerPassword, It.IsAny<IAccessTokenRequest>())) + .Returns<string, string, IAccessTokenRequest>((un, pw, req) => { + var response = new AutomatedUserAuthorizationCheckResponse(req, true, ResourceOwnerUsername); + Assert.That(req.UserName, Is.EqualTo(ResourceOwnerUsername)); + return response; + }); + var coordinator = new OAuth2Coordinator<WebServerClient>( + AuthorizationServerDescription, + authServerMock.Object, + new WebServerClient(AuthorizationServerDescription), + client => { + var authState = new AuthorizationState(TestScopes) { + Callback = ClientCallback, + }; + var result = client.ExchangeUserCredentialForToken(ResourceOwnerUsername, ResourceOwnerPassword, TestScopes); + Assert.That(result.AccessToken, Is.Not.Null); + }, + server => { + server.HandleTokenRequest().Respond(); + }); + coordinator.Run(); + } + + [Test] + public void CreateAccessTokenSeesAuthorizingUserClientCredentialGrant() { + var authServerMock = CreateAuthorizationServerMock(); + authServerMock + .Setup(a => a.CheckAuthorizeClientCredentialsGrant(It.IsAny<IAccessTokenRequest>())) + .Returns<IAccessTokenRequest>(req => { + Assert.That(req.UserName, Is.Null); + return new AutomatedAuthorizationCheckResponse(req, true); + }); + var coordinator = new OAuth2Coordinator<WebServerClient>( + AuthorizationServerDescription, + authServerMock.Object, + new WebServerClient(AuthorizationServerDescription), + client => { + var authState = new AuthorizationState(TestScopes) { + Callback = ClientCallback, + }; + var result = client.GetClientAccessToken(TestScopes); + Assert.That(result.AccessToken, Is.Not.Null); + }, + server => { + server.HandleTokenRequest().Respond(); + }); + coordinator.Run(); + } + + [Test] + public void CreateAccessTokenSeesAuthorizingUserAuthorizationCodeGrant() { + var authServerMock = CreateAuthorizationServerMock(); + authServerMock + .Setup(a => a.IsAuthorizationValid(It.IsAny<IAuthorizationDescription>())) + .Returns<IAuthorizationDescription>(req => { + Assert.That(req.User, Is.EqualTo(ResourceOwnerUsername)); + return true; + }); + var coordinator = new OAuth2Coordinator<WebServerClient>( + AuthorizationServerDescription, + authServerMock.Object, + new WebServerClient(AuthorizationServerDescription), + client => { + 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(); + }); + coordinator.Run(); + } + + [Test] + public void ClientCredentialScopeOverride() { + var clientRequestedScopes = new[] { "scope1", "scope2" }; + var serverOverriddenScopes = new[] { "scope1", "differentScope" }; + var authServerMock = CreateAuthorizationServerMock(); + authServerMock + .Setup(a => a.CheckAuthorizeClientCredentialsGrant(It.IsAny<IAccessTokenRequest>())) + .Returns<IAccessTokenRequest>(req => { + var response = new AutomatedAuthorizationCheckResponse(req, true); + response.ApprovedScope.Clear(); + response.ApprovedScope.UnionWith(serverOverriddenScopes); + return response; + }); + var coordinator = new OAuth2Coordinator<WebServerClient>( + AuthorizationServerDescription, + authServerMock.Object, + new WebServerClient(AuthorizationServerDescription), + client => { + var authState = new AuthorizationState(TestScopes) { + Callback = ClientCallback, + }; + var result = client.GetClientAccessToken(clientRequestedScopes); + Assert.That(result.Scope, Is.EquivalentTo(serverOverriddenScopes)); + }, + server => { + server.HandleTokenRequest().Respond(); + }); + coordinator.Run(); + } } } diff --git a/src/DotNetOpenAuth.Test/OAuth2/OAuth2TestBase.cs b/src/DotNetOpenAuth.Test/OAuth2/OAuth2TestBase.cs index b9e32fe..395b18c 100644 --- a/src/DotNetOpenAuth.Test/OAuth2/OAuth2TestBase.cs +++ b/src/DotNetOpenAuth.Test/OAuth2/OAuth2TestBase.cs @@ -56,8 +56,9 @@ namespace DotNetOpenAuth.Test.OAuth2 { d => d.ClientIdentifier == ClientId && d.User == ResourceOwnerUsername && MessagingUtilities.AreEquivalent(d.Scope, TestScopes)))).Returns(true); - string canonicalUserName = ResourceOwnerUsername; - authHostMock.Setup(m => m.TryAuthorizeResourceOwnerCredentialGrant(ResourceOwnerUsername, ResourceOwnerPassword, It.IsAny<IAccessTokenRequest>(), out canonicalUserName)).Returns(true); + authHostMock + .Setup(m => m.CheckAuthorizeResourceOwnerCredentialGrant(ResourceOwnerUsername, ResourceOwnerPassword, It.IsAny<IAccessTokenRequest>())) + .Returns<string, string, IAccessTokenRequest>((p1, p2, p3) => new AutomatedUserAuthorizationCheckResponse(p3, true, ResourceOwnerUsername)); authHostMock.Setup(m => m.CreateAccessToken(It.IsAny<IAccessTokenRequest>())).Returns(new AccessTokenResult(new AuthorizationServerAccessToken() { AccessTokenSigningKey = AsymmetricKey })); return authHostMock; } diff --git a/src/DotNetOpenAuth.Test/OAuth2/ResourceServerTests.cs b/src/DotNetOpenAuth.Test/OAuth2/ResourceServerTests.cs index a4d09de..80a8392 100644 --- a/src/DotNetOpenAuth.Test/OAuth2/ResourceServerTests.cs +++ b/src/DotNetOpenAuth.Test/OAuth2/ResourceServerTests.cs @@ -87,8 +87,8 @@ namespace DotNetOpenAuth.Test.OAuth2 { a => a.IsAuthorizationValid(It.Is<IAuthorizationDescription>(d => d.User == null && d.ClientIdentifier == ClientId && MessagingUtilities.AreEquivalent(d.Scope, TestScopes)))) .Returns(true); authServer.Setup( - a => a.TryAuthorizeClientCredentialsGrant(It.Is<IAccessTokenRequest>(d => d.ClientIdentifier == ClientId && MessagingUtilities.AreEquivalent(d.Scope, TestScopes)))) - .Returns(true); + a => a.CheckAuthorizeClientCredentialsGrant(It.Is<IAccessTokenRequest>(d => d.ClientIdentifier == ClientId && MessagingUtilities.AreEquivalent(d.Scope, TestScopes)))) + .Returns<IAccessTokenRequest>(req => new AutomatedAuthorizationCheckResponse(req, true)); var coordinator = new OAuth2Coordinator<WebServerClient>( AuthorizationServerDescription, authServer.Object, diff --git a/src/DotNetOpenAuth.Test/OAuth2/WebServerClientAuthorizeTests.cs b/src/DotNetOpenAuth.Test/OAuth2/WebServerClientAuthorizeTests.cs index 9a9c078..8b8b29c 100644 --- a/src/DotNetOpenAuth.Test/OAuth2/WebServerClientAuthorizeTests.cs +++ b/src/DotNetOpenAuth.Test/OAuth2/WebServerClientAuthorizeTests.cs @@ -84,8 +84,8 @@ namespace DotNetOpenAuth.Test.OAuth2 { a => a.IsAuthorizationValid(It.Is<IAuthorizationDescription>(d => d.User == null && d.ClientIdentifier == ClientId && MessagingUtilities.AreEquivalent(d.Scope, TestScopes)))) .Returns(true); authServer.Setup( - a => a.TryAuthorizeClientCredentialsGrant(It.Is<IAccessTokenRequest>(d => d.ClientIdentifier == ClientId && MessagingUtilities.AreEquivalent(d.Scope, TestScopes)))) - .Returns(true); + a => a.CheckAuthorizeClientCredentialsGrant(It.Is<IAccessTokenRequest>(d => d.ClientIdentifier == ClientId && MessagingUtilities.AreEquivalent(d.Scope, TestScopes)))) + .Returns<IAccessTokenRequest>(req => new AutomatedAuthorizationCheckResponse(req, true)); var coordinator = new OAuth2Coordinator<WebServerClient>( AuthorizationServerDescription, authServer.Object, @@ -102,6 +102,34 @@ namespace DotNetOpenAuth.Test.OAuth2 { } [Test] + public void GetClientAccessTokenReturnsApprovedScope() { + string[] approvedScopes = new[] { "Scope2", "Scope3" }; + var authServer = CreateAuthorizationServerMock(); + authServer.Setup( + a => a.IsAuthorizationValid(It.Is<IAuthorizationDescription>(d => d.User == null && d.ClientIdentifier == ClientId && MessagingUtilities.AreEquivalent(d.Scope, TestScopes)))) + .Returns(true); + authServer.Setup( + a => a.CheckAuthorizeClientCredentialsGrant(It.Is<IAccessTokenRequest>(d => d.ClientIdentifier == ClientId && MessagingUtilities.AreEquivalent(d.Scope, TestScopes)))) + .Returns<IAccessTokenRequest>(req => { + var response = new AutomatedAuthorizationCheckResponse(req, true); + response.ApprovedScope.ResetContents(approvedScopes); + return response; + }); + var coordinator = new OAuth2Coordinator<WebServerClient>( + AuthorizationServerDescription, + authServer.Object, + new WebServerClient(AuthorizationServerDescription), + client => { + var authState = client.GetClientAccessToken(TestScopes); + Assert.That(authState.Scope, Is.EquivalentTo(approvedScopes)); + }, + server => { + server.HandleTokenRequest().Respond(); + }); + coordinator.Run(); + } + + [Test] public void CreateAuthorizingHandlerBearer() { var client = new WebServerClient(AuthorizationServerDescription); string bearerToken = "mytoken"; diff --git a/src/version.txt b/src/version.txt index 53b95c0..95a7f86 100644 --- a/src/version.txt +++ b/src/version.txt @@ -1,3 +1,2 @@ -4.1.5 +4.2.2 -0.25.0-draft5 |