diff options
Diffstat (limited to 'src/DotNetOpenAuth')
29 files changed, 444 insertions, 220 deletions
diff --git a/src/DotNetOpenAuth/GlobalSuppressions.cs b/src/DotNetOpenAuth/GlobalSuppressions.cs index c4c6d22..8be62f8 100644 --- a/src/DotNetOpenAuth/GlobalSuppressions.cs +++ b/src/DotNetOpenAuth/GlobalSuppressions.cs @@ -42,3 +42,4 @@ [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA2210:AssembliesShouldHaveValidStrongNames", Justification = "We sign it when producing drops.")] [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", Target = "DotNetOpenAuth.OpenId.Extensions.OAuth")] [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", Target = "DotNetOpenAuth.OpenId.Extensions.UI")] +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", Target = "DotNetOpenAuth.Messaging.Reflection")] diff --git a/src/DotNetOpenAuth/InfoCard/InfoCardSelector.cs b/src/DotNetOpenAuth/InfoCard/InfoCardSelector.cs index b787300..949c12b 100644 --- a/src/DotNetOpenAuth/InfoCard/InfoCardSelector.cs +++ b/src/DotNetOpenAuth/InfoCard/InfoCardSelector.cs @@ -553,7 +553,7 @@ namespace DotNetOpenAuth.InfoCard { // generate the onclick script for the image string invokeScript = string.Format( CultureInfo.InvariantCulture, - @"if (ActivateSelector('{0}', '{1}')) {{ {2} }}", + @"if (document.infoCard.activate('{0}', '{1}')) {{ {2} }}", this.SelectorObjectId, this.HiddenFieldName, postback); @@ -587,22 +587,18 @@ namespace DotNetOpenAuth.InfoCard { cardSpaceControl.Attributes.Add(HtmlTextWriterAttribute.Type.ToString(), "application/x-informationcard"); cardSpaceControl.Attributes.Add(HtmlTextWriterAttribute.Id.ToString(), this.ClientID + "_cs"); - // issuer - if (this.Issuer != null) { + if (!string.IsNullOrEmpty(this.Issuer)) { cardSpaceControl.Controls.Add(CreateParam("issuer", this.Issuer)); } - // issuer policy if (!string.IsNullOrEmpty(this.IssuerPolicy)) { cardSpaceControl.Controls.Add(CreateParam("issuerPolicy", this.IssuerPolicy)); } - // token type if (!string.IsNullOrEmpty(this.TokenType)) { cardSpaceControl.Controls.Add(CreateParam("tokenType", this.TokenType)); } - // claims string requiredClaims, optionalClaims; this.GetRequestedClaims(out requiredClaims, out optionalClaims); ErrorUtilities.VerifyArgument(!string.IsNullOrEmpty(requiredClaims) || !string.IsNullOrEmpty(optionalClaims), InfoCardStrings.EmptyClaimListNotAllowed); @@ -613,12 +609,10 @@ namespace DotNetOpenAuth.InfoCard { cardSpaceControl.Controls.Add(CreateParam("optionalClaims", optionalClaims)); } - // privacy URL if (!string.IsNullOrEmpty(this.PrivacyUrl)) { cardSpaceControl.Controls.Add(CreateParam("privacyUrl", this.PrivacyUrl)); } - // privacy version if (!string.IsNullOrEmpty(this.PrivacyVersion)) { cardSpaceControl.Controls.Add(CreateParam("privacyVersion", this.PrivacyVersion)); } @@ -668,13 +662,13 @@ namespace DotNetOpenAuth.InfoCard { this.Page.ClientScript.RegisterStartupScript( typeof(InfoCardSelector), "SelectorSupportingScript_" + this.ClientID, - string.Format(CultureInfo.InvariantCulture, "CheckStatic('{0}', '{1}');", this.infoCardSupportedPanel.ClientID, this.infoCardNotSupportedPanel.ClientID), + string.Format(CultureInfo.InvariantCulture, "document.infoCard.checkStatic('{0}', '{1}');", this.infoCardSupportedPanel.ClientID, this.infoCardNotSupportedPanel.ClientID), true); } else if (RenderMode == RenderMode.Dynamic) { this.Page.ClientScript.RegisterStartupScript( typeof(InfoCardSelector), "SelectorSupportingScript_" + this.ClientID, - string.Format(CultureInfo.InvariantCulture, "CheckDynamic('{0}', '{1}');", this.infoCardSupportedPanel.ClientID, this.infoCardNotSupportedPanel.ClientID), + string.Format(CultureInfo.InvariantCulture, "document.infoCard.checkDynamic('{0}', '{1}');", this.infoCardSupportedPanel.ClientID, this.infoCardNotSupportedPanel.ClientID), true); } } diff --git a/src/DotNetOpenAuth/InfoCard/SupportingScript.js b/src/DotNetOpenAuth/InfoCard/SupportingScript.js index 60103f5..ce4b02d 100644 --- a/src/DotNetOpenAuth/InfoCard/SupportingScript.js +++ b/src/DotNetOpenAuth/InfoCard/SupportingScript.js @@ -1,122 +1,122 @@ -function AreCardsSupported() { - /// <summary> - /// Determines if information cards are supported by the - /// browser. - /// </summary> - /// <returns> - /// true-if the browser supports information cards. - ///</returns> +document.infoCard = { + isSupported: function() { + /// <summary> + /// Determines if information cards are supported by the + /// browser. + /// </summary> + /// <returns> + /// true-if the browser supports information cards. + ///</returns> - var IEVer = -1; - if (navigator.appName == 'Microsoft Internet Explorer') { - if (new RegExp("MSIE ([0-9]{1,}[\.0-9]{0,})").exec(navigator.userAgent) != null) { - IEVer = parseFloat(RegExp.$1); + var IEVer = -1; + if (navigator.appName == 'Microsoft Internet Explorer') { + if (new RegExp("MSIE ([0-9]{1,}[\.0-9]{0,})").exec(navigator.userAgent) != null) { + IEVer = parseFloat(RegExp.$1); + } } - } - // Look for IE 7+. - if (IEVer >= 7) { - var embed = document.createElement("object"); - embed.type = "application/x-informationcard"; - return "" + embed.issuerPolicy != "undefined" && embed.isInstalled; - } - - // not IE (any version) - if (IEVer < 0 && navigator.mimeTypes && navigator.mimeTypes.length) { - // check to see if there is a mimeType handler. - x = navigator.mimeTypes['application/x-informationcard']; - if (x && x.enabledPlugin) { - return true; + // Look for IE 7+. + if (IEVer >= 7) { + var embed = document.createElement("object"); + embed.type = "application/x-informationcard"; + return !(embed.issuerPolicy === undefined) && embed.isInstalled; } - // check for the IdentitySelector event handler is there. - if (document.addEventListener) { - var event = document.createEvent("Events"); - event.initEvent("IdentitySelectorAvailable", true, true); - top.dispatchEvent(event); - - if (top.IdentitySelectorAvailable == true) { + // not IE (any version) + if (IEVer < 0 && navigator.mimeTypes && navigator.mimeTypes.length) { + // check to see if there is a mimeType handler. + x = navigator.mimeTypes['application/x-informationcard']; + if (x && x.enabledPlugin) { return true; } - } - } - - return false; -} -function ActivateSelector(selectorId, hiddenFieldName) { - var selector = document.getElementById(selectorId); - var hiddenField = document.getElementsByName(hiddenFieldName)[0]; - try { - hiddenField.value = selector.value; - } catch (e) { - // Selector was canceled - return false; - } - if (hiddenField.value == 'undefined') { - // We're dealing with a bad FireFox selector plugin. - // Just add the control to the form by setting its name property and submit to activate. - selector.name = hiddenFieldName; - hiddenField.parentNode.removeChild(hiddenField); - return true; - } - return true; -}; + // check for the IdentitySelector event handler is there. + if (document.addEventListener) { + var event = document.createEvent("Events"); + event.initEvent("IdentitySelectorAvailable", true, true); + top.dispatchEvent(event); -function HideStatic(divName) { - var div = document.getElementById(divName); - if (div) { - div.style.visibility = 'hidden'; - } -} + if (top.IdentitySelectorAvailable == true) { + return true; + } + } + } + + return false; + }, -function ShowStatic(divName) { - var div = document.getElementById(divName); - if (div) { - div.style.visibility = 'visible'; - } -} + activate: function(selectorId, hiddenFieldName) { + var selector = document.getElementById(selectorId); + var hiddenField = document.getElementsByName(hiddenFieldName)[0]; + try { + hiddenField.value = selector.value; + } catch (e) { + // Selector was canceled + return false; + } + if (hiddenField.value == 'undefined') { // really the string, not === undefined + // We're dealing with a bad FireFox selector plugin. + // Just add the control to the form by setting its name property and submit to activate. + selector.name = hiddenFieldName; + hiddenField.parentNode.removeChild(hiddenField); + return true; + } + return true; + }, -function HideDynamic(divName) { - var div = document.getElementById(divName); - if (div) { - div.style.display = 'none'; - } -} + hideStatic: function (divName) { + var div = document.getElementById(divName); + if (div) { + div.style.visibility = 'hidden'; + } + }, -function ShowDynamic(divName) { - var div = document.getElementById(divName); - if (div) { - div.style.display = ''; - } -} + showStatic: function(divName) { + var div = document.getElementById(divName); + if (div) { + div.style.visibility = 'visible'; + } + }, -function CheckDynamic(controlDiv, unsupportedDiv) { - if (AreCardsSupported()) { - ShowDynamic(controlDiv); - if (unsupportedDiv != '') { - HideDynamic(unsupportedDiv); + hideDynamic: function (divName) { + var div = document.getElementById(divName); + if (div) { + div.style.display = 'none'; } - } - else { - HideDynamic(controlDiv); - if (unsupportedDiv != '') { - ShowDynamic(unsupportedDiv); + }, + + showDynamic: function (divName) { + var div = document.getElementById(divName); + if (div) { + div.style.display = ''; } - } -} + }, -function CheckStatic(controlDiv, unsupportedDiv) { - if (AreCardsSupported()) { - ShowStatic(controlDiv); - if (unsupportedDiv != '') { - HideStatic(unsupportedDiv); + checkDynamic: function (controlDiv, unsupportedDiv) { + if (this.isSupported()) { + this.showDynamic(controlDiv); + if (unsupportedDiv != '') { + this.hideDynamic(unsupportedDiv); + } + } else { + this.hideDynamic(controlDiv); + if (unsupportedDiv != '') { + this.showDynamic(unsupportedDiv); + } } - } - else { - HideStatic(controlDiv); - if (unsupportedDiv != '') { - ShowDynamic(unsupportedDiv); + }, + + checkStatic: function(controlDiv, unsupportedDiv) { + if (this.isSupported()) { + this.showStatic(controlDiv); + if (unsupportedDiv != '') { + this.hideStatic(unsupportedDiv); + } + } else { + this.hideStatic(controlDiv); + if (unsupportedDiv != '') { + this.showDynamic(unsupportedDiv); + } } } -}
\ No newline at end of file +}; diff --git a/src/DotNetOpenAuth/InfoCard/Token/Token.cs b/src/DotNetOpenAuth/InfoCard/Token/Token.cs index f07c555..5c955ed 100644 --- a/src/DotNetOpenAuth/InfoCard/Token/Token.cs +++ b/src/DotNetOpenAuth/InfoCard/Token/Token.cs @@ -107,8 +107,9 @@ namespace DotNetOpenAuth.InfoCard { public string SiteSpecificId { get { Contract.Requires(this.Claims.ContainsKey(ClaimTypes.PPID)); - ErrorUtilities.VerifyOperation(this.Claims.ContainsKey(ClaimTypes.PPID), InfoCardStrings.PpidClaimRequired); - return TokenUtility.CalculateSiteSpecificID(this.Claims[ClaimTypes.PPID]); + string ppidValue; + ErrorUtilities.VerifyOperation(this.Claims.TryGetValue(ClaimTypes.PPID, out ppidValue) && ppidValue != null, InfoCardStrings.PpidClaimRequired); + return TokenUtility.CalculateSiteSpecificID(ppidValue); } } diff --git a/src/DotNetOpenAuth/Messaging/Channel.cs b/src/DotNetOpenAuth/Messaging/Channel.cs index 0813d17..dd6c02c 100644 --- a/src/DotNetOpenAuth/Messaging/Channel.cs +++ b/src/DotNetOpenAuth/Messaging/Channel.cs @@ -447,6 +447,24 @@ namespace DotNetOpenAuth.Messaging { } /// <summary> + /// Checks whether a given HTTP method is expected to include an entity body in its request. + /// </summary> + /// <param name="httpMethod">The HTTP method.</param> + /// <returns><c>true</c> if the HTTP method is supposed to have an entity; <c>false</c> otherwise.</returns> + 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)) { + return false; + } else if (string.Equals(httpMethod, "POST", StringComparison.Ordinal) || + string.Equals(httpMethod, "PUT", StringComparison.Ordinal)) { + return true; + } else { + throw ErrorUtilities.ThrowArgumentNamed("httpMethod", MessagingStrings.UnsupportedHttpVerb, httpMethod); + } + } + + /// <summary> /// Releases unmanaged and - optionally - managed resources /// </summary> /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param> @@ -810,11 +828,31 @@ namespace DotNetOpenAuth.Messaging { HttpWebRequest httpRequest = (HttpWebRequest)WebRequest.Create(requestMessage.Recipient); httpRequest.CachePolicy = this.CachePolicy; httpRequest.Method = "POST"; + this.SendParametersInEntity(httpRequest, fields); + + return httpRequest; + } + + /// <summary> + /// Sends the given parameters in the entity stream of an HTTP request. + /// </summary> + /// <param name="httpRequest">The HTTP request.</param> + /// <param name="fields">The parameters to send.</param> + /// <remarks> + /// This method calls <see cref="HttpWebRequest.GetRequestStream()"/> and closes + /// the request stream, but does not call <see cref="HttpWebRequest.GetResponse"/>. + /// </remarks> + protected void SendParametersInEntity(HttpWebRequest httpRequest, IDictionary<string, string> fields) { + Contract.Requires(httpRequest != null); + Contract.Requires(fields != null); + ErrorUtilities.VerifyArgumentNotNull(httpRequest, "httpRequest"); + ErrorUtilities.VerifyArgumentNotNull(fields, "fields"); + httpRequest.ContentType = "application/x-www-form-urlencoded"; // Setting the content-encoding to "utf-8" causes Google to reply // with a 415 UnsupportedMediaType. But adding it doesn't buy us - // anything specific, so it's disable it until we know how to get it right. + // anything specific, so we disable it until we know how to get it right. ////httpRequest.Headers[HttpRequestHeader.ContentEncoding] = PostEntityEncoding.WebName; string requestBody = MessagingUtilities.CreateQueryString(fields); @@ -831,8 +869,6 @@ namespace DotNetOpenAuth.Messaging { requestStream.Dispose(); } } - - return httpRequest; } /// <summary> @@ -939,7 +975,7 @@ namespace DotNetOpenAuth.Messaging { /// <returns>The properly ordered list of elements.</returns> /// <exception cref="ProtocolException">Thrown when the binding elements are incomplete or inconsistent with each other.</exception> private static IEnumerable<IChannelBindingElement> ValidateAndPrepareBindingElements(IEnumerable<IChannelBindingElement> elements) { - Contract.Requires(elements == null || Contract.ForAll(elements, e => e != null)); + Contract.Requires(elements == null || elements.All(e => e != null)); Contract.Ensures(Contract.Result<IEnumerable<IChannelBindingElement>>() != null); if (elements == null) { return new IChannelBindingElement[0]; diff --git a/src/DotNetOpenAuth/Messaging/ErrorUtilities.cs b/src/DotNetOpenAuth/Messaging/ErrorUtilities.cs index 1c27d6c..d5b2040 100644 --- a/src/DotNetOpenAuth/Messaging/ErrorUtilities.cs +++ b/src/DotNetOpenAuth/Messaging/ErrorUtilities.cs @@ -379,7 +379,10 @@ namespace DotNetOpenAuth.Messaging { [Pure] internal static void VerifyHttpContext() { Contract.Ensures(HttpContext.Current != null); + Contract.Ensures(HttpContext.Current.Request != null); + ErrorUtilities.VerifyOperation(HttpContext.Current != null, MessagingStrings.HttpContextRequired); + ErrorUtilities.VerifyOperation(HttpContext.Current.Request != null, MessagingStrings.HttpContextRequired); } } } diff --git a/src/DotNetOpenAuth/Messaging/HttpRequestInfo.cs b/src/DotNetOpenAuth/Messaging/HttpRequestInfo.cs index e84a097..ecd6d44 100644 --- a/src/DotNetOpenAuth/Messaging/HttpRequestInfo.cs +++ b/src/DotNetOpenAuth/Messaging/HttpRequestInfo.cs @@ -10,6 +10,7 @@ namespace DotNetOpenAuth.Messaging { using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Diagnostics.Contracts; + using System.Globalization; using System.IO; using System.Net; using System.ServiceModel.Channels; @@ -280,6 +281,7 @@ namespace DotNetOpenAuth.Messaging { this.queryStringBeforeRewriting = this.QueryString; } else { // Rewriting detected! Recover the original request URI. + ErrorUtilities.VerifyInternal(this.UrlBeforeRewriting != null, "UrlBeforeRewriting is null, so the query string cannot be determined."); this.queryStringBeforeRewriting = HttpUtility.ParseQueryString(this.UrlBeforeRewriting.Query); } } @@ -336,7 +338,7 @@ namespace DotNetOpenAuth.Messaging { string[] hostAndPort = request.ServerVariables["HTTP_HOST"].Split(new[] { ':' }, 2); publicRequestUri.Host = hostAndPort[0]; if (hostAndPort.Length > 1) { - publicRequestUri.Port = Convert.ToInt32(hostAndPort[1]); + publicRequestUri.Port = Convert.ToInt32(hostAndPort[1], CultureInfo.InvariantCulture); } else { publicRequestUri.Port = publicRequestUri.Scheme == Uri.UriSchemeHttps ? 443 : 80; } diff --git a/src/DotNetOpenAuth/Messaging/MessagingStrings.Designer.cs b/src/DotNetOpenAuth/Messaging/MessagingStrings.Designer.cs index fb8620b..13e83e1 100644 --- a/src/DotNetOpenAuth/Messaging/MessagingStrings.Designer.cs +++ b/src/DotNetOpenAuth/Messaging/MessagingStrings.Designer.cs @@ -1,7 +1,7 @@ //------------------------------------------------------------------------------ // <auto-generated> // This code was generated by a tool. -// Runtime Version:2.0.50727.3521 +// Runtime Version:2.0.50727.4918 // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. @@ -547,6 +547,15 @@ namespace DotNetOpenAuth.Messaging { } /// <summary> + /// Looks up a localized string similar to The HTTP verb '{0}' is unrecognized and unsupported.. + /// </summary> + internal static string UnsupportedHttpVerb { + get { + return ResourceManager.GetString("UnsupportedHttpVerb", resourceCulture); + } + } + + /// <summary> /// Looks up a localized string similar to '{0}' messages cannot be received with HTTP verb '{1}'.. /// </summary> internal static string UnsupportedHttpVerbForMessageType { diff --git a/src/DotNetOpenAuth/Messaging/MessagingStrings.resx b/src/DotNetOpenAuth/Messaging/MessagingStrings.resx index 0a6f6a5..4bc92a7 100644 --- a/src/DotNetOpenAuth/Messaging/MessagingStrings.resx +++ b/src/DotNetOpenAuth/Messaging/MessagingStrings.resx @@ -288,4 +288,7 @@ <data name="UnexpectedHttpStatusCode" xml:space="preserve"> <value>Expected direct response to use HTTP status code {0} but was {1} instead.</value> </data> + <data name="UnsupportedHttpVerb" xml:space="preserve"> + <value>The HTTP verb '{0}' is unrecognized and unsupported.</value> + </data> </root>
\ No newline at end of file diff --git a/src/DotNetOpenAuth/Messaging/MessagingUtilities.cs b/src/DotNetOpenAuth/Messaging/MessagingUtilities.cs index fe1e4e8..10ddba0 100644 --- a/src/DotNetOpenAuth/Messaging/MessagingUtilities.cs +++ b/src/DotNetOpenAuth/Messaging/MessagingUtilities.cs @@ -31,6 +31,11 @@ namespace DotNetOpenAuth.Messaging { internal static readonly RandomNumberGenerator CryptoRandomDataGenerator = new RNGCryptoServiceProvider(); /// <summary> + /// The set of characters that are unreserved in RFC 2396 but are NOT unreserved in RFC 3986. + /// </summary> + private static readonly string[] UriRfc3986CharsToEscape = new[] { "!", "*", "'", "(", ")" }; + + /// <summary> /// A set of escaping mappings that help secure a string from javscript execution. /// </summary> /// <remarks> @@ -445,7 +450,7 @@ namespace DotNetOpenAuth.Messaging { /// <summary> /// Concatenates a list of name-value pairs as key=value&key=value, /// taking care to properly encode each key and value for URL - /// transmission. No ? is prefixed to the string. + /// transmission according to RFC 3986. No ? is prefixed to the string. /// </summary> /// <param name="args">The dictionary of key/values to read from.</param> /// <returns>The formulated querystring style string.</returns> @@ -461,9 +466,9 @@ namespace DotNetOpenAuth.Messaging { foreach (var p in args) { ErrorUtilities.VerifyArgument(!string.IsNullOrEmpty(p.Key), MessagingStrings.UnexpectedNullOrEmptyKey); ErrorUtilities.VerifyArgument(p.Value != null, MessagingStrings.UnexpectedNullValue, p.Key); - sb.Append(HttpUtility.UrlEncode(p.Key)); + sb.Append(EscapeUriDataStringRfc3986(p.Key)); sb.Append('='); - sb.Append(HttpUtility.UrlEncode(p.Value)); + sb.Append(EscapeUriDataStringRfc3986(p.Value)); sb.Append('&'); } sb.Length--; // remove trailing & @@ -689,6 +694,33 @@ namespace DotNetOpenAuth.Messaging { } /// <summary> + /// Escapes a string according to the URI data string rules given in RFC 3986. + /// </summary> + /// <param name="value">The value to escape.</param> + /// <returns>The escaped value.</returns> + /// <remarks> + /// The <see cref="Uri.EscapeDataString"/> method is <i>supposed</i> to take on + /// RFC 3986 behavior if certain elements are present in a .config file. Even if this + /// actually worked (which in my experiments it <i>doesn't</i>), we can't rely on every + /// host actually having this configuration element present. + /// </remarks> + internal static string EscapeUriDataStringRfc3986(string value) { + // Start with RFC 2396 escaping by calling the .NET method to do the work. + // This MAY sometimes exhibit RFC 3986 behavior (according to the documentation). + // If it does, the escaping we do that follows it will be a no-op since the + // characters we search for to replace can't possibly exist in the string. + StringBuilder escaped = new StringBuilder(Uri.EscapeDataString(value)); + + // Upgrade the escaping to RFC 3986, if necessary. + for (int i = 0; i < UriRfc3986CharsToEscape.Length; i++) { + escaped.Replace(UriRfc3986CharsToEscape[i], Uri.HexEscape(UriRfc3986CharsToEscape[i][0])); + } + + // Return the fully-RFC3986-escaped string. + return escaped.ToString(); + } + + /// <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/Messaging/Reflection/IMessagePartEncoder.cs b/src/DotNetOpenAuth/Messaging/Reflection/IMessagePartEncoder.cs index 7da25a5..d2c9596 100644 --- a/src/DotNetOpenAuth/Messaging/Reflection/IMessagePartEncoder.cs +++ b/src/DotNetOpenAuth/Messaging/Reflection/IMessagePartEncoder.cs @@ -16,7 +16,7 @@ namespace DotNetOpenAuth.Messaging.Reflection { /// <remarks> /// Implementations of this interface must include a default constructor and must be thread-safe. /// </remarks> - internal interface IMessagePartEncoder { + public interface IMessagePartEncoder { /// <summary> /// Encodes the specified value. /// </summary> diff --git a/src/DotNetOpenAuth/Messaging/Reflection/MessageDescriptionCollection.cs b/src/DotNetOpenAuth/Messaging/Reflection/MessageDescriptionCollection.cs index 7ac21f2..9af4bdf 100644 --- a/src/DotNetOpenAuth/Messaging/Reflection/MessageDescriptionCollection.cs +++ b/src/DotNetOpenAuth/Messaging/Reflection/MessageDescriptionCollection.cs @@ -7,6 +7,7 @@ namespace DotNetOpenAuth.Messaging.Reflection { using System; using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; using System.Diagnostics.Contracts; /// <summary> @@ -45,11 +46,12 @@ namespace DotNetOpenAuth.Messaging.Reflection { if (!this.reflectedMessageTypes.TryGetValue(key, out result)) { lock (this.reflectedMessageTypes) { if (!this.reflectedMessageTypes.TryGetValue(key, out result)) { - this.reflectedMessageTypes[key] = result = new MessageDescription(key.Type, key.Version); + this.reflectedMessageTypes[key] = result = new MessageDescription(messageType, messageVersion); } } } + Contract.Assume(result != null, "We should never assign null values to this dictionary."); return result; } @@ -112,6 +114,7 @@ namespace DotNetOpenAuth.Messaging.Reflection { /// <summary> /// Gets the message type. /// </summary> + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "Exposes basic identity on the type.")] internal Type Type { get { return this.type; } } @@ -119,6 +122,7 @@ namespace DotNetOpenAuth.Messaging.Reflection { /// <summary> /// Gets the message version. /// </summary> + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "Exposes basic identity on the type.")] internal Version Version { get { return this.version; } } diff --git a/src/DotNetOpenAuth/Messaging/Reflection/MessageDictionary.cs b/src/DotNetOpenAuth/Messaging/Reflection/MessageDictionary.cs index b721b63..0b5b6d0 100644 --- a/src/DotNetOpenAuth/Messaging/Reflection/MessageDictionary.cs +++ b/src/DotNetOpenAuth/Messaging/Reflection/MessageDictionary.cs @@ -95,7 +95,7 @@ namespace DotNetOpenAuth.Messaging.Reflection { } /// <summary> - /// Gets the set of official OAuth keys that have non-null values associated with them. + /// Gets the set of official message part names that have non-null values associated with them. /// </summary> public ICollection<string> DeclaredKeys { get { diff --git a/src/DotNetOpenAuth/Messaging/UntrustedWebRequestHandler.cs b/src/DotNetOpenAuth/Messaging/UntrustedWebRequestHandler.cs index 4adf3a7..733b698 100644 --- a/src/DotNetOpenAuth/Messaging/UntrustedWebRequestHandler.cs +++ b/src/DotNetOpenAuth/Messaging/UntrustedWebRequestHandler.cs @@ -33,11 +33,6 @@ namespace DotNetOpenAuth.Messaging { /// </remarks> public class UntrustedWebRequestHandler : IDirectWebRequestHandler { /// <summary> - /// Gets or sets the default cache policy to use for HTTP requests. - /// </summary> - internal static readonly RequestCachePolicy DefaultCachePolicy = HttpWebRequest.DefaultCachePolicy; - - /// <summary> /// The set of URI schemes allowed in untrusted web requests. /// </summary> private ICollection<string> allowableSchemes = new List<string> { "http", "https" }; diff --git a/src/DotNetOpenAuth/OAuth/ChannelElements/OAuthChannel.cs b/src/DotNetOpenAuth/OAuth/ChannelElements/OAuthChannel.cs index b18b809..dcd4494 100644 --- a/src/DotNetOpenAuth/OAuth/ChannelElements/OAuthChannel.cs +++ b/src/DotNetOpenAuth/OAuth/ChannelElements/OAuthChannel.cs @@ -112,11 +112,13 @@ namespace DotNetOpenAuth.OAuth.ChannelElements { /// a protocol request message. /// </summary> /// <param name="request">The HTTP request to search.</param> - /// <returns>A dictionary of data in the request. Should never be null, but may be empty.</returns> + /// <returns>The deserialized message, if one is found. Null otherwise.</returns> protected override IDirectedProtocolMessage ReadFromRequestCore(HttpRequestInfo request) { ErrorUtilities.VerifyArgumentNotNull(request, "request"); - // First search the Authorization header. Use it exclusively if it's present. + var fields = new Dictionary<string, string>(); + + // First search the Authorization header. string authorization = request.Headers[HttpRequestHeader.Authorization]; if (authorization != null) { string[] authorizationSections = authorization.Split(';'); // TODO: is this the right delimiter? @@ -129,21 +131,28 @@ namespace DotNetOpenAuth.OAuth.ChannelElements { if (trimmedAuth.StartsWith(oauthPrefix, StringComparison.Ordinal)) { // We found an Authorization: OAuth header. // Parse it according to the rules in section 5.4.1 of the V1.0 spec. - var fields = new Dictionary<string, string>(); foreach (string stringPair in trimmedAuth.Substring(oauthPrefix.Length).Split(',')) { string[] keyValueStringPair = stringPair.Trim().Split('='); string key = Uri.UnescapeDataString(keyValueStringPair[0]); string value = Uri.UnescapeDataString(keyValueStringPair[1].Trim('"')); fields.Add(key, value); } - - return (IDirectedProtocolMessage)this.Receive(fields, request.GetRecipient()); } } } - // We didn't find an OAuth authorization header. Revert to other payload methods. - IDirectedProtocolMessage message = base.ReadFromRequestCore(request); + // Scrape the entity + foreach (string key in request.Form) { + fields.Add(key, request.Form[key]); + } + + // Scrape the query string + foreach (string key in request.QueryStringBeforeRewriting) { + fields.Add(key, request.QueryStringBeforeRewriting[key]); + } + + // Deserialize the message using all the data we've collected. + var message = (IDirectedProtocolMessage)this.Receive(fields, request.GetRecipient()); // Add receiving HTTP transport information required for signature generation. var signedMessage = message as ITamperResistantOAuthMessage; @@ -239,19 +248,38 @@ namespace DotNetOpenAuth.OAuth.ChannelElements { ErrorUtilities.VerifyArgumentNotNull(destination, "destination"); foreach (var pair in source) { - var key = Uri.EscapeDataString(pair.Key); - var value = Uri.EscapeDataString(pair.Value); + var key = MessagingUtilities.EscapeUriDataStringRfc3986(pair.Key); + var value = MessagingUtilities.EscapeUriDataStringRfc3986(pair.Value); destination.Add(key, value); } } /// <summary> + /// Gets the HTTP method to use for a message. + /// </summary> + /// <param name="message">The message.</param> + /// <returns>"POST", "GET" or some other similar http verb.</returns> + private static string GetHttpMethod(IDirectedProtocolMessage message) { + Contract.Requires(message != null); + ErrorUtilities.VerifyArgumentNotNull(message, "message"); + + var signedMessage = message as ITamperResistantOAuthMessage; + if (signedMessage != null) { + return signedMessage.HttpMethod; + } else { + return (message.HttpMethods & HttpDeliveryMethods.PostRequest) != 0 ? "POST" : "GET"; + } + } + + /// <summary> /// Prepares to send a request to the Service Provider via the Authorization header. /// </summary> /// <param name="requestMessage">The message to be transmitted to the ServiceProvider.</param> /// <returns>The web request ready to send.</returns> /// <remarks> - /// This method implements OAuth 1.0 section 5.2, item #1 (described in section 5.4). + /// <para>If the message has non-empty ExtraData in it, the request stream is sent to + /// the server automatically. If it is empty, the request stream must be sent by the caller.</para> + /// <para>This method implements OAuth 1.0 section 5.2, item #1 (described in section 5.4).</para> /// </remarks> private HttpWebRequest InitializeRequestAsAuthHeader(IDirectedProtocolMessage requestMessage) { var protocol = Protocol.Lookup(requestMessage.Version); @@ -266,16 +294,22 @@ namespace DotNetOpenAuth.OAuth.ChannelElements { fields.Add("realm", this.Realm.AbsoluteUri); } - UriBuilder builder = new UriBuilder(requestMessage.Recipient); - MessagingUtilities.AppendQueryArgs(builder, requestMessage.ExtraData); - HttpWebRequest httpRequest = (HttpWebRequest)WebRequest.Create(builder.Uri); + HttpWebRequest httpRequest; + UriBuilder recipientBuilder = new UriBuilder(requestMessage.Recipient); + bool hasEntity = HttpMethodHasEntity(GetHttpMethod(requestMessage)); + + if (!hasEntity) { + MessagingUtilities.AppendQueryArgs(recipientBuilder, requestMessage.ExtraData); + } + httpRequest = (HttpWebRequest)WebRequest.Create(recipientBuilder.Uri); + httpRequest.Method = GetHttpMethod(requestMessage); StringBuilder authorization = new StringBuilder(); authorization.Append(protocol.AuthorizationHeaderScheme); authorization.Append(" "); foreach (var pair in fields) { - string key = Uri.EscapeDataString(pair.Key); - string value = Uri.EscapeDataString(pair.Value); + string key = MessagingUtilities.EscapeUriDataStringRfc3986(pair.Key); + string value = MessagingUtilities.EscapeUriDataStringRfc3986(pair.Value); authorization.Append(key); authorization.Append("=\""); authorization.Append(value); @@ -285,6 +319,18 @@ namespace DotNetOpenAuth.OAuth.ChannelElements { httpRequest.Headers.Add(HttpRequestHeader.Authorization, authorization.ToString()); + if (hasEntity) { + // WARNING: We only set up the request stream for the caller if there is + // extra data. If there isn't any extra data, the caller must do this themselves. + if (requestMessage.ExtraData.Count > 0) { + SendParametersInEntity(httpRequest, requestMessage.ExtraData); + } else { + // We'll assume the content length is zero since the caller may not have + // anything. They're responsible to change it when the add the payload if they have one. + httpRequest.ContentLength = 0; + } + } + return httpRequest; } diff --git a/src/DotNetOpenAuth/OAuth/ChannelElements/SigningBindingElementBase.cs b/src/DotNetOpenAuth/OAuth/ChannelElements/SigningBindingElementBase.cs index d5ba346..6991818 100644 --- a/src/DotNetOpenAuth/OAuth/ChannelElements/SigningBindingElementBase.cs +++ b/src/DotNetOpenAuth/OAuth/ChannelElements/SigningBindingElementBase.cs @@ -7,9 +7,11 @@ namespace DotNetOpenAuth.OAuth.ChannelElements { using System; using System.Collections.Generic; + using System.Collections.Specialized; using System.Diagnostics.Contracts; using System.Globalization; using System.Text; + using System.Web; using DotNetOpenAuth.Messaging; using DotNetOpenAuth.Messaging.Bindings; using DotNetOpenAuth.Messaging.Reflection; @@ -164,13 +166,34 @@ namespace DotNetOpenAuth.OAuth.ChannelElements { signatureBaseStringElements.Add(message.HttpMethod.ToUpperInvariant()); + var encodedDictionary = OAuthChannel.GetUriEscapedParameters(messageDictionary); + + // An incoming message will already have included the query and form parameters + // in the message dictionary, but an outgoing message COULD have SOME parameters + // in the query that are not in the message dictionary because they were included + // in the receiving endpoint (the original URL). + // In an outgoing message, the POST entity can only contain parameters if they were + // in the message dictionary, so no need to pull out any parameters from there. + if (message.Recipient.Query != null) { + NameValueCollection nvc = HttpUtility.ParseQueryString(message.Recipient.Query); + foreach (string key in nvc) { + string escapedKey = MessagingUtilities.EscapeUriDataStringRfc3986(key); + string escapedValue = MessagingUtilities.EscapeUriDataStringRfc3986(nvc[key]); + string existingValue; + if (!encodedDictionary.TryGetValue(escapedKey, out existingValue)) { + encodedDictionary.Add(escapedKey, escapedValue); + } else { + ErrorUtilities.VerifyInternal(escapedValue == existingValue, "Somehow we have conflicting values for the '{0}' parameter.", escapedKey); + } + } + } + encodedDictionary.Remove("oauth_signature"); + UriBuilder endpoint = new UriBuilder(message.Recipient); endpoint.Query = null; endpoint.Fragment = null; signatureBaseStringElements.Add(endpoint.Uri.AbsoluteUri); - var encodedDictionary = OAuthChannel.GetUriEscapedParameters(messageDictionary); - encodedDictionary.Remove("oauth_signature"); var sortedKeyValueList = new List<KeyValuePair<string, string>>(encodedDictionary); sortedKeyValueList.Sort(SignatureBaseStringParameterComparer); StringBuilder paramBuilder = new StringBuilder(); @@ -192,7 +215,7 @@ namespace DotNetOpenAuth.OAuth.ChannelElements { signatureBaseString.Append("&"); } - signatureBaseString.Append(Uri.EscapeDataString(element)); + signatureBaseString.Append(MessagingUtilities.EscapeUriDataStringRfc3986(element)); } Logger.Bindings.DebugFormat("Constructed signature base string: {0}", signatureBaseString); @@ -207,11 +230,11 @@ namespace DotNetOpenAuth.OAuth.ChannelElements { protected static string GetConsumerAndTokenSecretString(ITamperResistantOAuthMessage message) { StringBuilder builder = new StringBuilder(); if (!string.IsNullOrEmpty(message.ConsumerSecret)) { - builder.Append(Uri.EscapeDataString(message.ConsumerSecret)); + builder.Append(MessagingUtilities.EscapeUriDataStringRfc3986(message.ConsumerSecret)); } builder.Append("&"); if (!string.IsNullOrEmpty(message.TokenSecret)) { - builder.Append(Uri.EscapeDataString(message.TokenSecret)); + builder.Append(MessagingUtilities.EscapeUriDataStringRfc3986(message.TokenSecret)); } return builder.ToString(); } diff --git a/src/DotNetOpenAuth/OAuth/ConsumerBase.cs b/src/DotNetOpenAuth/OAuth/ConsumerBase.cs index 58f8f60..3abb794 100644 --- a/src/DotNetOpenAuth/OAuth/ConsumerBase.cs +++ b/src/DotNetOpenAuth/OAuth/ConsumerBase.cs @@ -72,13 +72,63 @@ namespace DotNetOpenAuth.OAuth { /// <param name="endpoint">The URL and method on the Service Provider to send the request to.</param> /// <param name="accessToken">The access token that permits access to the protected resource.</param> /// <returns>The initialized WebRequest object.</returns> - public WebRequest PrepareAuthorizedRequest(MessageReceivingEndpoint endpoint, string accessToken) { + public HttpWebRequest PrepareAuthorizedRequest(MessageReceivingEndpoint endpoint, string accessToken) { + Contract.Requires(endpoint != null); + Contract.Requires(!String.IsNullOrEmpty(accessToken)); + ErrorUtilities.VerifyArgumentNotNull(endpoint, "endpoint"); + ErrorUtilities.VerifyNonZeroLength(accessToken, "accessToken"); + + return this.PrepareAuthorizedRequest(endpoint, accessToken, EmptyDictionary<string, string>.Instance); + } + + /// <summary> + /// Creates a web request prepared with OAuth authorization + /// that may be further tailored by adding parameters by the caller. + /// </summary> + /// <param name="endpoint">The URL and method on the Service Provider to send the request to.</param> + /// <param name="accessToken">The access token that permits access to the protected resource.</param> + /// <param name="extraData">Extra parameters to include in the message. Must not be null, but may be empty.</param> + /// <returns>The initialized WebRequest object.</returns> + public HttpWebRequest PrepareAuthorizedRequest(MessageReceivingEndpoint endpoint, string accessToken, IDictionary<string, string> extraData) { + Contract.Requires(endpoint != null); + Contract.Requires(!String.IsNullOrEmpty(accessToken)); + Contract.Requires(extraData != null); + ErrorUtilities.VerifyArgumentNotNull(endpoint, "endpoint"); + ErrorUtilities.VerifyNonZeroLength(accessToken, "accessToken"); + ErrorUtilities.VerifyArgumentNotNull(extraData, "extraData"); + IDirectedProtocolMessage message = this.CreateAuthorizingMessage(endpoint, accessToken); + foreach (var pair in extraData) { + message.ExtraData.Add(pair); + } + HttpWebRequest wr = this.OAuthChannel.InitializeRequest(message); return wr; } /// <summary> + /// Prepares an HTTP request that has OAuth authorization already attached to it. + /// </summary> + /// <param name="message">The OAuth authorization message to attach to the HTTP request.</param> + /// <returns> + /// The HttpWebRequest that can be used to send the HTTP request to the remote service provider. + /// </returns> + /// <remarks> + /// If <see cref="IDirectedProtocolMessage.HttpMethods"/> property on the + /// <paramref name="message"/> has the + /// <see cref="HttpDeliveryMethods.AuthorizationHeaderRequest"/> flag set and + /// <see cref="ITamperResistantOAuthMessage.HttpMethod"/> is set to an HTTP method + /// that includes an entity body, the request stream is automatically sent + /// if and only if the <see cref="IMessage.ExtraData"/> dictionary is non-empty. + /// </remarks> + [SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters", Justification = "Type of parameter forces the method to apply only to specific scenario.")] + public HttpWebRequest PrepareAuthorizedRequest(AccessProtectedResourceRequest message) { + Contract.Requires(message != null); + ErrorUtilities.VerifyArgumentNotNull(message, "message"); + return this.OAuthChannel.InitializeRequest(message); + } + + /// <summary> /// Creates a web request prepared with OAuth authorization /// that may be further tailored by adding parameters by the caller. /// </summary> @@ -105,6 +155,27 @@ namespace DotNetOpenAuth.OAuth { #endregion /// <summary> + /// Creates a web request prepared with OAuth authorization + /// that may be further tailored by adding parameters by the caller. + /// </summary> + /// <param name="endpoint">The URL and method on the Service Provider to send the request to.</param> + /// <param name="accessToken">The access token that permits access to the protected resource.</param> + /// <returns>The initialized WebRequest object.</returns> + protected internal AccessProtectedResourceRequest CreateAuthorizingMessage(MessageReceivingEndpoint endpoint, string accessToken) { + Contract.Requires(endpoint != null); + Contract.Requires(!String.IsNullOrEmpty(accessToken)); + ErrorUtilities.VerifyArgumentNotNull(endpoint, "endpoint"); + ErrorUtilities.VerifyNonZeroLength(accessToken, "accessToken"); + + AccessProtectedResourceRequest message = new AccessProtectedResourceRequest(endpoint) { + AccessToken = accessToken, + ConsumerKey = this.ConsumerKey, + }; + + return message; + } + + /// <summary> /// Prepares an OAuth message that begins an authorization request that will /// redirect the user to the Service Provider to provide that authorization. /// </summary> @@ -139,27 +210,6 @@ namespace DotNetOpenAuth.OAuth { } /// <summary> - /// Creates a web request prepared with OAuth authorization - /// that may be further tailored by adding parameters by the caller. - /// </summary> - /// <param name="endpoint">The URL and method on the Service Provider to send the request to.</param> - /// <param name="accessToken">The access token that permits access to the protected resource.</param> - /// <returns>The initialized WebRequest object.</returns> - protected internal AccessProtectedResourceRequest CreateAuthorizingMessage(MessageReceivingEndpoint endpoint, string accessToken) { - Contract.Requires(endpoint != null); - Contract.Requires(!String.IsNullOrEmpty(accessToken)); - ErrorUtilities.VerifyArgumentNotNull(endpoint, "endpoint"); - ErrorUtilities.VerifyNonZeroLength(accessToken, "accessToken"); - - AccessProtectedResourceRequest message = new AccessProtectedResourceRequest(endpoint) { - AccessToken = accessToken, - ConsumerKey = this.ConsumerKey, - }; - - return message; - } - - /// <summary> /// Exchanges a given request token for access token. /// </summary> /// <param name="requestToken">The request token that the user has authorized.</param> diff --git a/src/DotNetOpenAuth/OAuth/ServiceProvider.cs b/src/DotNetOpenAuth/OAuth/ServiceProvider.cs index 78ee252..886e5b3 100644 --- a/src/DotNetOpenAuth/OAuth/ServiceProvider.cs +++ b/src/DotNetOpenAuth/OAuth/ServiceProvider.cs @@ -271,23 +271,44 @@ namespace DotNetOpenAuth.OAuth { /// <param name="request">The Consumer's original authorization request.</param> /// <returns> /// The message to send to the Consumer using <see cref="Channel"/> if one is necessary. - /// Null if the Consumer did not request a callback. + /// Null if the Consumer did not request a callback as part of the authorization request. /// </returns> [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "Consistent user experience with instance.")] public UserAuthorizationResponse PrepareAuthorizationResponse(UserAuthorizationRequest request) { + Contract.Requires(request != null); ErrorUtilities.VerifyArgumentNotNull(request, "request"); if (request.Callback != null) { - var authorization = new UserAuthorizationResponse(request.Callback) { - RequestToken = request.RequestToken, - }; - return authorization; + return this.PrepareAuthorizationResponse(request, request.Callback); } else { return null; } } /// <summary> + /// Prepares the message to send back to the consumer following proper authorization of + /// a token by an interactive user at the Service Provider's web site. + /// </summary> + /// <param name="request">The Consumer's original authorization request.</param> + /// <param name="callback">The callback URI the consumer has previously registered + /// with this service provider.</param> + /// <returns> + /// The message to send to the Consumer using <see cref="Channel"/>. + /// </returns> + [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "Consistent user experience with instance.")] + public UserAuthorizationResponse PrepareAuthorizationResponse(UserAuthorizationRequest request, Uri callback) { + Contract.Requires(request != null); + Contract.Requires(callback != null); + ErrorUtilities.VerifyArgumentNotNull(request, "request"); + ErrorUtilities.VerifyArgumentNotNull(callback, "callback"); + + var authorization = new UserAuthorizationResponse(request.Callback) { + RequestToken = request.RequestToken, + }; + return authorization; + } + + /// <summary> /// Gets the incoming request to exchange an authorized token for an access token. /// </summary> /// <returns>The incoming request, or null if no OAuth message was attached.</returns> diff --git a/src/DotNetOpenAuth/OpenId/ChannelElements/BackwardCompatibilityBindingElement.cs b/src/DotNetOpenAuth/OpenId/ChannelElements/BackwardCompatibilityBindingElement.cs index f23da51..de9cda9 100644 --- a/src/DotNetOpenAuth/OpenId/ChannelElements/BackwardCompatibilityBindingElement.cs +++ b/src/DotNetOpenAuth/OpenId/ChannelElements/BackwardCompatibilityBindingElement.cs @@ -20,13 +20,13 @@ namespace DotNetOpenAuth.OpenId.ChannelElements { /// The name of the callback parameter that stores the Provider Endpoint URL /// to tack onto the return_to URI. /// </summary> - private const string ProviderEndpointParameterName = "dnoi.op_endpoint"; + private const string ProviderEndpointParameterName = OpenIdUtilities.CustomParameterPrefix + "op_endpoint"; /// <summary> /// The name of the callback parameter that stores the Claimed Identifier /// to tack onto the return_to URI. /// </summary> - private const string ClaimedIdentifierParameterName = "dnoi.claimed_id"; + private const string ClaimedIdentifierParameterName = OpenIdUtilities.CustomParameterPrefix + "claimed_id"; #region IChannelBindingElement Members diff --git a/src/DotNetOpenAuth/OpenId/ChannelElements/ExtensionsBindingElement.cs b/src/DotNetOpenAuth/OpenId/ChannelElements/ExtensionsBindingElement.cs index cdb571b..d9c244f 100644 --- a/src/DotNetOpenAuth/OpenId/ChannelElements/ExtensionsBindingElement.cs +++ b/src/DotNetOpenAuth/OpenId/ChannelElements/ExtensionsBindingElement.cs @@ -23,39 +23,13 @@ namespace DotNetOpenAuth.OpenId.ChannelElements { /// </summary> internal class ExtensionsBindingElement : IChannelBindingElement { /// <summary> - /// The security settings on the Relying Party that is hosting this binding element. - /// </summary> - private RelyingPartySecuritySettings rpSecuritySettings; - - /// <summary> - /// The security settings on the Provider that is hosting this binding element. - /// </summary> - private ProviderSecuritySettings opSecuritySettings; - - /// <summary> - /// Initializes a new instance of the <see cref="ExtensionsBindingElement"/> class. - /// </summary> - /// <param name="extensionFactory">The extension factory.</param> - /// <param name="securitySettings">The security settings to apply.</param> - internal ExtensionsBindingElement(IOpenIdExtensionFactory extensionFactory, RelyingPartySecuritySettings securitySettings) { - ErrorUtilities.VerifyArgumentNotNull(extensionFactory, "extensionFactory"); - ErrorUtilities.VerifyArgumentNotNull(securitySettings, "securitySettings"); - - this.ExtensionFactory = extensionFactory; - this.rpSecuritySettings = securitySettings; - } - - /// <summary> /// Initializes a new instance of the <see cref="ExtensionsBindingElement"/> class. /// </summary> /// <param name="extensionFactory">The extension factory.</param> - /// <param name="securitySettings">The security settings to apply.</param> - internal ExtensionsBindingElement(IOpenIdExtensionFactory extensionFactory, ProviderSecuritySettings securitySettings) { + internal ExtensionsBindingElement(IOpenIdExtensionFactory extensionFactory) { ErrorUtilities.VerifyArgumentNotNull(extensionFactory, "extensionFactory"); - ErrorUtilities.VerifyArgumentNotNull(securitySettings, "securitySettings"); this.ExtensionFactory = extensionFactory; - this.opSecuritySettings = securitySettings; } #region IChannelBindingElement Members diff --git a/src/DotNetOpenAuth/OpenId/ChannelElements/OpenIdChannel.cs b/src/DotNetOpenAuth/OpenId/ChannelElements/OpenIdChannel.cs index c7d4ac7..1efcdf2 100644 --- a/src/DotNetOpenAuth/OpenId/ChannelElements/OpenIdChannel.cs +++ b/src/DotNetOpenAuth/OpenId/ChannelElements/OpenIdChannel.cs @@ -339,9 +339,9 @@ namespace DotNetOpenAuth.OpenId.ChannelElements { var extensionFactory = OpenIdExtensionFactoryAggregator.LoadFromConfiguration(); List<IChannelBindingElement> elements = new List<IChannelBindingElement>(8); + elements.Add(new ExtensionsBindingElement(extensionFactory)); if (isRelyingPartyRole) { elements.Add(new RelyingPartySecurityOptions(rpSecuritySettings)); - elements.Add(new ExtensionsBindingElement(extensionFactory, rpSecuritySettings)); elements.Add(new BackwardCompatibilityBindingElement()); ReturnToNonceBindingElement requestNonceElement = null; @@ -361,8 +361,6 @@ namespace DotNetOpenAuth.OpenId.ChannelElements { ErrorUtilities.VerifyOperation(!rpSecuritySettings.RejectUnsolicitedAssertions || requestNonceElement != null, OpenIdStrings.UnsolicitedAssertionRejectionRequiresNonceStore); } else { - elements.Add(new ExtensionsBindingElement(extensionFactory, opSecuritySettings)); - // Providers must always have a nonce store. ErrorUtilities.VerifyArgumentNotNull(nonceStore, "nonceStore"); } diff --git a/src/DotNetOpenAuth/OpenId/ChannelElements/ReturnToNonceBindingElement.cs b/src/DotNetOpenAuth/OpenId/ChannelElements/ReturnToNonceBindingElement.cs index bd9c28b..9c4c46d 100644 --- a/src/DotNetOpenAuth/OpenId/ChannelElements/ReturnToNonceBindingElement.cs +++ b/src/DotNetOpenAuth/OpenId/ChannelElements/ReturnToNonceBindingElement.cs @@ -50,7 +50,7 @@ namespace DotNetOpenAuth.OpenId.ChannelElements { /// The parameter of the callback parameter we tack onto the return_to URL /// to store the replay-detection nonce. /// </summary> - internal const string NonceParameter = "dnoi.request_nonce"; + internal const string NonceParameter = OpenIdUtilities.CustomParameterPrefix + "request_nonce"; /// <summary> /// The length of the generated nonce's random part. diff --git a/src/DotNetOpenAuth/OpenId/ChannelElements/ReturnToSignatureBindingElement.cs b/src/DotNetOpenAuth/OpenId/ChannelElements/ReturnToSignatureBindingElement.cs index 75d86cf..a760c0d 100644 --- a/src/DotNetOpenAuth/OpenId/ChannelElements/ReturnToSignatureBindingElement.cs +++ b/src/DotNetOpenAuth/OpenId/ChannelElements/ReturnToSignatureBindingElement.cs @@ -33,13 +33,13 @@ namespace DotNetOpenAuth.OpenId.ChannelElements { /// The name of the callback parameter we'll tack onto the return_to value /// to store our signature on the return_to parameter. /// </summary> - private const string ReturnToSignatureParameterName = "dnoi.return_to_sig"; + private const string ReturnToSignatureParameterName = OpenIdUtilities.CustomParameterPrefix + "return_to_sig"; /// <summary> /// The name of the callback parameter we'll tack onto the return_to value /// to store the handle of the association we use to sign the return_to parameter. /// </summary> - private const string ReturnToSignatureHandleParameterName = "dnoi.return_to_sig_handle"; + private const string ReturnToSignatureHandleParameterName = OpenIdUtilities.CustomParameterPrefix + "return_to_sig_handle"; /// <summary> /// The hashing algorithm used to generate the private signature on the return_to parameter. diff --git a/src/DotNetOpenAuth/OpenId/HmacShaAssociation.cs b/src/DotNetOpenAuth/OpenId/HmacShaAssociation.cs index d35dcd1..16d8f74 100644 --- a/src/DotNetOpenAuth/OpenId/HmacShaAssociation.cs +++ b/src/DotNetOpenAuth/OpenId/HmacShaAssociation.cs @@ -8,6 +8,7 @@ namespace DotNetOpenAuth.OpenId { using System; using System.Collections.Generic; using System.Diagnostics; + using System.Globalization; using System.Linq; using System.Security.Cryptography; using DotNetOpenAuth.Messaging; @@ -148,13 +149,19 @@ namespace DotNetOpenAuth.OpenId { ErrorUtilities.VerifyNonZeroLength(associationType, "associationType"); ErrorUtilities.VerifyArgumentNotNull(securitySettings, "securitySettings"); + int secretLength = GetSecretLength(protocol, associationType); + // Generate the handle. It must be unique, and preferably unpredictable, // so we use a time element and a random data element to generate it. string uniq = MessagingUtilities.GetCryptoRandomDataAsBase64(4); - string handle = "{" + associationType + "}{" + DateTime.UtcNow.Ticks + "}{" + uniq + "}"; + string handle = string.Format( + CultureInfo.InvariantCulture, + "{{{0}}}{{{1}}}{{{2}}}", + DateTime.UtcNow.Ticks, + uniq, + secretLength); // Generate the secret that will be used for signing - int secretLength = GetSecretLength(protocol, associationType); byte[] secret = MessagingUtilities.GetCryptoRandomData(secretLength); TimeSpan lifetime; diff --git a/src/DotNetOpenAuth/OpenId/Messages/AssociateRequest.cs b/src/DotNetOpenAuth/OpenId/Messages/AssociateRequest.cs index f9f638a..6a5c0a8 100644 --- a/src/DotNetOpenAuth/OpenId/Messages/AssociateRequest.cs +++ b/src/DotNetOpenAuth/OpenId/Messages/AssociateRequest.cs @@ -40,7 +40,7 @@ namespace DotNetOpenAuth.OpenId.Messages { /// </summary> /// <value>Value: A valid association session type from Section 8.4 (Association Session Types). </value> /// <remarks>Note: Unless using transport layer encryption, "no-encryption" MUST NOT be used. </remarks> - [MessagePart("openid.session_type", IsRequired = true, AllowEmpty = true)] + [MessagePart("openid.session_type", IsRequired = false, AllowEmpty = true)] [MessagePart("openid.session_type", IsRequired = true, AllowEmpty = false, MinVersion = "2.0")] internal string SessionType { get; set; } diff --git a/src/DotNetOpenAuth/OpenId/OpenIdUtilities.cs b/src/DotNetOpenAuth/OpenId/OpenIdUtilities.cs index f09bbe1..3cee968 100644 --- a/src/DotNetOpenAuth/OpenId/OpenIdUtilities.cs +++ b/src/DotNetOpenAuth/OpenId/OpenIdUtilities.cs @@ -24,6 +24,11 @@ namespace DotNetOpenAuth.OpenId { /// </summary> internal static class OpenIdUtilities { /// <summary> + /// The prefix to designate this library's proprietary parameters added to the protocol. + /// </summary> + internal const string CustomParameterPrefix = "dnoa."; + + /// <summary> /// Gets the OpenID protocol instance for the version in a message. /// </summary> /// <param name="message">The message.</param> diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/AuthenticationRequest.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/AuthenticationRequest.cs index d806eeb..b3a9020 100644 --- a/src/DotNetOpenAuth/OpenId/RelyingParty/AuthenticationRequest.cs +++ b/src/DotNetOpenAuth/OpenId/RelyingParty/AuthenticationRequest.cs @@ -24,7 +24,7 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { /// <summary> /// The name of the internal callback parameter to use to store the user-supplied identifier. /// </summary> - internal const string UserSuppliedIdentifierParameterName = "dnoi.userSuppliedIdentifier"; + internal const string UserSuppliedIdentifierParameterName = OpenIdUtilities.CustomParameterPrefix + "userSuppliedIdentifier"; /// <summary> /// The relying party that created this request object. @@ -250,6 +250,7 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { Contract.Requires(userSuppliedIdentifier != null); Contract.Requires(relyingParty != null); Contract.Requires(realm != null); + Contract.Ensures(Contract.Result<IEnumerable<AuthenticationRequest>>() != null); // We have a long data validation and preparation process ErrorUtilities.VerifyArgumentNotNull(userSuppliedIdentifier, "userSuppliedIdentifier"); @@ -323,6 +324,12 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { /// before calling this method. /// </remarks> private static IEnumerable<AuthenticationRequest> CreateInternal(Identifier userSuppliedIdentifier, OpenIdRelyingParty relyingParty, Realm realm, Uri returnToUrl, IEnumerable<ServiceEndpoint> serviceEndpoints, bool createNewAssociationsAsNeeded) { + Contract.Requires(userSuppliedIdentifier != null); + Contract.Requires(relyingParty != null); + Contract.Requires(realm != null); + Contract.Requires(serviceEndpoints != null); + Contract.Ensures(Contract.Result<IEnumerable<AuthenticationRequest>>() != null); + Logger.Yadis.InfoFormat("Performing discovery on user-supplied identifier: {0}", userSuppliedIdentifier); IEnumerable<ServiceEndpoint> endpoints = FilterAndSortEndpoints(serviceEndpoints, relyingParty); diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingParty.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingParty.cs index 0b400da..1c82098 100644 --- a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingParty.cs +++ b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingParty.cs @@ -9,6 +9,7 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { using System.Collections.Generic; using System.Collections.Specialized; using System.ComponentModel; + using System.Diagnostics.CodeAnalysis; using System.Diagnostics.Contracts; using System.Linq; using System.Web; @@ -511,7 +512,7 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { internal static bool IsOpenIdSupportingParameter(string parameterName) { Protocol protocol = Protocol.Default; return parameterName.StartsWith(protocol.openid.Prefix, StringComparison.OrdinalIgnoreCase) - || parameterName.StartsWith("dnoi.", StringComparison.Ordinal); + || parameterName.StartsWith(OpenIdUtilities.CustomParameterPrefix, StringComparison.Ordinal); } /// <summary> @@ -529,6 +530,18 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { return rp; } +#if CONTRACTS_FULL + /// <summary> + /// Verifies conditions that should be true for any valid state of this object. + /// </summary> + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "Called by code contracts.")] + [ContractInvariantMethod] + private void ObjectInvariant() { + Contract.Invariant(this.SecuritySettings != null); + Contract.Invariant(this.Channel != null); + } +#endif + /// <summary> /// Releases unmanaged and - optionally - managed resources /// </summary> diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdTextBox.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdTextBox.cs index 4cccc3d..a24b968 100644 --- a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdTextBox.cs +++ b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdTextBox.cs @@ -982,7 +982,7 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { /// <summary> /// Enables a server control to perform final clean up before it is released from memory. /// </summary> - [SuppressMessage("Microsoft.Design", "CA1063:ImplementIDisposableCorrectly", Justification = "Poor base class implementation mandates that we call its Dispose() method.")] + [SuppressMessage("Microsoft.Design", "CA1063:ImplementIDisposableCorrectly", Justification = "Base class doesn't implement virtual Dispose(bool), so we must call its Dispose() method.")] public sealed override void Dispose() { this.Dispose(true); base.Dispose(); |