diff options
Diffstat (limited to 'src')
14 files changed, 1244 insertions, 387 deletions
diff --git a/src/DotNetOpenAuth/DotNetOpenAuth.csproj b/src/DotNetOpenAuth/DotNetOpenAuth.csproj index 80487fc..4754514 100644 --- a/src/DotNetOpenAuth/DotNetOpenAuth.csproj +++ b/src/DotNetOpenAuth/DotNetOpenAuth.csproj @@ -302,6 +302,8 @@ http://opensource.org/licenses/ms-pl.html <Compile Include="Messaging\Reflection\IMessagePartEncoder.cs" /> <Compile Include="Messaging\Reflection\IMessagePartNullEncoder.cs" /> <Compile Include="Messaging\Reflection\MessageDescriptionCollection.cs" /> + <Compile Include="Mvc\OpenIdHelper.cs" /> + <Compile Include="Mvc\OpenIdAjaxOptions.cs" /> <Compile Include="OAuth\ChannelElements\ICombinedOpenIdProviderTokenManager.cs" /> <Compile Include="OAuth\ChannelElements\IConsumerDescription.cs" /> <Compile Include="OAuth\ChannelElements\IConsumerTokenManager.cs" /> @@ -529,7 +531,9 @@ http://opensource.org/licenses/ms-pl.html <Compile Include="OpenId\RelyingParty\AssociationPreference.cs" /> <Compile Include="OpenId\RelyingParty\AuthenticationRequest.cs" /> <Compile Include="OpenId\RelyingParty\AuthenticationRequestMode.cs" /> + <Compile Include="OpenId\RelyingParty\DuplicateRequestedHostsComparer.cs" /> <Compile Include="OpenId\RelyingParty\IProviderEndpoint.cs" /> + <Compile Include="OpenId\RelyingParty\OpenIdAjaxRelyingParty.cs" /> <Compile Include="OpenId\RelyingParty\SelectorButtonContract.cs" /> <Compile Include="OpenId\RelyingParty\SelectorProviderButton.cs" /> <Compile Include="OpenId\RelyingParty\SelectorOpenIdButton.cs" /> diff --git a/src/DotNetOpenAuth/Messaging/MessagingUtilities.cs b/src/DotNetOpenAuth/Messaging/MessagingUtilities.cs index a52f51b..7367c01 100644 --- a/src/DotNetOpenAuth/Messaging/MessagingUtilities.cs +++ b/src/DotNetOpenAuth/Messaging/MessagingUtilities.cs @@ -176,6 +176,20 @@ namespace DotNetOpenAuth.Messaging { } /// <summary> + /// Flattens the specified sequence of sequences. + /// </summary> + /// <typeparam name="T">The type of element contained in the sequence.</typeparam> + /// <param name="sequence">The sequence of sequences to flatten.</param> + /// <returns>A sequence of the contained items.</returns> + public static IEnumerable<T> Flatten<T>(this IEnumerable<IEnumerable<T>> sequence) { + foreach (IEnumerable<T> subsequence in sequence) { + foreach (T item in subsequence) { + yield return item; + } + } + } + + /// <summary> /// Sends a multipart HTTP POST request (useful for posting files) but doesn't call GetResponse on it. /// </summary> /// <param name="request">The HTTP request.</param> diff --git a/src/DotNetOpenAuth/Messaging/OutgoingWebResponse.cs b/src/DotNetOpenAuth/Messaging/OutgoingWebResponse.cs index cf22bb2..6ccc1cb 100644 --- a/src/DotNetOpenAuth/Messaging/OutgoingWebResponse.cs +++ b/src/DotNetOpenAuth/Messaging/OutgoingWebResponse.cs @@ -183,7 +183,10 @@ namespace DotNetOpenAuth.Messaging { /// would transmit the message that normally would be transmitted via a user agent redirect. /// </summary> /// <param name="channel">The channel to use for encoding.</param> - /// <returns>The URL that would transmit the original message.</returns> + /// <returns> + /// The URL that would transmit the original message. This URL may exceed the normal 2K limit, + /// and should therefore be broken up manually and POSTed as form fields when it exceeds this length. + /// </returns> /// <remarks> /// This is useful for desktop applications that will spawn a user agent to transmit the message /// rather than cause a redirect. diff --git a/src/DotNetOpenAuth/Mvc/OpenIdAjaxOptions.cs b/src/DotNetOpenAuth/Mvc/OpenIdAjaxOptions.cs new file mode 100644 index 0000000..9956966 --- /dev/null +++ b/src/DotNetOpenAuth/Mvc/OpenIdAjaxOptions.cs @@ -0,0 +1,59 @@ +//----------------------------------------------------------------------- +// <copyright file="OpenIdAjaxOptions.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.Mvc { + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + + /// <summary> + /// A set of customizations available for the scripts sent to the browser in AJAX OpenID scenarios. + /// </summary> + public class OpenIdAjaxOptions { + /// <summary> + /// Initializes a new instance of the <see cref="OpenIdAjaxOptions"/> class. + /// </summary> + public OpenIdAjaxOptions() { + this.AssertionHiddenFieldId = "openid_openidAuthData"; + this.ReturnUrlHiddenFieldId = "ReturnUrl"; + } + + /// <summary> + /// Gets or sets the ID of the hidden field that should carry the positive assertion + /// until it is posted to the RP. + /// </summary> + public string AssertionHiddenFieldId { get; set; } + + /// <summary> + /// Gets or sets the ID of the hidden field that should be set with the parent window/frame's URL + /// prior to posting the form with the positive assertion. Useful for jQuery popup dialogs. + /// </summary> + public string ReturnUrlHiddenFieldId { get; set; } + + /// <summary> + /// Gets or sets the index of the form in the document.forms array on the browser that should + /// be submitted when the user is ready to send the positive assertion to the RP. + /// </summary> + public int FormIndex { get; set; } + + /// <summary> + /// Gets or sets the preloaded discovery results. + /// </summary> + public string PreloadedDiscoveryResults { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether to print diagnostic trace messages in the browser. + /// </summary> + public bool ShowDiagnosticTrace { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether to show all the "hidden" iframes that facilitate + /// asynchronous authentication of the user for diagnostic purposes. + /// </summary> + public bool ShowDiagnosticIFrame { get; set; } + } +} diff --git a/src/DotNetOpenAuth/Mvc/OpenIdHelper.cs b/src/DotNetOpenAuth/Mvc/OpenIdHelper.cs new file mode 100644 index 0000000..f276019 --- /dev/null +++ b/src/DotNetOpenAuth/Mvc/OpenIdHelper.cs @@ -0,0 +1,432 @@ +//----------------------------------------------------------------------- +// <copyright file="OpenIdHelper.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.Mvc { + using System; + using System.Collections.Generic; + using System.Diagnostics.Contracts; + using System.Globalization; + using System.IO; + using System.Linq; + using System.Text; + using System.Web; + using System.Web.Mvc; + using System.Web.Routing; + using System.Web.UI; + using DotNetOpenAuth.Configuration; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.OpenId; + using DotNetOpenAuth.OpenId.RelyingParty; + + /// <summary> + /// Methods that generate HTML or Javascript for hosting AJAX OpenID "controls" on + /// ASP.NET MVC web sites. + /// </summary> + public static class OpenIdHelper { + /// <summary> + /// Emits a series of stylesheet import tags to support the AJAX OpenID Selector. + /// </summary> + /// <param name="html">The <see cref="HtmlHelper"/> on the view.</param> + /// <param name="page">The page being rendered.</param> + /// <returns>HTML that should be sent directly to the browser.</returns> + public static string OpenIdSelectorStyles(this HtmlHelper html, Page page) { + Contract.Requires<ArgumentNullException>(html != null); + Contract.Requires<ArgumentNullException>(page != null); + Contract.Ensures(Contract.Result<string>() != null); + + StringWriter result = new StringWriter(); + result.WriteStylesheetLink(page, OpenId.RelyingParty.OpenIdSelector.EmbeddedStylesheetResourceName); + result.WriteStylesheetLink(page, OpenId.RelyingParty.OpenIdAjaxTextBox.EmbeddedStylesheetResourceName); + return result.ToString(); + } + + /// <summary> + /// Emits a series of script import tags and some inline script to support the AJAX OpenID Selector. + /// </summary> + /// <param name="html">The <see cref="HtmlHelper"/> on the view.</param> + /// <param name="page">The page being rendered.</param> + /// <returns>HTML that should be sent directly to the browser.</returns> + public static string OpenIdSelectorScripts(this HtmlHelper html, Page page) { + return OpenIdSelectorScripts(html, page, null, null); + } + + /// <summary> + /// Emits a series of script import tags and some inline script to support the AJAX OpenID Selector. + /// </summary> + /// <param name="html">The <see cref="HtmlHelper"/> on the view.</param> + /// <param name="page">The page being rendered.</param> + /// <param name="selectorOptions">An optional instance of an <see cref="OpenIdSelector"/> control, whose properties have been customized to express how this MVC control should be rendered.</param> + /// <param name="additionalOptions">An optional set of additional script customizations.</param> + /// <returns> + /// HTML that should be sent directly to the browser. + /// </returns> + public static string OpenIdSelectorScripts(this HtmlHelper html, Page page, OpenIdSelector selectorOptions, OpenIdAjaxOptions additionalOptions) { + Contract.Requires<ArgumentNullException>(html != null); + Contract.Requires<ArgumentNullException>(page != null); + Contract.Ensures(Contract.Result<string>() != null); + + if (selectorOptions == null) { + selectorOptions = new OpenId.RelyingParty.OpenIdSelector(); + } + + if (additionalOptions == null) { + additionalOptions = new OpenIdAjaxOptions(); + } + + StringWriter result = new StringWriter(); + + if (additionalOptions.ShowDiagnosticIFrame || additionalOptions.ShowDiagnosticTrace) { + string scriptFormat = @"window.openid_visible_iframe = {0}; // causes the hidden iframe to show up +window.openid_trace = {1}; // causes lots of messages"; + result.WriteScriptBlock(string.Format( + CultureInfo.InvariantCulture, + scriptFormat, + additionalOptions.ShowDiagnosticIFrame ? "true" : "false", + additionalOptions.ShowDiagnosticTrace ? "true" : "false")); + } + var scriptResources = new[] { + OpenIdRelyingPartyControlBase.EmbeddedJavascriptResource, + OpenIdRelyingPartyAjaxControlBase.EmbeddedAjaxJavascriptResource, + OpenId.RelyingParty.OpenIdAjaxTextBox.EmbeddedScriptResourceName, + }; + result.WriteScriptTags(page, scriptResources); + + if (selectorOptions.DownloadYahooUILibrary) { + result.WriteScriptTags(new[] { "https://ajax.googleapis.com/ajax/libs/yui/2.8.0r4/build/yuiloader/yuiloader-min.js" }); + } + + var blockBuilder = new StringWriter(); + if (selectorOptions.DownloadYahooUILibrary) { + blockBuilder.WriteLine(@" try { + if (YAHOO) { + var loader = new YAHOO.util.YUILoader({ + require: ['button', 'menu'], + loadOptional: false, + combine: true + }); + + loader.insert(); + } + } catch (e) { }"); + } + + blockBuilder.WriteLine("window.aspnetapppath = '{0}';", VirtualPathUtility.AppendTrailingSlash(HttpContext.Current.Request.ApplicationPath)); + + // Positive assertions can last no longer than this library is willing to consider them valid, + // and when they come with OP private associations they last no longer than the OP is willing + // to consider them valid. We assume the OP will hold them valid for at least five minutes. + double assertionLifetimeInMilliseconds = Math.Min(TimeSpan.FromMinutes(5).TotalMilliseconds, Math.Min(DotNetOpenAuthSection.Configuration.OpenId.MaxAuthenticationTime.TotalMilliseconds, DotNetOpenAuthSection.Configuration.Messaging.MaximumMessageLifetime.TotalMilliseconds)); + blockBuilder.WriteLine( + "{0} = {1};", + OpenIdRelyingPartyAjaxControlBase.MaxPositiveAssertionLifetimeJsName, + assertionLifetimeInMilliseconds.ToString(CultureInfo.InvariantCulture)); + + if (additionalOptions.PreloadedDiscoveryResults != null) { + blockBuilder.WriteLine(additionalOptions.PreloadedDiscoveryResults); + } + + string discoverUrl = VirtualPathUtility.AppendTrailingSlash(HttpContext.Current.Request.ApplicationPath) + html.RouteCollection["OpenIdDiscover"].GetVirtualPath(html.ViewContext.RequestContext, new RouteValueDictionary(new { identifier = "xxx" })).VirtualPath; + string blockFormat = @" {0} = function (argument, resultFunction, errorCallback) {{ + jQuery.ajax({{ + async: true, + dataType: 'text', + error: function (request, status, error) {{ errorCallback(status, argument); }}, + success: function (result) {{ resultFunction(result, argument); }}, + url: '{1}'.replace('xxx', encodeURIComponent(argument)) + }}); + }};"; + blockBuilder.WriteLine(blockFormat, OpenIdRelyingPartyAjaxControlBase.CallbackJSFunctionAsync, discoverUrl); + + blockFormat = @" window.postLoginAssertion = function (positiveAssertion) {{ + $('#{0}')[0].setAttribute('value', positiveAssertion); + if ($('#{1}')[0] && !$('#{1}')[0].value) {{ // popups have no ReturnUrl predefined, but full page LogOn does. + $('#{1}')[0].setAttribute('value', window.parent.location.href); + }} + document.forms[{2}].submit(); + }};"; + blockBuilder.WriteLine( + blockFormat, + additionalOptions.AssertionHiddenFieldId, + additionalOptions.ReturnUrlHiddenFieldId, + additionalOptions.FormIndex); + + blockFormat = @" $(function () {{ + var box = document.getElementsByName('openid_identifier')[0]; + initAjaxOpenId(box, {0}, {1}, {2}, {3}, {4}, {5}, + null, // js function to invoke on receiving a positive assertion + {6}, {7}, {8}, {9}, {10}, {11}, {12}, {13}, {14}, {15}, {16}, {17}, + false, // auto postback + null); // PostBackEventReference (unused in MVC) + }});"; + blockBuilder.WriteLine( + blockFormat, + MessagingUtilities.GetSafeJavascriptValue(page.ClientScript.GetWebResourceUrl(typeof(OpenIdRelyingPartyControlBase), OpenIdTextBox.EmbeddedLogoResourceName)), + MessagingUtilities.GetSafeJavascriptValue(page.ClientScript.GetWebResourceUrl(typeof(OpenIdRelyingPartyControlBase), OpenId.RelyingParty.OpenIdAjaxTextBox.EmbeddedSpinnerResourceName)), + MessagingUtilities.GetSafeJavascriptValue(page.ClientScript.GetWebResourceUrl(typeof(OpenIdRelyingPartyControlBase), OpenId.RelyingParty.OpenIdAjaxTextBox.EmbeddedLoginSuccessResourceName)), + MessagingUtilities.GetSafeJavascriptValue(page.ClientScript.GetWebResourceUrl(typeof(OpenIdRelyingPartyControlBase), OpenId.RelyingParty.OpenIdAjaxTextBox.EmbeddedLoginFailureResourceName)), + selectorOptions.Throttle, + selectorOptions.Timeout.TotalMilliseconds, + MessagingUtilities.GetSafeJavascriptValue(selectorOptions.TextBox.LogOnText), + MessagingUtilities.GetSafeJavascriptValue(selectorOptions.TextBox.LogOnToolTip), + selectorOptions.TextBox.ShowLogOnPostBackButton ? "true" : "false", + MessagingUtilities.GetSafeJavascriptValue(selectorOptions.TextBox.LogOnPostBackToolTip), + MessagingUtilities.GetSafeJavascriptValue(selectorOptions.TextBox.RetryText), + MessagingUtilities.GetSafeJavascriptValue(selectorOptions.TextBox.RetryToolTip), + MessagingUtilities.GetSafeJavascriptValue(selectorOptions.TextBox.BusyToolTip), + MessagingUtilities.GetSafeJavascriptValue(selectorOptions.TextBox.IdentifierRequiredMessage), + MessagingUtilities.GetSafeJavascriptValue(selectorOptions.TextBox.LogOnInProgressMessage), + MessagingUtilities.GetSafeJavascriptValue(selectorOptions.TextBox.AuthenticationSucceededToolTip), + MessagingUtilities.GetSafeJavascriptValue(selectorOptions.TextBox.AuthenticatedAsToolTip), + MessagingUtilities.GetSafeJavascriptValue(selectorOptions.TextBox.AuthenticationFailedToolTip)); + + result.WriteScriptBlock(blockBuilder.ToString()); + result.WriteScriptTags(page, OpenId.RelyingParty.OpenIdSelector.EmbeddedScriptResourceName); + + Reporting.RecordFeatureUse("MVC " + typeof(OpenIdSelector).Name); + return result.ToString(); + } + + /// <summary> + /// Emits the HTML to render an OpenID Provider button as a part of the overall OpenID Selector UI. + /// </summary> + /// <param name="html">The <see cref="HtmlHelper"/> on the view.</param> + /// <param name="page">The page being rendered.</param> + /// <param name="providerIdentifier">The OP Identifier.</param> + /// <param name="imageUrl">The URL of the image to display on the button.</param> + /// <returns> + /// HTML that should be sent directly to the browser. + /// </returns> + public static string OpenIdSelectorOPButton(this HtmlHelper html, Page page, Identifier providerIdentifier, string imageUrl) { + Contract.Requires<ArgumentNullException>(html != null); + Contract.Requires<ArgumentNullException>(page != null); + Contract.Requires<ArgumentNullException>(providerIdentifier != null); + Contract.Requires<ArgumentException>(!string.IsNullOrEmpty(imageUrl)); + Contract.Ensures(Contract.Result<string>() != null); + + return OpenIdSelectorButton(html, page, providerIdentifier, "OPButton", imageUrl); + } + + /// <summary> + /// Emits the HTML to render a generic OpenID button as a part of the overall OpenID Selector UI, + /// allowing the user to enter their own OpenID. + /// </summary> + /// <param name="html">The <see cref="HtmlHelper"/> on the view.</param> + /// <param name="page">The page being rendered.</param> + /// <param name="imageUrl">The URL of the image to display on the button.</param> + /// <returns> + /// HTML that should be sent directly to the browser. + /// </returns> + public static string OpenIdSelectorOpenIdButton(this HtmlHelper html, Page page, string imageUrl) { + Contract.Requires<ArgumentNullException>(html != null); + Contract.Requires<ArgumentNullException>(page != null); + Contract.Requires<ArgumentException>(!string.IsNullOrEmpty(imageUrl)); + Contract.Ensures(Contract.Result<string>() != null); + + return OpenIdSelectorButton(html, page, "OpenIDButton", "OpenIDButton", imageUrl); + } + + /// <summary> + /// Emits the HTML to render the entire OpenID Selector UI. + /// </summary> + /// <param name="html">The <see cref="HtmlHelper"/> on the view.</param> + /// <param name="page">The page being rendered.</param> + /// <param name="buttons">The buttons to include on the selector.</param> + /// <returns> + /// HTML that should be sent directly to the browser. + /// </returns> + public static string OpenIdSelector(this HtmlHelper html, Page page, params SelectorButton[] buttons) { + Contract.Requires<ArgumentNullException>(html != null); + Contract.Requires<ArgumentNullException>(page != null); + Contract.Requires<ArgumentNullException>(buttons != null); + Contract.Ensures(Contract.Result<string>() != null); + + var writer = new StringWriter(); + var h = new HtmlTextWriter(writer); + + h.AddAttribute(HtmlTextWriterAttribute.Class, "OpenIdProviders"); + h.RenderBeginTag(HtmlTextWriterTag.Ul); + + foreach (SelectorButton button in buttons) { + var op = button as SelectorProviderButton; + if (op != null) { + h.Write(OpenIdSelectorOPButton(html, page, op.OPIdentifier, op.Image)); + continue; + } + + var openid = button as SelectorOpenIdButton; + if (openid != null) { + h.Write(OpenIdSelectorOpenIdButton(html, page, openid.Image)); + continue; + } + + ErrorUtilities.VerifySupported(false, "The {0} button is not yet supported for MVC.", button.GetType().Name); + } + + h.RenderEndTag(); // ul + + if (buttons.OfType<SelectorOpenIdButton>().Any()) { + h.Write(OpenIdAjaxTextBox(html)); + } + + return writer.ToString(); + } + + /// <summary> + /// Emits the HTML to render the <see cref="OpenIdAjaxTextBox"/> control as a part of the overall + /// OpenID Selector UI. + /// </summary> + /// <param name="html">The <see cref="HtmlHelper"/> on the view.</param> + /// <returns> + /// HTML that should be sent directly to the browser. + /// </returns> + public static string OpenIdAjaxTextBox(this HtmlHelper html) { + return @"<div style='display: none' id='OpenIDForm'> + <span class='OpenIdAjaxTextBox' style='display: inline-block; position: relative; font-size: 16px'> + <input name='openid_identifier' id='openid_identifier' size='40' style='padding-left: 18px; border-style: solid; border-width: 1px; border-color: lightgray' /> + </span> + </div>"; + } + + /// <summary> + /// Emits the HTML to render a button as a part of the overall OpenID Selector UI. + /// </summary> + /// <param name="html">The <see cref="HtmlHelper"/> on the view.</param> + /// <param name="page">The page being rendered.</param> + /// <param name="id">The value to assign to the HTML id attribute.</param> + /// <param name="cssClass">The value to assign to the HTML class attribute.</param> + /// <param name="imageUrl">The URL of the image to draw on the button.</param> + /// <returns> + /// HTML that should be sent directly to the browser. + /// </returns> + private static string OpenIdSelectorButton(this HtmlHelper html, Page page, string id, string cssClass, string imageUrl) { + Contract.Requires<ArgumentNullException>(html != null); + Contract.Requires<ArgumentNullException>(page != null); + Contract.Requires<ArgumentNullException>(id != null); + Contract.Requires<ArgumentException>(!string.IsNullOrEmpty(imageUrl)); + Contract.Ensures(Contract.Result<string>() != null); + + var writer = new StringWriter(); + var h = new HtmlTextWriter(writer); + + h.AddAttribute(HtmlTextWriterAttribute.Id, id); + if (!string.IsNullOrEmpty(cssClass)) { + h.AddAttribute(HtmlTextWriterAttribute.Class, cssClass); + } + h.RenderBeginTag(HtmlTextWriterTag.Li); + + h.AddAttribute(HtmlTextWriterAttribute.Href, "#"); + h.RenderBeginTag(HtmlTextWriterTag.A); + + h.RenderBeginTag(HtmlTextWriterTag.Div); + h.RenderBeginTag(HtmlTextWriterTag.Div); + + h.AddAttribute(HtmlTextWriterAttribute.Src, imageUrl); + h.RenderBeginTag(HtmlTextWriterTag.Img); + h.RenderEndTag(); + + h.AddAttribute(HtmlTextWriterAttribute.Src, page.ClientScript.GetWebResourceUrl(typeof(OpenIdSelector), OpenId.RelyingParty.OpenIdAjaxTextBox.EmbeddedLoginSuccessResourceName)); + h.AddAttribute(HtmlTextWriterAttribute.Class, "loginSuccess"); + h.AddAttribute(HtmlTextWriterAttribute.Title, "Authenticated as {0}"); + h.RenderBeginTag(HtmlTextWriterTag.Img); + h.RenderEndTag(); + + h.RenderEndTag(); // div + + h.AddAttribute(HtmlTextWriterAttribute.Class, "ui-widget-overlay"); + h.RenderBeginTag(HtmlTextWriterTag.Div); + h.RenderEndTag(); // div + + h.RenderEndTag(); // div + h.RenderEndTag(); // a + h.RenderEndTag(); // li + + return writer.ToString(); + } + + /// <summary> + /// Emits <script> tags that import a given set of scripts given their URLs. + /// </summary> + /// <param name="writer">The writer to emit the tags to.</param> + /// <param name="scriptUrls">The locations of the scripts to import.</param> + private static void WriteScriptTags(this TextWriter writer, IEnumerable<string> scriptUrls) { + Contract.Requires<ArgumentNullException>(writer != null); + Contract.Requires<ArgumentNullException>(scriptUrls != null); + + foreach (string script in scriptUrls) { + writer.WriteLine("<script type='text/javascript' src='{0}'></script>", script); + } + } + + /// <summary> + /// Writes out script tags that import a script from resources embedded in this assembly. + /// </summary> + /// <param name="writer">The writer to emit the tags to.</param> + /// <param name="page">The page being rendered.</param> + /// <param name="resourceName">Name of the resource.</param> + private static void WriteScriptTags(this TextWriter writer, Page page, string resourceName) { + Contract.Requires<ArgumentNullException>(writer != null); + Contract.Requires<ArgumentNullException>(page != null); + Contract.Requires<ArgumentException>(!string.IsNullOrEmpty(resourceName)); + + WriteScriptTags(writer, page, new[] { resourceName }); + } + + /// <summary> + /// Writes out script tags that import scripts from resources embedded in this assembly. + /// </summary> + /// <param name="writer">The writer to emit the tags to.</param> + /// <param name="page">The page being rendered.</param> + /// <param name="resourceNames">The resource names.</param> + private static void WriteScriptTags(this TextWriter writer, Page page, IEnumerable<string> resourceNames) { + Contract.Requires<ArgumentNullException>(writer != null); + Contract.Requires<ArgumentNullException>(page != null); + Contract.Requires<ArgumentNullException>(resourceNames != null); + + writer.WriteScriptTags(resourceNames.Select(r => page.ClientScript.GetWebResourceUrl(typeof(OpenIdRelyingPartyControlBase), r))); + } + + /// <summary> + /// Writes a given script block, surrounding it with <script> and CDATA tags. + /// </summary> + /// <param name="writer">The writer to emit the tags to.</param> + /// <param name="script">The script to inline on the page.</param> + private static void WriteScriptBlock(this TextWriter writer, string script) { + Contract.Requires<ArgumentNullException>(writer != null); + Contract.Requires<ArgumentException>(!string.IsNullOrEmpty(script)); + + writer.WriteLine("<script type='text/javascript' language='javascript'><!--"); + writer.WriteLine("//<![CDATA["); + writer.WriteLine(script); + writer.WriteLine("//]]>--></script>"); + } + + /// <summary> + /// Writes a given CSS link. + /// </summary> + /// <param name="writer">The writer to emit the tags to.</param> + /// <param name="page">The page being rendered.</param> + /// <param name="resourceName">Name of the resource containing the CSS content.</param> + private static void WriteStylesheetLink(this TextWriter writer, Page page, string resourceName) { + Contract.Requires<ArgumentNullException>(writer != null); + Contract.Requires<ArgumentNullException>(page != null); + Contract.Requires<ArgumentException>(!string.IsNullOrEmpty(resourceName)); + + WriteStylesheetLink(writer, page.ClientScript.GetWebResourceUrl(typeof(OpenIdRelyingPartyAjaxControlBase), resourceName)); + } + + /// <summary> + /// Writes a given CSS link. + /// </summary> + /// <param name="writer">The writer to emit the tags to.</param> + /// <param name="stylesheet">The stylesheet to link in.</param> + private static void WriteStylesheetLink(this TextWriter writer, string stylesheet) { + Contract.Requires<ArgumentNullException>(writer != null); + Contract.Requires<ArgumentException>(!string.IsNullOrEmpty(stylesheet)); + + writer.WriteLine("<link rel='stylesheet' type='text/css' href='{0}' />", stylesheet); + } + } +} diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/DuplicateRequestedHostsComparer.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/DuplicateRequestedHostsComparer.cs new file mode 100644 index 0000000..94eb5ba --- /dev/null +++ b/src/DotNetOpenAuth/OpenId/RelyingParty/DuplicateRequestedHostsComparer.cs @@ -0,0 +1,74 @@ +//----------------------------------------------------------------------- +// <copyright file="DuplicateRequestedHostsComparer.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.RelyingParty { + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + + /// <summary> + /// An authentication request comparer that judges equality solely on the OP endpoint hostname. + /// </summary> + internal class DuplicateRequestedHostsComparer : IEqualityComparer<IAuthenticationRequest> { + /// <summary> + /// The singleton instance of this comparer. + /// </summary> + private static IEqualityComparer<IAuthenticationRequest> instance = new DuplicateRequestedHostsComparer(); + + /// <summary> + /// Prevents a default instance of the <see cref="DuplicateRequestedHostsComparer"/> class from being created. + /// </summary> + private DuplicateRequestedHostsComparer() { + } + + /// <summary> + /// Gets the singleton instance of this comparer. + /// </summary> + internal static IEqualityComparer<IAuthenticationRequest> Instance { + get { return instance; } + } + + #region IEqualityComparer<IAuthenticationRequest> Members + + /// <summary> + /// Determines whether the specified objects are equal. + /// </summary> + /// <param name="x">The first object to compare.</param> + /// <param name="y">The second object to compare.</param> + /// <returns> + /// true if the specified objects are equal; otherwise, false. + /// </returns> + public bool Equals(IAuthenticationRequest x, IAuthenticationRequest y) { + if (x == null && y == null) { + return true; + } + + if (x == null || y == null) { + return false; + } + + // We'll distinguish based on the host name only, which + // admittedly is only a heuristic, but if we remove one that really wasn't a duplicate, well, + // this multiple OP attempt thing was just a convenience feature anyway. + return string.Equals(x.Provider.Uri.Host, y.Provider.Uri.Host, StringComparison.OrdinalIgnoreCase); + } + + /// <summary> + /// Returns a hash code for the specified object. + /// </summary> + /// <param name="obj">The <see cref="T:System.Object"/> for which a hash code is to be returned.</param> + /// <returns>A hash code for the specified object.</returns> + /// <exception cref="T:System.ArgumentNullException"> + /// The type of <paramref name="obj"/> is a reference type and <paramref name="obj"/> is null. + /// </exception> + public int GetHashCode(IAuthenticationRequest obj) { + return obj.Provider.Uri.Host.GetHashCode(); + } + + #endregion + } +} diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdAjaxRelyingParty.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdAjaxRelyingParty.cs new file mode 100644 index 0000000..bdf4df5 --- /dev/null +++ b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdAjaxRelyingParty.cs @@ -0,0 +1,238 @@ +//----------------------------------------------------------------------- +// <copyright file="OpenIdAjaxRelyingParty.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.RelyingParty { + using System; + using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; + using System.Diagnostics.Contracts; + using System.Globalization; + using System.Linq; + using System.Net; + using System.Net.Mime; + using System.Text; + using System.Web; + using System.Web.Script.Serialization; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.OpenId.Extensions; + using DotNetOpenAuth.OpenId.Extensions.UI; + + /// <summary> + /// Provides the programmatic facilities to act as an AJAX-enabled OpenID relying party. + /// </summary> + public class OpenIdAjaxRelyingParty : OpenIdRelyingParty { + /// <summary> + /// Initializes a new instance of the <see cref="OpenIdAjaxRelyingParty"/> class. + /// </summary> + public OpenIdAjaxRelyingParty() { + Reporting.RecordFeatureUse(this); + } + + /// <summary> + /// Initializes a new instance of the <see cref="OpenIdAjaxRelyingParty"/> class. + /// </summary> + /// <param name="applicationStore">The application store. If <c>null</c>, the relying party will always operate in "dumb mode".</param> + public OpenIdAjaxRelyingParty(IRelyingPartyApplicationStore applicationStore) + : base(applicationStore) { + Reporting.RecordFeatureUse(this); + } + + /// <summary> + /// Generates AJAX-ready authentication requests that can satisfy the requirements of some OpenID Identifier. + /// </summary> + /// <param name="userSuppliedIdentifier">The Identifier supplied by the user. This may be a URL, an XRI or i-name.</param> + /// <param name="realm">The shorest URL that describes this relying party web site's address. + /// For example, if your login page is found at https://www.example.com/login.aspx, + /// your realm would typically be https://www.example.com/.</param> + /// <param name="returnToUrl">The URL of the login page, or the page prepared to receive authentication + /// responses from the OpenID Provider.</param> + /// <returns> + /// A sequence of authentication requests, any of which constitutes a valid identity assertion on the Claimed Identifier. + /// Never null, but may be empty. + /// </returns> + /// <remarks> + /// <para>Any individual generated request can satisfy the authentication. + /// The generated requests are sorted in preferred order. + /// Each request is generated as it is enumerated to. Associations are created only as + /// <see cref="IAuthenticationRequest.RedirectingResponse"/> is called.</para> + /// <para>No exception is thrown if no OpenID endpoints were discovered. + /// An empty enumerable is returned instead.</para> + /// </remarks> + public override IEnumerable<IAuthenticationRequest> CreateRequests(Identifier userSuppliedIdentifier, Realm realm, Uri returnToUrl) { + var requests = base.CreateRequests(userSuppliedIdentifier, realm, returnToUrl); + + // Alter the requests so that have AJAX characteristics. + // Some OPs may be listed multiple times (one with HTTPS and the other with HTTP, for example). + // Since we're gathering OPs to try one after the other, just take the first choice of each OP + // and don't try it multiple times. + requests = requests.Distinct(DuplicateRequestedHostsComparer.Instance); + + // Configure each generated request. + int reqIndex = 0; + foreach (var req in requests) { + // Inform ourselves in return_to that we're in a popup. + req.SetUntrustedCallbackArgument(OpenIdRelyingPartyControlBase.UIPopupCallbackKey, "1"); + + if (req.DiscoveryResult.IsExtensionSupported<UIRequest>()) { + // Inform the OP that we'll be using a popup window consistent with the UI extension. + req.AddExtension(new UIRequest()); + + // Provide a hint for the client javascript about whether the OP supports the UI extension. + // This is so the window can be made the correct size for the extension. + // If the OP doesn't advertise support for the extension, the javascript will use + // a bigger popup window. + req.SetUntrustedCallbackArgument(OpenIdRelyingPartyControlBase.PopupUISupportedJSHint, "1"); + } + + req.SetUntrustedCallbackArgument("index", (reqIndex++).ToString(CultureInfo.InvariantCulture)); + + // If the ReturnToUrl was explicitly set, we'll need to reset our first parameter + if (string.IsNullOrEmpty(HttpUtility.ParseQueryString(req.ReturnToUrl.Query)[AuthenticationRequest.UserSuppliedIdentifierParameterName])) { + req.SetUntrustedCallbackArgument(AuthenticationRequest.UserSuppliedIdentifierParameterName, userSuppliedIdentifier.OriginalString); + } + + // Our javascript needs to let the user know which endpoint responded. So we force it here. + // This gives us the info even for 1.0 OPs and 2.0 setup_required responses. + req.SetUntrustedCallbackArgument(OpenIdRelyingPartyAjaxControlBase.OPEndpointParameterName, req.Provider.Uri.AbsoluteUri); + req.SetUntrustedCallbackArgument(OpenIdRelyingPartyAjaxControlBase.ClaimedIdParameterName, (string)req.ClaimedIdentifier ?? string.Empty); + + // Inform ourselves in return_to that we're in a popup or iframe. + req.SetUntrustedCallbackArgument(OpenIdRelyingPartyAjaxControlBase.UIPopupCallbackKey, "1"); + + // We append a # at the end so that if the OP happens to support it, + // the OpenID response "query string" is appended after the hash rather than before, resulting in the + // browser being super-speedy in closing the popup window since it doesn't try to pull a newer version + // of the static resource down from the server merely because of a changed URL. + // http://www.nabble.com/Re:-Defining-how-OpenID-should-behave-with-fragments-in-the-return_to-url-p22694227.html + ////TODO: + + yield return req; + } + } + + /// <summary> + /// Serializes discovery results on some <i>single</i> identifier on behalf of Javascript running on the browser. + /// </summary> + /// <param name="requests">The discovery results from just <i>one</i> identifier to serialize as a JSON response.</param> + /// <returns> + /// The JSON result to return to the user agent. + /// </returns> + /// <remarks> + /// We prepare a JSON object with this interface: + /// <code> + /// class jsonResponse { + /// string claimedIdentifier; + /// Array requests; // never null + /// string error; // null if no error + /// } + /// </code> + /// Each element in the requests array looks like this: + /// <code> + /// class jsonAuthRequest { + /// string endpoint; // URL to the OP endpoint + /// string immediate; // URL to initiate an immediate request + /// string setup; // URL to initiate a setup request. + /// } + /// </code> + /// </remarks> + public OutgoingWebResponse AsAjaxDiscoveryResult(IEnumerable<IAuthenticationRequest> requests) { + Contract.Requires<ArgumentNullException>(requests != null); + + var serializer = new JavaScriptSerializer(); + return new OutgoingWebResponse { + Body = serializer.Serialize(this.AsJsonDiscoveryResult(requests)), + }; + } + + /// <summary> + /// Serializes discovery on a set of identifiers for preloading into an HTML page that carries + /// an AJAX-aware OpenID control. + /// </summary> + /// <param name="requests">The discovery results to serialize as a JSON response.</param> + /// <returns> + /// The JSON result to return to the user agent. + /// </returns> + public string AsAjaxPreloadedDiscoveryResult(IEnumerable<IAuthenticationRequest> requests) { + Contract.Requires<ArgumentNullException>(requests != null); + + var serializer = new JavaScriptSerializer(); + string json = serializer.Serialize(this.AsJsonPreloadedDiscoveryResult(requests)); + + string script = "window.dnoa_internal.loadPreloadedDiscoveryResults(" + json + ");"; + return script; + } + + /// <summary> + /// Converts a sequence of authentication requests to a JSON object for seeding an AJAX-enabled login page. + /// </summary> + /// <param name="requests">The discovery results from just <i>one</i> identifier to serialize as a JSON response.</param> + /// <returns>A JSON object, not yet serialized.</returns> + internal object AsJsonDiscoveryResult(IEnumerable<IAuthenticationRequest> requests) { + Contract.Requires<ArgumentNullException>(requests != null); + + requests = requests.CacheGeneratedResults(); + + if (requests.Any()) { + return new { + claimedIdentifier = requests.First().ClaimedIdentifier, + requests = requests.Select(req => new { + endpoint = req.Provider.Uri.AbsoluteUri, + immediate = this.GetRedirectUrl(req, true), + setup = this.GetRedirectUrl(req, false), + }).ToArray() + }; + } else { + return new { + requests = new object[0], + error = OpenIdStrings.OpenIdEndpointNotFound, + }; + } + } + + /// <summary> + /// Serializes discovery on a set of identifiers for preloading into an HTML page that carries + /// an AJAX-aware OpenID control. + /// </summary> + /// <param name="requests">The discovery results to serialize as a JSON response.</param> + /// <returns> + /// A JSON object, not yet serialized to a string. + /// </returns> + private object AsJsonPreloadedDiscoveryResult(IEnumerable<IAuthenticationRequest> requests) { + Contract.Requires<ArgumentNullException>(requests != null); + + // We prepare a JSON object with this interface: + // Array discoveryWrappers; + // Where each element in the above array has this interface: + // class discoveryWrapper { + // string userSuppliedIdentifier; + // jsonResponse discoveryResult; // contains result of call to SerializeDiscoveryAsJson(Identifier) + // } + var json = (from request in requests + group request by request.DiscoveryResult.UserSuppliedIdentifier into requestsByIdentifier + select new { + userSuppliedIdentifier = requestsByIdentifier.Key.ToString(), + discoveryResult = this.AsJsonDiscoveryResult(requestsByIdentifier), + }).ToArray(); + + return json; + } + + /// <summary> + /// Gets the full URL that carries an OpenID message, even if it exceeds the normal maximum size of a URL, + /// for purposes of sending to an AJAX component running in the browser. + /// </summary> + /// <param name="request">The authentication request.</param> + /// <param name="immediate"><c>true</c>to create a checkid_immediate request; + /// <c>false</c> to create a checkid_setup request.</param> + /// <returns>The absolute URL that carries the entire OpenID message.</returns> + private Uri GetRedirectUrl(IAuthenticationRequest request, bool immediate) { + Contract.Requires<ArgumentNullException>(request != null); + + request.Mode = immediate ? AuthenticationRequestMode.Immediate : AuthenticationRequestMode.Setup; + return request.RedirectingResponse.GetDirectUriRequest(this.Channel); + } + } +} diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdAjaxTextBox.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdAjaxTextBox.cs index 097d065..d80bf6a 100644 --- a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdAjaxTextBox.cs +++ b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdAjaxTextBox.cs @@ -64,6 +64,11 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { /// </summary> internal const bool DownloadYahooUILibraryDefault = true; + /// <summary> + /// The default value for the <see cref="Throttle"/> property. + /// </summary> + internal const int ThrottleDefault = 3; + #region Property viewstate keys /// <summary> @@ -221,11 +226,6 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { private const string AuthenticationFailedToolTipDefault = "Authentication failed."; /// <summary> - /// The default value for the <see cref="Throttle"/> property. - /// </summary> - private const int ThrottleDefault = 3; - - /// <summary> /// The default value for the <see cref="LogOnText"/> property. /// </summary> private const string LogOnTextDefault = "LOG IN"; diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingParty.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingParty.cs index a0a0fb9..dc8e50f 100644 --- a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingParty.cs +++ b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingParty.cs @@ -12,7 +12,11 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { using System.ComponentModel; using System.Diagnostics.CodeAnalysis; using System.Diagnostics.Contracts; + using System.Globalization; using System.Linq; + using System.Net; + using System.Net.Mime; + using System.Text; using System.Web; using DotNetOpenAuth.Configuration; using DotNetOpenAuth.Messaging; @@ -33,10 +37,10 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { public delegate bool EndpointSelector(IProviderEndpoint endpoint); /// <summary> - /// Provides the programmatic facilities to act as an OpenId consumer. + /// Provides the programmatic facilities to act as an OpenID relying party. /// </summary> [ContractVerification(true)] - public sealed class OpenIdRelyingParty : IDisposable { + public class OpenIdRelyingParty : IDisposable { /// <summary> /// The name of the key to use in the HttpApplication cache to store the /// instance of <see cref="StandardRelyingPartyApplicationStore"/> to use. @@ -54,6 +58,22 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { private readonly IList<IIdentifierDiscoveryService> discoveryServices = new List<IIdentifierDiscoveryService>(2); /// <summary> + /// Backing field for the <see cref="NonVerifyingRelyingParty"/> property. + /// </summary> + private OpenIdRelyingParty nonVerifyingRelyingParty; + + /// <summary> + /// The lock to obtain when initializing the <see cref="nonVerifyingRelyingParty"/> member. + /// </summary> + private object nonVerifyingRelyingPartyInitLock = new object(); + + /// <summary> + /// A dictionary of extension response types and the javascript member + /// name to map them to on the user agent. + /// </summary> + private Dictionary<Type, string> clientScriptExtensions = new Dictionary<Type, string>(); + + /// <summary> /// Backing field for the <see cref="SecuritySettings"/> property. /// </summary> private RelyingPartySecuritySettings securitySettings; @@ -78,7 +98,7 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { /// <summary> /// Initializes a new instance of the <see cref="OpenIdRelyingParty"/> class. /// </summary> - /// <param name="applicationStore">The application store. If null, the relying party will always operate in "dumb mode".</param> + /// <param name="applicationStore">The application store. If <c>null</c>, the relying party will always operate in "dumb mode".</param> public OpenIdRelyingParty(IRelyingPartyApplicationStore applicationStore) : this(applicationStore, applicationStore) { } @@ -270,6 +290,24 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { internal AssociationManager AssociationManager { get; private set; } /// <summary> + /// Gets the <see cref="OpenIdRelyingParty"/> instance used to process authentication responses + /// without verifying the assertion or consuming nonces. + /// </summary> + protected OpenIdRelyingParty NonVerifyingRelyingParty { + get { + if (this.nonVerifyingRelyingParty == null) { + lock (this.nonVerifyingRelyingPartyInitLock) { + if (this.nonVerifyingRelyingParty == null) { + this.nonVerifyingRelyingParty = OpenIdRelyingParty.CreateNonVerifying(); + } + } + } + + return this.nonVerifyingRelyingParty; + } + } + + /// <summary> /// Creates an authentication request to verify that a user controls /// some given Identifier. /// </summary> @@ -389,13 +427,13 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { /// <para>No exception is thrown if no OpenID endpoints were discovered. /// An empty enumerable is returned instead.</para> /// </remarks> - public IEnumerable<IAuthenticationRequest> CreateRequests(Identifier userSuppliedIdentifier, Realm realm, Uri returnToUrl) { + public virtual IEnumerable<IAuthenticationRequest> CreateRequests(Identifier userSuppliedIdentifier, Realm realm, Uri returnToUrl) { Contract.Requires<ArgumentNullException>(userSuppliedIdentifier != null); Contract.Requires<ArgumentNullException>(realm != null); Contract.Requires<ArgumentNullException>(returnToUrl != null); Contract.Ensures(Contract.Result<IEnumerable<IAuthenticationRequest>>() != null); - return AuthenticationRequest.Create(userSuppliedIdentifier, this, realm, returnToUrl, true).Cast<IAuthenticationRequest>(); + return AuthenticationRequest.Create(userSuppliedIdentifier, this, realm, returnToUrl, true).Cast<IAuthenticationRequest>().CacheGeneratedResults(); } /// <summary> @@ -487,6 +525,7 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { /// <para>Requires an <see cref="HttpContext.Current">HttpContext.Current</see> context.</para> /// </remarks> public IAuthenticationResponse GetResponse() { + Contract.Requires<InvalidOperationException>(HttpContext.Current != null && HttpContext.Current.Request != null, MessagingStrings.HttpContextRequired); return this.GetResponse(this.Channel.GetRequestFromContext()); } @@ -533,6 +572,52 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { } } + /// <summary> + /// Processes the response received in a popup window or iframe to an AJAX-directed OpenID authentication. + /// </summary> + /// <returns>The HTTP response to send to this HTTP request.</returns> + /// <remarks> + /// <para>Requires an <see cref="HttpContext.Current">HttpContext.Current</see> context.</para> + /// </remarks> + public OutgoingWebResponse ProcessResponseFromPopup() { + Contract.Requires<InvalidOperationException>(HttpContext.Current != null && HttpContext.Current.Request != null, MessagingStrings.HttpContextRequired); + Contract.Ensures(Contract.Result<OutgoingWebResponse>() != null); + + return this.ProcessResponseFromPopup(this.Channel.GetRequestFromContext()); + } + + /// <summary> + /// Processes the response received in a popup window or iframe to an AJAX-directed OpenID authentication. + /// </summary> + /// <param name="request">The incoming HTTP request that is expected to carry an OpenID authentication response.</param> + /// <returns>The HTTP response to send to this HTTP request.</returns> + public OutgoingWebResponse ProcessResponseFromPopup(HttpRequestInfo request) { + Contract.Requires<ArgumentNullException>(request != null); + Contract.Ensures(Contract.Result<OutgoingWebResponse>() != null); + + return this.ProcessResponseFromPopup(request, null); + } + + /// <summary> + /// Allows an OpenID extension to read data out of an unverified positive authentication assertion + /// and send it down to the client browser so that Javascript running on the page can perform + /// some preprocessing on the extension data. + /// </summary> + /// <typeparam name="T">The extension <i>response</i> type that will read data from the assertion.</typeparam> + /// <param name="propertyName">The property name on the openid_identifier input box object that will be used to store the extension data. For example: sreg</param> + /// <remarks> + /// This method should be called before <see cref="ProcessResponseFromPopup()"/>. + /// </remarks> + [SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter", Justification = "By design")] + public void RegisterClientScriptExtension<T>(string propertyName) where T : IClientScriptExtensionResponse { + Contract.Requires<ArgumentException>(!string.IsNullOrEmpty(propertyName)); + ErrorUtilities.VerifyArgumentNamed(!this.clientScriptExtensions.ContainsValue(propertyName), "propertyName", OpenIdStrings.ClientScriptExtensionPropertyNameCollision, propertyName); + foreach (var ext in this.clientScriptExtensions.Keys) { + ErrorUtilities.VerifyArgument(ext != typeof(T), OpenIdStrings.ClientScriptExtensionTypeCollision, typeof(T).FullName); + } + this.clientScriptExtensions.Add(typeof(T), propertyName); + } + #region IDisposable Members /// <summary> @@ -580,6 +665,66 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { } /// <summary> + /// Processes the response received in a popup window or iframe to an AJAX-directed OpenID authentication. + /// </summary> + /// <param name="request">The incoming HTTP request that is expected to carry an OpenID authentication response.</param> + /// <param name="callback">The callback fired after the response status has been determined but before the Javascript response is formulated.</param> + /// <returns> + /// The HTTP response to send to this HTTP request. + /// </returns> + internal OutgoingWebResponse ProcessResponseFromPopup(HttpRequestInfo request, Action<AuthenticationStatus> callback) { + Contract.Requires<ArgumentNullException>(request != null); + Contract.Ensures(Contract.Result<OutgoingWebResponse>() != null); + + string extensionsJson = null; + var authResponse = this.NonVerifyingRelyingParty.GetResponse(); + ErrorUtilities.VerifyProtocol(authResponse != null, "OpenID popup window or iframe did not recognize an OpenID response in the request."); + + // Give the caller a chance to notify the hosting page and fill up the clientScriptExtensions collection. + if (callback != null) { + callback(authResponse.Status); + } + + Logger.OpenId.DebugFormat("Popup or iframe callback from OP: {0}", request.Url); + Logger.Controls.DebugFormat( + "An authentication response was found in a popup window or iframe using a non-verifying RP with status: {0}", + authResponse.Status); + if (authResponse.Status == AuthenticationStatus.Authenticated) { + var extensionsDictionary = new Dictionary<string, string>(); + foreach (var pair in this.clientScriptExtensions) { + IClientScriptExtensionResponse extension = (IClientScriptExtensionResponse)authResponse.GetExtension(pair.Key); + if (extension == null) { + continue; + } + var positiveResponse = (PositiveAuthenticationResponse)authResponse; + string js = extension.InitializeJavaScriptData(positiveResponse.Response); + if (!string.IsNullOrEmpty(js)) { + extensionsDictionary[pair.Value] = js; + } + } + + extensionsJson = MessagingUtilities.CreateJsonObject(extensionsDictionary, true); + } + + string payload = "document.URL"; + if (request.HttpMethod == "POST") { + // Promote all form variables to the query string, but since it won't be passed + // to any server (this is a javascript window-to-window transfer) the length of + // it can be arbitrarily long, whereas it was POSTed here probably because it + // was too long for HTTP transit. + UriBuilder payloadUri = new UriBuilder(request.Url); + payloadUri.AppendQueryArgs(request.Form.ToDictionary()); + payload = MessagingUtilities.GetSafeJavascriptValue(payloadUri.Uri.AbsoluteUri); + } + + if (!string.IsNullOrEmpty(extensionsJson)) { + payload += ", " + extensionsJson; + } + + return InvokeParentPageScript("dnoa_internal.processAuthorizationResult(" + payload + ")"); + } + + /// <summary> /// Performs discovery on the specified identifier. /// </summary> /// <param name="identifier">The identifier to discover services for.</param> @@ -619,8 +764,13 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { /// 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> - private void Dispose(bool disposing) { + protected virtual void Dispose(bool disposing) { if (disposing) { + if (this.nonVerifyingRelyingParty != null) { + this.nonVerifyingRelyingParty.Dispose(); + this.nonVerifyingRelyingParty = null; + } + // Tear off the instance member as a local variable for thread safety. IDisposable disposableChannel = this.channel as IDisposable; if (disposableChannel != null) { @@ -630,6 +780,42 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { } /// <summary> + /// Invokes a method on a parent frame or window and closes the calling popup window if applicable. + /// </summary> + /// <param name="methodCall">The method to call on the parent window, including + /// parameters. (i.e. "callback('arg1', 2)"). No escaping is done by this method.</param> + /// <returns>The entire HTTP response to send to the popup window or iframe to perform the invocation.</returns> + private static OutgoingWebResponse InvokeParentPageScript(string methodCall) { + Contract.Requires<ArgumentException>(!string.IsNullOrEmpty(methodCall)); + + Logger.OpenId.DebugFormat("Sending Javascript callback: {0}", methodCall); + StringBuilder builder = new StringBuilder(); + builder.AppendLine("<html><body><script type='text/javascript' language='javascript'><!--"); + builder.AppendLine("//<![CDATA["); + builder.Append(@" var inPopup = !window.frameElement; + var objSrc = inPopup ? window.opener : window.frameElement; +"); + + // Something about calling objSrc.{0} can somehow cause FireFox to forget about the inPopup variable, + // so we have to actually put the test for it ABOVE the call to objSrc.{0} so that it already + // whether to call window.self.close() after the call. + string htmlFormat = @" if (inPopup) {{ + objSrc.{0}; + window.self.close(); + }} else {{ + objSrc.{0}; + }}"; + builder.AppendFormat(CultureInfo.InvariantCulture, htmlFormat, methodCall); + builder.AppendLine("//]]>--></script>"); + builder.AppendLine("</body></html>"); + + var response = new OutgoingWebResponse(); + response.Body = builder.ToString(); + response.Headers.Add(HttpResponseHeader.ContentType, new ContentType("text/html").ToString()); + return response; + } + + /// <summary> /// Called by derived classes when behaviors are added or removed. /// </summary> /// <param name="sender">The collection being modified.</param> diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingPartyAjaxControlBase.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingPartyAjaxControlBase.cs index 12d676b..37ba8c1 100644 --- a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingPartyAjaxControlBase.cs +++ b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingPartyAjaxControlBase.cs @@ -16,6 +16,7 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { using System.Linq; using System.Text; using System.Web; + using System.Web.Script.Serialization; using System.Web.UI; using DotNetOpenAuth.Configuration; using DotNetOpenAuth.Messaging; @@ -31,30 +32,30 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { internal const string EmbeddedAjaxJavascriptResource = Util.DefaultNamespace + ".OpenId.RelyingParty.OpenIdRelyingPartyAjaxControlBase.js"; /// <summary> - /// The name of the javascript function that will initiate a synchronous callback. + /// The "dnoa.op_endpoint" string. /// </summary> - protected const string CallbackJSFunction = "window.dnoa_internal.callback"; + internal const string OPEndpointParameterName = OpenIdUtilities.CustomParameterPrefix + "op_endpoint"; /// <summary> - /// The name of the javascript function that will initiate an asynchronous callback. + /// The "dnoa.claimed_id" string. /// </summary> - protected const string CallbackJSFunctionAsync = "window.dnoa_internal.callbackAsync"; + internal const string ClaimedIdParameterName = OpenIdUtilities.CustomParameterPrefix + "claimed_id"; /// <summary> /// The name of the javascript field that stores the maximum time a positive assertion is /// good for before it must be refreshed. /// </summary> - private const string MaxPositiveAssertionLifetimeJsName = "window.dnoa_internal.maxPositiveAssertionLifetime"; + internal const string MaxPositiveAssertionLifetimeJsName = "window.dnoa_internal.maxPositiveAssertionLifetime"; /// <summary> - /// The "dnoa.op_endpoint" string. + /// The name of the javascript function that will initiate an asynchronous callback. /// </summary> - private const string OPEndpointParameterName = OpenIdUtilities.CustomParameterPrefix + "op_endpoint"; + protected internal const string CallbackJSFunctionAsync = "window.dnoa_internal.callbackAsync"; /// <summary> - /// The "dnoa.claimed_id" string. + /// The name of the javascript function that will initiate a synchronous callback. /// </summary> - private const string ClaimedIdParameterName = OpenIdUtilities.CustomParameterPrefix + "claimed_id"; + protected const string CallbackJSFunction = "window.dnoa_internal.callback"; #region Property viewstate keys @@ -86,11 +87,6 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { private const LogOnSiteNotification LogOnModeDefault = LogOnSiteNotification.None; /// <summary> - /// Backing field for the <see cref="RelyingPartyNonVerifying"/> property. - /// </summary> - private static OpenIdRelyingParty relyingPartyNonVerifying; - - /// <summary> /// The authentication response that just came in. /// </summary> private IAuthenticationResponse authenticationResponse; @@ -102,12 +98,6 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { private string discoveryResult; /// <summary> - /// A dictionary of extension response types and the javascript member - /// name to map them to on the user agent. - /// </summary> - private Dictionary<Type, string> clientScriptExtensions = new Dictionary<Type, string>(); - - /// <summary> /// Initializes a new instance of the <see cref="OpenIdRelyingPartyAjaxControlBase"/> class. /// </summary> protected OpenIdRelyingPartyAjaxControlBase() { @@ -152,6 +142,31 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { } /// <summary> + /// Gets or sets the <see cref="OpenIdRelyingParty"/> instance to use. + /// </summary> + /// <value> + /// The default value is an <see cref="OpenIdRelyingParty"/> instance initialized according to the web.config file. + /// </value> + /// <remarks> + /// A performance optimization would be to store off the + /// instance as a static member in your web site and set it + /// to this property in your <see cref="Control.Load">Page.Load</see> + /// event since instantiating these instances can be expensive on + /// heavily trafficked web pages. + /// </remarks> + public override OpenIdRelyingParty RelyingParty { + get { + return base.RelyingParty; + } + + set { + // Make sure we get an AJAX-ready instance. + ErrorUtilities.VerifyArgument(value is OpenIdAjaxRelyingParty, OpenIdStrings.TypeMustImplementX, typeof(OpenIdAjaxRelyingParty).Name); + base.RelyingParty = value; + } + } + + /// <summary> /// Gets the completed authentication response. /// </summary> public IAuthenticationResponse AuthenticationResponse { @@ -193,22 +208,17 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { } /// <summary> - /// Gets the name of the open id auth data form key (for the value as stored at the user agent as a FORM field). + /// Gets the relying party as its AJAX type. /// </summary> - /// <value>Usually a concatenation of the control's name and <c>"_openidAuthData"</c>.</value> - protected abstract string OpenIdAuthDataFormKey { get; } + protected OpenIdAjaxRelyingParty AjaxRelyingParty { + get { return (OpenIdAjaxRelyingParty)this.RelyingParty; } + } /// <summary> - /// Gets the relying party to use when verification of incoming messages is NOT wanted. + /// Gets the name of the open id auth data form key (for the value as stored at the user agent as a FORM field). /// </summary> - private static OpenIdRelyingParty RelyingPartyNonVerifying { - get { - if (relyingPartyNonVerifying == null) { - relyingPartyNonVerifying = OpenIdRelyingParty.CreateNonVerifying(); - } - return relyingPartyNonVerifying; - } - } + /// <value>Usually a concatenation of the control's name and <c>"_openidAuthData"</c>.</value> + protected abstract string OpenIdAuthDataFormKey { get; } /// <summary> /// Gets or sets a value indicating whether an authentication in the page's view state @@ -232,11 +242,7 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { [SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter", Justification = "By design")] public void RegisterClientScriptExtension<T>(string propertyName) where T : IClientScriptExtensionResponse { Contract.Requires<ArgumentException>(!string.IsNullOrEmpty(propertyName)); - ErrorUtilities.VerifyArgumentNamed(!this.clientScriptExtensions.ContainsValue(propertyName), "propertyName", OpenIdStrings.ClientScriptExtensionPropertyNameCollision, propertyName); - foreach (var ext in this.clientScriptExtensions.Keys) { - ErrorUtilities.VerifyArgument(ext != typeof(T), OpenIdStrings.ClientScriptExtensionTypeCollision, typeof(T).FullName); - } - this.clientScriptExtensions.Add(typeof(T), propertyName); + this.RelyingParty.RegisterClientScriptExtension<T>(propertyName); } #region ICallbackEventHandler Members @@ -263,27 +269,6 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { #endregion /// <summary> - /// Creates the authentication requests for a given user-supplied Identifier. - /// </summary> - /// <param name="identifier">The identifier to create a request for.</param> - /// <returns> - /// A sequence of authentication requests, any one of which may be - /// used to determine the user's control of the <see cref="IAuthenticationRequest.ClaimedIdentifier"/>. - /// </returns> - protected internal override IEnumerable<IAuthenticationRequest> CreateRequests(Identifier identifier) { - // If this control is actually a member of another OpenID RP control, - // delegate creation of requests to the parent control. - var parentOwner = this.ParentControls.OfType<OpenIdRelyingPartyControlBase>().FirstOrDefault(); - if (parentOwner != null) { - return parentOwner.CreateRequests(identifier); - } else { - // We delegate all our logic to another method, since invoking base. methods - // within an iterator method results in unverifiable code. - return this.CreateRequestsCore(base.CreateRequests(identifier)); - } - } - - /// <summary> /// Returns the results of a callback event that targets a control. /// </summary> /// <returns>The result of the callback.</returns> @@ -305,7 +290,19 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { Logger.OpenId.InfoFormat("AJAX discovery on {0} requested.", userSuppliedIdentifier); this.Identifier = userSuppliedIdentifier; - this.discoveryResult = this.SerializeDiscoveryAsJson(this.Identifier); + + var serializer = new JavaScriptSerializer(); + IEnumerable<IAuthenticationRequest> requests = this.CreateRequests(this.Identifier); + this.discoveryResult = serializer.Serialize(this.AjaxRelyingParty.AsJsonDiscoveryResult(requests)); + } + + /// <summary> + /// Creates the relying party instance used to generate authentication requests. + /// </summary> + /// <param name="store">The store to pass to the relying party constructor.</param> + /// <returns>The instantiated relying party.</returns> + protected override OpenIdRelyingParty CreateRelyingParty(IRelyingPartyApplicationStore store) { + return new OpenIdAjaxRelyingParty(store); } /// <summary> @@ -323,8 +320,8 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { /// </summary> /// <param name="identifiers">The identifiers to perform discovery on.</param> protected void PreloadDiscovery(IEnumerable<Identifier> identifiers) { - string discoveryResults = this.SerializeDiscoveryAsJson(identifiers); - string script = "window.dnoa_internal.loadPreloadedDiscoveryResults(" + discoveryResults + ");"; + string script = this.AjaxRelyingParty.AsAjaxPreloadedDiscoveryResult( + identifiers.Select(id => this.CreateRequests(id)).Flatten()); this.Page.ClientScript.RegisterClientScriptBlock(typeof(OpenIdRelyingPartyAjaxControlBase), this.ClientID, script, true); } @@ -420,172 +417,17 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { /// Notifies the user agent via an AJAX response of a completed authentication attempt. /// </summary> protected override void ScriptClosingPopupOrIFrame() { - Logger.OpenId.DebugFormat("AJAX (iframe) callback from OP: {0}", this.Page.Request.Url); - string extensionsJson = null; - - var authResponse = RelyingPartyNonVerifying.GetResponse(); - Logger.Controls.DebugFormat( - "The {0} control checked for an authentication response from a popup window or iframe using a non-verifying RP and found: {1}", - this.ID, - authResponse.Status); - if (authResponse.Status == AuthenticationStatus.Authenticated) { - this.OnUnconfirmedPositiveAssertion(); // event handler will fill the clientScriptExtensions collection. - var extensionsDictionary = new Dictionary<string, string>(); - foreach (var pair in this.clientScriptExtensions) { - IClientScriptExtensionResponse extension = (IClientScriptExtensionResponse)authResponse.GetExtension(pair.Key); - if (extension == null) { - continue; - } - var positiveResponse = (PositiveAuthenticationResponse)authResponse; - string js = extension.InitializeJavaScriptData(positiveResponse.Response); - if (!string.IsNullOrEmpty(js)) { - extensionsDictionary[pair.Value] = js; - } + Action<AuthenticationStatus> callback = status => { + if (status == AuthenticationStatus.Authenticated) { + this.OnUnconfirmedPositiveAssertion(); // event handler will fill the clientScriptExtensions collection. } + }; - extensionsJson = MessagingUtilities.CreateJsonObject(extensionsDictionary, true); - } - - string payload = "document.URL"; - if (Page.Request.HttpMethod == "POST") { - // Promote all form variables to the query string, but since it won't be passed - // to any server (this is a javascript window-to-window transfer) the length of - // it can be arbitrarily long, whereas it was POSTed here probably because it - // was too long for HTTP transit. - UriBuilder payloadUri = new UriBuilder(Page.Request.Url); - payloadUri.AppendQueryArgs(Page.Request.Form.ToDictionary()); - payload = MessagingUtilities.GetSafeJavascriptValue(payloadUri.Uri.AbsoluteUri); - } - - if (!string.IsNullOrEmpty(extensionsJson)) { - payload += ", " + extensionsJson; - } - - this.CallbackUserAgentMethod("dnoa_internal.processAuthorizationResult(" + payload + ")"); - } - - /// <summary> - /// Serializes the discovery of multiple identifiers as a JSON object. - /// </summary> - /// <param name="identifiers">The identifiers to perform discovery on and create requests for.</param> - /// <returns>The serialized JSON object.</returns> - private string SerializeDiscoveryAsJson(IEnumerable<Identifier> identifiers) { - ErrorUtilities.VerifyArgumentNotNull(identifiers, "identifiers"); - - // We prepare a JSON object with this interface: - // Array discoveryWrappers; - // Where each element in the above array has this interface: - // class discoveryWrapper { - // string userSuppliedIdentifier; - // jsonResponse discoveryResult; // contains result of call to SerializeDiscoveryAsJson(Identifier) - // } - StringBuilder discoveryResultBuilder = new StringBuilder(); - discoveryResultBuilder.Append("["); - foreach (var identifier in identifiers) { // TODO: parallelize discovery on these identifiers - discoveryResultBuilder.Append("{"); - discoveryResultBuilder.AppendFormat("userSuppliedIdentifier: {0},", MessagingUtilities.GetSafeJavascriptValue(identifier)); - discoveryResultBuilder.AppendFormat("discoveryResult: {0}", this.SerializeDiscoveryAsJson(identifier)); - discoveryResultBuilder.Append("},"); - } - - discoveryResultBuilder.Length -= 1; // trim last comma - discoveryResultBuilder.Append("]"); - return discoveryResultBuilder.ToString(); - } - - /// <summary> - /// Serializes the results of discovery and the created auth requests as a JSON object - /// for the user agent to initiate. - /// </summary> - /// <param name="identifier">The identifier to perform discovery on.</param> - /// <returns>The JSON string.</returns> - private string SerializeDiscoveryAsJson(Identifier identifier) { - ErrorUtilities.VerifyArgumentNotNull(identifier, "identifier"); - - // We prepare a JSON object with this interface: - // class jsonResponse { - // string claimedIdentifier; - // Array requests; // never null - // string error; // null if no error - // } - // Each element in the requests array looks like this: - // class jsonAuthRequest { - // string endpoint; // URL to the OP endpoint - // string immediate; // URL to initiate an immediate request - // string setup; // URL to initiate a setup request. - // } - StringBuilder discoveryResultBuilder = new StringBuilder(); - discoveryResultBuilder.Append("{"); - try { - IEnumerable<IAuthenticationRequest> requests = this.CreateRequests(identifier).CacheGeneratedResults(); - if (requests.Any()) { - discoveryResultBuilder.AppendFormat("claimedIdentifier: {0},", MessagingUtilities.GetSafeJavascriptValue(requests.First().ClaimedIdentifier)); - discoveryResultBuilder.Append("requests: ["); - foreach (IAuthenticationRequest request in requests) { - discoveryResultBuilder.Append("{"); - discoveryResultBuilder.AppendFormat("endpoint: {0},", MessagingUtilities.GetSafeJavascriptValue(request.Provider.Uri.AbsoluteUri)); - request.Mode = AuthenticationRequestMode.Immediate; - OutgoingWebResponse response = request.RedirectingResponse; - discoveryResultBuilder.AppendFormat("immediate: {0},", MessagingUtilities.GetSafeJavascriptValue(response.GetDirectUriRequest(this.RelyingParty.Channel).AbsoluteUri)); - request.Mode = AuthenticationRequestMode.Setup; - response = request.RedirectingResponse; - discoveryResultBuilder.AppendFormat("setup: {0}", MessagingUtilities.GetSafeJavascriptValue(response.GetDirectUriRequest(this.RelyingParty.Channel).AbsoluteUri)); - discoveryResultBuilder.Append("},"); - } - discoveryResultBuilder.Length -= 1; // trim off last comma - discoveryResultBuilder.Append("]"); - } else { - discoveryResultBuilder.Append("requests: [],"); - discoveryResultBuilder.AppendFormat("error: {0}", MessagingUtilities.GetSafeJavascriptValue(OpenIdStrings.OpenIdEndpointNotFound)); - } - } catch (ProtocolException ex) { - discoveryResultBuilder.Append("requests: [],"); - discoveryResultBuilder.AppendFormat("error: {0}", MessagingUtilities.GetSafeJavascriptValue(ex.Message)); - } - - discoveryResultBuilder.Append("}"); - return discoveryResultBuilder.ToString(); - } - - /// <summary> - /// Creates the authentication requests for a given user-supplied Identifier. - /// </summary> - /// <param name="requests">The authentication requests to prepare.</param> - /// <returns> - /// A sequence of authentication requests, any one of which may be - /// used to determine the user's control of the <see cref="IAuthenticationRequest.ClaimedIdentifier"/>. - /// </returns> - private IEnumerable<IAuthenticationRequest> CreateRequestsCore(IEnumerable<IAuthenticationRequest> requests) { - ErrorUtilities.VerifyArgumentNotNull(requests, "requests"); // NO CODE CONTRACTS! (yield return used here) - - // Configure each generated request. - int reqIndex = 0; - foreach (var req in requests) { - req.SetUntrustedCallbackArgument("index", (reqIndex++).ToString(CultureInfo.InvariantCulture)); - - // If the ReturnToUrl was explicitly set, we'll need to reset our first parameter - if (string.IsNullOrEmpty(HttpUtility.ParseQueryString(req.ReturnToUrl.Query)[AuthenticationRequest.UserSuppliedIdentifierParameterName])) { - Identifier userSuppliedIdentifier = ((AuthenticationRequest)req).DiscoveryResult.UserSuppliedIdentifier; - req.SetUntrustedCallbackArgument(AuthenticationRequest.UserSuppliedIdentifierParameterName, userSuppliedIdentifier.OriginalString); - } - - // Our javascript needs to let the user know which endpoint responded. So we force it here. - // This gives us the info even for 1.0 OPs and 2.0 setup_required responses. - req.SetUntrustedCallbackArgument(OPEndpointParameterName, req.Provider.Uri.AbsoluteUri); - req.SetUntrustedCallbackArgument(ClaimedIdParameterName, (string)req.ClaimedIdentifier ?? string.Empty); - - // Inform ourselves in return_to that we're in a popup or iframe. - req.SetUntrustedCallbackArgument(UIPopupCallbackKey, "1"); - - // We append a # at the end so that if the OP happens to support it, - // the OpenID response "query string" is appended after the hash rather than before, resulting in the - // browser being super-speedy in closing the popup window since it doesn't try to pull a newer version - // of the static resource down from the server merely because of a changed URL. - // http://www.nabble.com/Re:-Defining-how-OpenID-should-behave-with-fragments-in-the-return_to-url-p22694227.html - ////TODO: + OutgoingWebResponse response = this.RelyingParty.ProcessResponseFromPopup( + this.RelyingParty.Channel.GetRequestFromContext(), + callback); - yield return req; - } + response.Send(); } /// <summary> @@ -615,33 +457,6 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { } /// <summary> - /// Invokes a method on a parent frame/window's OpenIdAjaxTextBox, - /// and closes the calling popup window if applicable. - /// </summary> - /// <param name="methodCall">The method to call on the OpenIdAjaxTextBox, including - /// parameters. (i.e. "callback('arg1', 2)"). No escaping is done by this method.</param> - private void CallbackUserAgentMethod(string methodCall) { - Logger.OpenId.DebugFormat("Sending Javascript callback: {0}", methodCall); - Page.Response.Write(@"<html><body><script language='javascript'> - var inPopup = !window.frameElement; - var objSrc = inPopup ? window.opener : window.frameElement; -"); - - // Something about calling objSrc.{0} can somehow cause FireFox to forget about the inPopup variable, - // so we have to actually put the test for it ABOVE the call to objSrc.{0} so that it already - // whether to call window.self.close() after the call. - string htmlFormat = @" if (inPopup) {{ - objSrc.{0}; - window.self.close(); - }} else {{ - objSrc.{0}; - }} -</script></body></html>"; - Page.Response.Write(string.Format(CultureInfo.InvariantCulture, htmlFormat, methodCall)); - Page.Response.End(); - } - - /// <summary> /// Sets the window.aspnetapppath variable on the user agent so that cookies can be set with the proper path. /// </summary> private void SetWebAppPathOnUserAgent() { diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingPartyControlBase.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingPartyControlBase.cs index a4a60d1..838b749 100644 --- a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingPartyControlBase.cs +++ b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingPartyControlBase.cs @@ -92,6 +92,21 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { /// </summary> internal const string ReturnToReceivingControlId = OpenIdUtilities.CustomParameterPrefix + "receiver"; + #region Protected internal callback parameter names + + /// <summary> + /// The callback parameter to use for recognizing when the callback is in a popup window or hidden iframe. + /// </summary> + protected internal const string UIPopupCallbackKey = OpenIdUtilities.CustomParameterPrefix + "uipopup"; + + /// <summary> + /// The parameter name to include in the formulated auth request so that javascript can know whether + /// the OP advertises support for the UI extension. + /// </summary> + protected internal const string PopupUISupportedJSHint = OpenIdUtilities.CustomParameterPrefix + "popupUISupported"; + + #endregion + #region Property category constants /// <summary> @@ -111,18 +126,7 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { #endregion - #region Callback parameter names - - /// <summary> - /// The callback parameter to use for recognizing when the callback is in a popup window or hidden iframe. - /// </summary> - protected const string UIPopupCallbackKey = OpenIdUtilities.CustomParameterPrefix + "uipopup"; - - /// <summary> - /// The parameter name to include in the formulated auth request so that javascript can know whether - /// the OP advertises support for the UI extension. - /// </summary> - protected const string PopupUISupportedJSHint = OpenIdUtilities.CustomParameterPrefix + "popupUISupported"; + #region Private callback parameter names /// <summary> /// The callback parameter for use with persisting the <see cref="UsePersistentCookie"/> property. @@ -293,10 +297,11 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { /// heavily trafficked web pages. /// </remarks> [Browsable(false)] - public OpenIdRelyingParty RelyingParty { + public virtual OpenIdRelyingParty RelyingParty { get { if (this.relyingParty == null) { this.relyingParty = this.CreateRelyingParty(); + this.ConfigureRelyingParty(this.relyingParty); this.relyingPartyOwned = true; } return this.relyingParty; @@ -556,8 +561,15 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { protected internal virtual IEnumerable<IAuthenticationRequest> CreateRequests(Identifier identifier) { Contract.Requires<ArgumentNullException>(identifier != null); - // Delegate to a private method to keep 'yield return' and Code Contract separate. - return this.CreateRequestsCore(identifier); + // If this control is actually a member of another OpenID RP control, + // delegate creation of requests to the parent control. + var parentOwner = this.ParentControls.OfType<OpenIdRelyingPartyControlBase>().FirstOrDefault(); + if (parentOwner != null) { + return parentOwner.CreateRequests(identifier); + } else { + // Delegate to a private method to keep 'yield return' and Code Contract separate. + return this.CreateRequestsCore(identifier); + } } /// <summary> @@ -634,6 +646,13 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { } /// <summary> + /// Notifies the user agent via an AJAX response of a completed authentication attempt. + /// </summary> + protected virtual void ScriptClosingPopupOrIFrame() { + this.RelyingParty.ProcessResponseFromPopup(); + } + + /// <summary> /// Called when the <see cref="Identifier"/> property is changed. /// </summary> protected virtual void OnIdentifierChanged() { @@ -769,29 +788,32 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { /// Creates the relying party instance used to generate authentication requests. /// </summary> /// <returns>The instantiated relying party.</returns> - protected virtual OpenIdRelyingParty CreateRelyingParty() { - return this.CreateRelyingParty(true); + protected OpenIdRelyingParty CreateRelyingParty() { + IRelyingPartyApplicationStore store = this.Stateless ? null : DotNetOpenAuthSection.Configuration.OpenId.RelyingParty.ApplicationStore.CreateInstance(OpenIdRelyingParty.HttpApplicationStore); + return this.CreateRelyingParty(store); } /// <summary> /// Creates the relying party instance used to generate authentication requests. /// </summary> - /// <param name="verifySignature"> - /// A value indicating whether message protections should be applied to the processed messages. - /// Use <c>false</c> to postpone verification to a later time without invalidating nonces. - /// </param> + /// <param name="store">The store to pass to the relying party constructor.</param> /// <returns>The instantiated relying party.</returns> - protected virtual OpenIdRelyingParty CreateRelyingParty(bool verifySignature) { - IRelyingPartyApplicationStore store = this.Stateless ? null : DotNetOpenAuthSection.Configuration.OpenId.RelyingParty.ApplicationStore.CreateInstance(OpenIdRelyingParty.HttpApplicationStore); - var rp = verifySignature ? new OpenIdRelyingParty(store) : OpenIdRelyingParty.CreateNonVerifying(); + protected virtual OpenIdRelyingParty CreateRelyingParty(IRelyingPartyApplicationStore store) { + return new OpenIdRelyingParty(store); + } + + /// <summary> + /// Configures the relying party. + /// </summary> + /// <param name="relyingParty">The relying party.</param> + protected virtual void ConfigureRelyingParty(OpenIdRelyingParty relyingParty) { + Contract.Requires<ArgumentNullException>(relyingParty != null); // Only set RequireSsl to true, as we don't want to override // a .config setting of true with false. if (this.RequireSsl) { - rp.SecuritySettings.RequireSsl = true; + relyingParty.SecuritySettings.RequireSsl = true; } - - return rp; } /// <summary> @@ -841,21 +863,6 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { } /// <summary> - /// Wires the popup window to close itself and pass the authentication result to the parent window. - /// </summary> - protected virtual void ScriptClosingPopupOrIFrame() { - StringBuilder startupScript = new StringBuilder(); - startupScript.AppendLine("window.opener.dnoa_internal.processAuthorizationResult(document.URL);"); - startupScript.AppendLine("window.close();"); - - this.Page.ClientScript.RegisterStartupScript(typeof(OpenIdRelyingPartyControlBase), "loginPopupClose", startupScript.ToString(), true); - - // TODO: alternately we should probably take over rendering this page here to avoid - // a lot of unnecessary work on the server and possible momentary display of the - // page in the popup window. - } - - /// <summary> /// Creates the identifier-persisting cookie, either for saving or deleting. /// </summary> /// <param name="response">The positive authentication response; or <c>null</c> to clear the cookie.</param> @@ -939,7 +946,11 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { if (req.DiscoveryResult.IsExtensionSupported<UIRequest>()) { // Inform the OP that we'll be using a popup window consistent with the UI extension. - req.AddExtension(new UIRequest()); + // But beware that the extension MAY have already been added if we're using + // the OpenIdAjaxRelyingParty class. + if (!((AuthenticationRequest)req).Extensions.OfType<UIRequest>().Any()) { + req.AddExtension(new UIRequest()); + } // Provide a hint for the client javascript about whether the OP supports the UI extension. // This is so the window can be made the correct size for the extension. @@ -1029,67 +1040,5 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { return false; } - - /// <summary> - /// An authentication request comparer that judges equality solely on the OP endpoint hostname. - /// </summary> - private class DuplicateRequestedHostsComparer : IEqualityComparer<IAuthenticationRequest> { - /// <summary> - /// The singleton instance of this comparer. - /// </summary> - private static IEqualityComparer<IAuthenticationRequest> instance = new DuplicateRequestedHostsComparer(); - - /// <summary> - /// Prevents a default instance of the <see cref="DuplicateRequestedHostsComparer"/> class from being created. - /// </summary> - private DuplicateRequestedHostsComparer() { - } - - /// <summary> - /// Gets the singleton instance of this comparer. - /// </summary> - internal static IEqualityComparer<IAuthenticationRequest> Instance { - get { return instance; } - } - - #region IEqualityComparer<IAuthenticationRequest> Members - - /// <summary> - /// Determines whether the specified objects are equal. - /// </summary> - /// <param name="x">The first object to compare.</param> - /// <param name="y">The second object to compare.</param> - /// <returns> - /// true if the specified objects are equal; otherwise, false. - /// </returns> - public bool Equals(IAuthenticationRequest x, IAuthenticationRequest y) { - if (x == null && y == null) { - return true; - } - - if (x == null || y == null) { - return false; - } - - // We'll distinguish based on the host name only, which - // admittedly is only a heuristic, but if we remove one that really wasn't a duplicate, well, - // this multiple OP attempt thing was just a convenience feature anyway. - return string.Equals(x.Provider.Uri.Host, y.Provider.Uri.Host, StringComparison.OrdinalIgnoreCase); - } - - /// <summary> - /// Returns a hash code for the specified object. - /// </summary> - /// <param name="obj">The <see cref="T:System.Object"/> for which a hash code is to be returned.</param> - /// <returns>A hash code for the specified object.</returns> - /// <exception cref="T:System.ArgumentNullException"> - /// The type of <paramref name="obj"/> is a reference type and <paramref name="obj"/> is null. - /// </exception> - public int GetHashCode(IAuthenticationRequest obj) { - return obj.Provider.Uri.Host.GetHashCode(); - } - - #endregion - } } } diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdSelector.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdSelector.cs index e93383d..5b85e7c 100644 --- a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdSelector.cs +++ b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdSelector.cs @@ -81,11 +81,6 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { private HiddenField positiveAssertionField; /// <summary> - /// A field to store the value to set on the <see cref="textBox"/> control after it's created. - /// </summary> - private bool downloadYuiLibrary = OpenIdAjaxTextBox.DownloadYahooUILibraryDefault; - - /// <summary> /// Initializes a new instance of the <see cref="OpenIdSelector"/> class. /// </summary> public OpenIdSelector() { @@ -102,6 +97,50 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { public event EventHandler<TokenProcessingErrorEventArgs> TokenProcessingError; /// <summary> + /// Gets the text box where applicable. + /// </summary> + public OpenIdAjaxTextBox TextBox { + get { + this.EnsureChildControlsAreCreatedSafe(); + return this.textBox; + } + } + + /// <summary> + /// Gets or sets the maximum number of OpenID Providers to simultaneously try to authenticate with. + /// </summary> + [Browsable(true), DefaultValue(OpenIdAjaxTextBox.ThrottleDefault), Category(BehaviorCategory)] + [Description("The maximum number of OpenID Providers to simultaneously try to authenticate with.")] + public int Throttle { + get { + this.EnsureChildControlsAreCreatedSafe(); + return this.textBox.Throttle; + } + + set { + this.EnsureChildControlsAreCreatedSafe(); + this.textBox.Throttle = value; + } + } + + /// <summary> + /// Gets or sets the time duration for the AJAX control to wait for an OP to respond before reporting failure to the user. + /// </summary> + [Browsable(true), DefaultValue(typeof(TimeSpan), "00:00:08"), Category(BehaviorCategory)] + [Description("The time duration for the AJAX control to wait for an OP to respond before reporting failure to the user.")] + public TimeSpan Timeout { + get { + this.EnsureChildControlsAreCreatedSafe(); + return this.textBox.Timeout; + } + + set { + this.EnsureChildControlsAreCreatedSafe(); + this.textBox.Timeout = value; + } + } + + /// <summary> /// Gets or sets the tool tip text that appears on the green checkmark when authentication succeeds. /// </summary> [Bindable(true), DefaultValue(AuthenticatedAsToolTipDefault), Localizable(true), Category(AppearanceCategory)] @@ -126,19 +165,13 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { [Description("Whether a split button will be used for the \"log in\" when the user provides an identifier that delegates to more than one Provider.")] public bool DownloadYahooUILibrary { get { - return this.textBox != null ? this.textBox.DownloadYahooUILibrary : this.downloadYuiLibrary; + this.EnsureChildControlsAreCreatedSafe(); + return this.textBox.DownloadYahooUILibrary; } set { - // We don't just call EnsureChildControls() and then set the property on - // this.textBox itself because (apparently) setting this property in the ASPX - // page and thus calling this EnsureID() via EnsureChildControls() this early - // results in no ID. - if (this.textBox != null) { - this.textBox.DownloadYahooUILibrary = value; - } else { - this.downloadYuiLibrary = value; - } + this.EnsureChildControlsAreCreatedSafe(); + this.textBox.DownloadYahooUILibrary = value; } } @@ -207,10 +240,37 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { /// Called by the ASP.NET page framework to notify server controls that use composition-based implementation to create any child controls they contain in preparation for posting back or rendering. /// </summary> protected override void CreateChildControls() { + this.EnsureChildControlsAreCreatedSafe(); + base.CreateChildControls(); + + // Now do the ID specific work. this.EnsureID(); ErrorUtilities.VerifyInternal(!string.IsNullOrEmpty(this.UniqueID), "Control.EnsureID() failed to give us a unique ID. Try setting an ID on the OpenIdSelector control. But please also file this bug with the project owners."); + this.Controls.Add(this.textBox); + + this.positiveAssertionField.ID = this.ID + AuthDataFormKeySuffix; + this.Controls.Add(this.positiveAssertionField); + } + + /// <summary> + /// Ensures that the child controls have been built, but doesn't set control + /// properties that require executing <see cref="Control.EnsureID"/> in order to avoid + /// certain initialization order problems. + /// </summary> + /// <remarks> + /// We don't just call EnsureChildControls() and then set the property on + /// this.textBox itself because (apparently) setting this property in the ASPX + /// page and thus calling this EnsureID() via EnsureChildControls() this early + /// results in no ID. + /// </remarks> + protected virtual void EnsureChildControlsAreCreatedSafe() { + // If we've already created the child controls, this method is a no-op. + if (this.textBox != null) { + return; + } + var selectorButton = this.Buttons.OfType<SelectorInfoCardButton>().FirstOrDefault(); if (selectorButton != null) { var selector = selectorButton.InfoCardSelector; @@ -225,12 +285,8 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { this.textBox.ID = "openid_identifier"; this.textBox.HookFormSubmit = false; this.textBox.ShowLogOnPostBackButton = true; - this.textBox.DownloadYahooUILibrary = this.downloadYuiLibrary; - this.Controls.Add(this.textBox); this.positiveAssertionField = new HiddenField(); - this.positiveAssertionField.ID = this.ID + AuthDataFormKeySuffix; - this.Controls.Add(this.positiveAssertionField); } /// <summary> diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/SelectorOpenIdButton.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/SelectorOpenIdButton.cs index 15b6ca7..ac4dcbf 100644 --- a/src/DotNetOpenAuth/OpenId/RelyingParty/SelectorOpenIdButton.cs +++ b/src/DotNetOpenAuth/OpenId/RelyingParty/SelectorOpenIdButton.cs @@ -5,6 +5,7 @@ //----------------------------------------------------------------------- namespace DotNetOpenAuth.OpenId.RelyingParty { + using System; using System.ComponentModel; using System.Diagnostics.Contracts; using System.Drawing.Design; @@ -24,6 +25,17 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { } /// <summary> + /// Initializes a new instance of the <see cref="SelectorOpenIdButton"/> class. + /// </summary> + /// <param name="imageUrl">The image to display on the button.</param> + public SelectorOpenIdButton(string imageUrl) + : this() { + Contract.Requires<ArgumentException>(!string.IsNullOrEmpty(imageUrl)); + + this.Image = imageUrl; + } + + /// <summary> /// Gets or sets the path to the image to display on the button's surface. /// </summary> /// <value>The virtual path to the image.</value> diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/SelectorProviderButton.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/SelectorProviderButton.cs index 3a05287..2195e73 100644 --- a/src/DotNetOpenAuth/OpenId/RelyingParty/SelectorProviderButton.cs +++ b/src/DotNetOpenAuth/OpenId/RelyingParty/SelectorProviderButton.cs @@ -5,6 +5,7 @@ //----------------------------------------------------------------------- namespace DotNetOpenAuth.OpenId.RelyingParty { + using System; using System.ComponentModel; using System.Diagnostics.Contracts; using System.Drawing.Design; @@ -25,6 +26,20 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { } /// <summary> + /// Initializes a new instance of the <see cref="SelectorProviderButton"/> class. + /// </summary> + /// <param name="providerIdentifier">The OP Identifier.</param> + /// <param name="imageUrl">The image to display on the button.</param> + public SelectorProviderButton(Identifier providerIdentifier, string imageUrl) + : this() { + Contract.Requires<ArgumentNullException>(providerIdentifier != null); + Contract.Requires<ArgumentException>(!string.IsNullOrEmpty(imageUrl)); + + this.OPIdentifier = providerIdentifier; + this.Image = imageUrl; + } + + /// <summary> /// Gets or sets the path to the image to display on the button's surface. /// </summary> /// <value>The virtual path to the image.</value> |