diff options
5 files changed, 396 insertions, 213 deletions
diff --git a/samples/OpenIdRelyingPartyWebForms/login.aspx b/samples/OpenIdRelyingPartyWebForms/login.aspx index 42d40e8..aa96339 100644 --- a/samples/OpenIdRelyingPartyWebForms/login.aspx +++ b/samples/OpenIdRelyingPartyWebForms/login.aspx @@ -2,6 +2,14 @@ ValidateRequest="false" MasterPageFile="~/Site.Master" %> <%@ Register Assembly="DotNetOpenAuth" Namespace="DotNetOpenAuth.OpenId.RelyingParty" TagPrefix="rp" %> + +<asp:Content runat=server ContentPlaceHolderID=head> +<script> + +window.openid_visible_iframe = true; // causes the hidden iframe to show up +window.openid_trace = true; // causes lots of messages +</script> +</asp:Content> <asp:Content runat="server" ContentPlaceHolderID="Main"> <h2>Login Page </h2> <rp:OpenIdLogin ID="OpenIdLogin1" runat="server" CssClass="openid_login" RequestCountry="Request" @@ -23,8 +31,7 @@ <asp:Label ID="setupRequiredLabel" runat="server" EnableViewState="False" Text="You must log into your Provider first to use Immediate mode." Visible="False" /> <p> - <rp:OpenIdButton runat="server" ImageUrl="~/images/yahoo.png" - ID="yahooLoginButton" Identifier="https://me.yahoo.com/" - /> + <rp:OpenIdButton runat="server" ImageUrl="~/images/yahoo.png" + ID="yahooLoginButton" Identifier="https://me.yahoo.com/" /> </p> </asp:Content> diff --git a/src/DotNetOpenAuth/DotNetOpenAuth.csproj b/src/DotNetOpenAuth/DotNetOpenAuth.csproj index 34dc7b4..40c219b 100644 --- a/src/DotNetOpenAuth/DotNetOpenAuth.csproj +++ b/src/DotNetOpenAuth/DotNetOpenAuth.csproj @@ -431,6 +431,7 @@ <Compile Include="OpenId\RelyingParty\AssociationPreference.cs" /> <Compile Include="OpenId\RelyingParty\AuthenticationRequest.cs" /> <Compile Include="OpenId\RelyingParty\AuthenticationRequestMode.cs" /> + <Compile Include="OpenId\RelyingParty\OpenIdRelyingPartyAjaxControlBase.cs" /> <Compile Include="OpenId\RelyingParty\NegativeAuthenticationResponse.cs" /> <Compile Include="OpenId\RelyingParty\OpenIdAjaxTextBox.cs" /> <Compile Include="OpenId\RelyingParty\OpenIdButton.cs" /> @@ -564,9 +565,6 @@ <ItemGroup> <EmbeddedResource Include="OpenId\RelyingParty\OpenIdRelyingPartyControlBase.js" /> </ItemGroup> - <ItemGroup> - <EmbeddedResource Include="OpenId\RelyingParty\OpenIdRelyingPartyControlBase.ReturnTo.html" /> - </ItemGroup> <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> <Import Project="..\..\tools\DotNetOpenAuth.Versioning.targets" /> </Project>
\ No newline at end of file diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingPartyAjaxControlBase.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingPartyAjaxControlBase.cs new file mode 100644 index 0000000..7745231 --- /dev/null +++ b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingPartyAjaxControlBase.cs @@ -0,0 +1,290 @@ +//----------------------------------------------------------------------- +// <copyright file="OpenIdRelyingPartyAjaxControlBase.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +[assembly: System.Web.UI.WebResource(DotNetOpenAuth.OpenId.RelyingParty.OpenIdRelyingPartyControlBase.EmbeddedJavascriptResource, "text/javascript")] + +namespace DotNetOpenAuth.OpenId.RelyingParty { + using System; + using System.Collections.Generic; + using System.ComponentModel; + using System.Diagnostics.CodeAnalysis; + using System.Diagnostics.Contracts; + using System.Drawing.Design; + using System.Globalization; + using System.Linq; + using System.Text; + using System.Text.RegularExpressions; + using System.Web.Security; + using System.Web.UI; + using DotNetOpenAuth.ComponentModel; + using DotNetOpenAuth.Configuration; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.OpenId.Extensions.UI; + using System.Web; + using DotNetOpenAuth.OpenId.Extensions; + + /// <summary> + /// A common base class for OpenID Relying Party controls. + /// </summary> + public abstract class OpenIdRelyingPartyAjaxControlBase : OpenIdRelyingPartyControlBase, ICallbackEventHandler { + /// <summary> + /// The manifest resource name of the javascript file to include on the hosting page. + /// </summary> + internal const string EmbeddedJavascriptResource = Util.DefaultNamespace + ".OpenId.RelyingParty.OpenIdRelyingPartyAjaxControlBase.js"; + + /// <summary> + /// The name of the javascript function that will initiate a synchronous callback. + /// </summary> + protected const string CallbackJsFunction = "window.dnoa_internal.callback"; + + /// <summary> + /// The name of the javascript function that will initiate an asynchronous callback. + /// </summary> + protected const string CallbackJsFunctionAsync = "window.dnoa_internal.callbackAsync"; + + /// <summary> + /// Stores the result of a AJAX callback discovery. + /// </summary> + 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() { + } + + #region ICallbackEventHandler Members + + /// <summary> + /// Returns the result of discovery on some Identifier passed to <see cref="ICallbackEventHandler.RaiseCallbackEvent"/>. + /// </summary> + /// <returns>The result of the callback.</returns> + /// <value>A whitespace delimited list of URLs that can be used to initiate authentication.</value> + string ICallbackEventHandler.GetCallbackResult() { + this.Page.Response.ContentType = "text/javascript"; + return this.discoveryResult; + } + + /// <summary> + /// Performs discovery on some OpenID Identifier. Called directly from the user agent via + /// AJAX callback mechanisms. + /// </summary> + /// <param name="eventArgument">The identifier to perform discovery on.</param> + void ICallbackEventHandler.RaiseCallbackEvent(string eventArgument) { + string userSuppliedIdentifier = eventArgument; + + ErrorUtilities.VerifyNonZeroLength(userSuppliedIdentifier, "userSuppliedIdentifier"); + Logger.OpenId.InfoFormat("AJAX discovery on {0} requested.", userSuppliedIdentifier); + + // 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 { + this.Identifier = userSuppliedIdentifier; + IEnumerable<IAuthenticationRequest> requests = this.CreateRequests().CacheGeneratedResults(); + if (requests.Any()) { + discoveryResultBuilder.AppendFormat("claimedIdentifier: {0},", MessagingUtilities.GetSafeJavascriptValue(requests.First().ClaimedIdentifier)); + discoveryResultBuilder.Append("requests: ["); + foreach (IAuthenticationRequest request in requests) { + this.OnLoggingIn(request); + 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: new Array(),"); + discoveryResultBuilder.AppendFormat("error: {0}", MessagingUtilities.GetSafeJavascriptValue(OpenIdStrings.OpenIdEndpointNotFound)); + } + } catch (ProtocolException ex) { + discoveryResultBuilder.Append("requests: new Array(),"); + discoveryResultBuilder.AppendFormat("error: {0}", MessagingUtilities.GetSafeJavascriptValue(ex.Message)); + } + discoveryResultBuilder.Append("}"); + this.discoveryResult = discoveryResultBuilder.ToString(); + } + + #endregion + + /// <summary> + /// Creates the authentication requests for a given user-supplied Identifier. + /// </summary> + /// <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 override IEnumerable<IAuthenticationRequest> CreateRequests() +{ + Contract.Requires(this.Identifier != null, OpenIdStrings.NoIdentifierSet); + ErrorUtilities.VerifyOperation(this.Identifier != null, OpenIdStrings.NoIdentifierSet); + IEnumerable<IAuthenticationRequest> requests = base.CreateRequests(); + + // Configure each generated request. + int reqIndex = 0; + foreach (var req in requests) { + req.AddCallbackArguments("index", (reqIndex++).ToString(CultureInfo.InvariantCulture)); + + if (IsPopupAppropriate(req)) { + // 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.AddCallbackArguments("dotnetopenid.popupUISupported", "1"); + } + + // If the ReturnToUrl was explicitly set, we'll need to reset our first parameter + if (string.IsNullOrEmpty(HttpUtility.ParseQueryString(req.ReturnToUrl.Query)["dotnetopenid.userSuppliedIdentifier"])) { + req.AddCallbackArguments("dotnetopenid.userSuppliedIdentifier", this.Identifier); + } + + // 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.AddCallbackArguments("dotnetopenid.op_endpoint", req.Provider.Uri.AbsoluteUri); + req.AddCallbackArguments("dotnetopenid.claimed_id", (string)req.ClaimedIdentifier ?? string.Empty); + ((AuthenticationRequest)req).AssociationPreference = AssociationPreference.IfAlreadyEstablished; + + // 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; + } + } + + private string GetJsCallbackConvenienceFunction(bool async) { + string argumentParameterName = "argument"; + string callbackResultParameterName = "resultFunction"; + string callbackErrorCallbackParameterName = "errorCallback"; + string callback = Page.ClientScript.GetCallbackEventReference( + this, + argumentParameterName, + callbackResultParameterName, + argumentParameterName, + callbackErrorCallbackParameterName, + async); + return string.Format( + CultureInfo.InvariantCulture, + "function({1}, {2}, {3}) {{{0}\treturn {4};{0}}};", + Environment.NewLine, + argumentParameterName, + callbackResultParameterName, + callbackErrorCallbackParameterName, + callback); + } + + /// <summary> + /// Raises the <see cref="E:System.Web.UI.Control.PreRender"/> event. + /// </summary> + /// <param name="e">An <see cref="T:System.EventArgs"/> object that contains the event data.</param> + protected override void OnPreRender(EventArgs e) { + base.OnPreRender(e); + + this.Page.ClientScript.RegisterClientScriptResource(typeof(OpenIdRelyingPartyControlBase), EmbeddedJavascriptResource); + + StringBuilder initScript = new StringBuilder(); + + initScript.AppendLine(CallbackJsFunctionAsync + " = " + GetJsCallbackConvenienceFunction(true)); + initScript.AppendLine(CallbackJsFunction + " = " + GetJsCallbackConvenienceFunction(false)); + + this.Page.ClientScript.RegisterClientScriptBlock(typeof(OpenIdRelyingPartyControlBase), "initializer", initScript.ToString(), true); + } + + /// <summary> + /// Notifies the user agent via an AJAX response of a completed authentication attempt. + /// </summary> + private void ReportAuthenticationResult() { + Logger.OpenId.InfoFormat("AJAX (iframe) callback from OP: {0}", this.Page.Request.Url); + List<string> assignments = new List<string>(); + + var authResponse = this.RelyingParty.GetResponse(); + if (authResponse.Status == AuthenticationStatus.Authenticated) { + this.OnLoggedIn(authResponse); + 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)) { + js = "null"; + } + assignments.Add(pair.Value + " = " + js); + } + } + + this.CallbackUserAgentMethod("dnoi_internal.processAuthorizationResult(document.URL)", assignments.ToArray()); + } + + /// <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) { + this.CallbackUserAgentMethod(methodCall, null); + } + + /// <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> + /// <param name="preAssignments">An optional list of assignments to make to the input box object before placing the method call.</param> + private void CallbackUserAgentMethod(string methodCall, string[] preAssignments) { + Logger.OpenId.InfoFormat("Sending Javascript callback: {0}", methodCall); + Page.Response.Write(@"<html><body><script language='javascript'> + var inPopup = !window.frameElement; + var objSrc = inPopup ? window.opener.waiting_openidBox : window.frameElement.openidBox; +"); + if (preAssignments != null) { + foreach (string assignment in preAssignments) { + Page.Response.Write(string.Format(CultureInfo.InvariantCulture, " objSrc.{0};\n", assignment)); + } + } + + // 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(); + } + } +} diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingPartyControlBase.ReturnTo.html b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingPartyControlBase.ReturnTo.html deleted file mode 100644 index 22ac9a2..0000000 --- a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingPartyControlBase.ReturnTo.html +++ /dev/null @@ -1,11 +0,0 @@ -<html> - <head> - <script type="text/javascript" language="javascript"> - //<![CDATA[ - alert("Hey, it worked: " + location); - //]]> - </script> - </head> - <body> - </body> -</html>
\ No newline at end of file diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingPartyControlBase.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingPartyControlBase.cs index 9ff78bf..c1c31e1 100644 --- a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingPartyControlBase.cs +++ b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingPartyControlBase.cs @@ -5,7 +5,6 @@ //----------------------------------------------------------------------- [assembly: System.Web.UI.WebResource(DotNetOpenAuth.OpenId.RelyingParty.OpenIdRelyingPartyControlBase.EmbeddedJavascriptResource, "text/javascript")] -[assembly: System.Web.UI.WebResource(DotNetOpenAuth.OpenId.RelyingParty.OpenIdRelyingPartyControlBase.ReturnToStaticPageResource, "text/html")] namespace DotNetOpenAuth.OpenId.RelyingParty { using System; @@ -25,31 +24,18 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { using DotNetOpenAuth.Messaging; using DotNetOpenAuth.OpenId.Extensions.UI; using System.Web; + using DotNetOpenAuth.OpenId.Extensions; /// <summary> /// A common base class for OpenID Relying Party controls. /// </summary> - public abstract class OpenIdRelyingPartyControlBase : Control, ICallbackEventHandler { + [DefaultProperty("Identifier"), ValidationProperty("Identifier")] + public abstract class OpenIdRelyingPartyControlBase : Control { /// <summary> /// The manifest resource name of the javascript file to include on the hosting page. /// </summary> internal const string EmbeddedJavascriptResource = Util.DefaultNamespace + ".OpenId.RelyingParty.OpenIdRelyingPartyControlBase.js"; - /// <summary> - /// The manifest resource name of the static HTML file that serves as the return_to URL for popup windows and iframes. - /// </summary> - internal const string ReturnToStaticPageResource = Util.DefaultNamespace + ".OpenId.RelyingParty.OpenIdRelyingPartyControlBase.ReturnTo.html"; - - /// <summary> - /// The name of the javascript function that will initiate a synchronous callback. - /// </summary> - protected const string CallbackJsFunction = "window.dnoa_internal.callback"; - - /// <summary> - /// The name of the javascript function that will initiate an asynchronous callback. - /// </summary> - protected const string CallbackJsFunctionAsync = "window.dnoa_internal.callbackAsync"; - #region Property category constants /// <summary> @@ -72,6 +58,11 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { private const bool StatelessDefault = false; /// <summary> + /// The default value for the <see cref="ReturnToUrl"/> property. + /// </summary> + private const string ReturnToUrlDefault = ""; + + /// <summary> /// Default value of <see cref="UsePersistentCookie"/>. /// </summary> private const bool UsePersistentCookieDefault = false; @@ -111,6 +102,11 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { private const string RealmUrlViewStateKey = "RealmUrl"; /// <summary> + /// The viewstate key to use for the <see cref="ReturnToUrl"/> property. + /// </summary> + private const string ReturnToUrlViewStateKey = "ReturnToUrl"; + + /// <summary> /// The key under which the value for the <see cref="Identifier"/> property will be stored. /// </summary> private const string IdentifierViewStateKey = "Identifier"; @@ -132,7 +128,7 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { /// <summary> /// The callback parameter for use with persisting the <see cref="UsePersistentCookie"/> property. /// </summary> - private const string UsePersistentCookieCallbackKey = "OpenIdTextBox_UsePersistentCookie"; + private const string UsePersistentCookieCallbackKey = OpenIdUtilities.CustomParameterPrefix + "UsePersistentCookie"; /// <summary> /// The callback parameter to use for recognizing when the callback is in a popup window. @@ -144,12 +140,9 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { /// </summary> private const string UIPopupCallbackParentKey = OpenIdUtilities.CustomParameterPrefix + "uipopupParent"; - #endregion + private const string ReturnToReceivingControlId = OpenIdUtilities.CustomParameterPrefix + "receiver"; - /// <summary> - /// Stores the result of a AJAX callback discovery. - /// </summary> - private string discoveryResult; + #endregion /// <summary> /// Backing field for the <see cref="RelyingParty"/> property. @@ -260,6 +253,39 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { } /// <summary> + /// Gets or sets the OpenID ReturnTo of the relying party web site. + /// </summary> + [SuppressMessage("Microsoft.Usage", "CA2234:PassSystemUriObjectsInsteadOfStrings", Justification = "Bindable property must be simple type")] + [SuppressMessage("Microsoft.Usage", "CA1806:DoNotIgnoreMethodResults", MessageId = "System.Uri", Justification = "Using Uri.ctor for validation.")] + [SuppressMessage("Microsoft.Design", "CA1056:UriPropertiesShouldNotBeStrings", Justification = "Bindable property must be simple type")] + [Bindable(true), DefaultValue(ReturnToUrlDefault), Category(BehaviorCategory)] + [Description("The OpenID ReturnTo of the relying party web site.")] + [UrlProperty, Editor("System.Web.UI.Design.UrlEditor, System.Design, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a", typeof(UITypeEditor))] + public string ReturnToUrl { + get { + return (string)(this.ViewState[ReturnToUrlViewStateKey] ?? ReturnToUrlDefault); + } + + set { + if (this.Page != null && !this.DesignMode) { + // Validate new value by trying to construct a Uri based on it. + new Uri(this.RelyingParty.Channel.GetRequestFromContext().UrlBeforeRewriting, this.Page.ResolveUrl(value)); // throws an exception on failure. + } else { + // We can't fully test it, but it should start with either ~/ or a protocol. + if (Regex.IsMatch(value, @"^https?://")) { + new Uri(value); // make sure it's fully-qualified, but ignore wildcards + } else if (value.StartsWith("~/", StringComparison.Ordinal)) { + // this is valid too + } else { + throw new UriFormatException(); + } + } + + this.ViewState[ReturnToUrlViewStateKey] = value; + } + } + + /// <summary> /// Gets or sets a value indicating whether to send a persistent cookie upon successful /// login so the user does not have to log in upon returning to this site. /// </summary> @@ -318,104 +344,38 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { } } - #region ICallbackEventHandler Members - - /// <summary> - /// Returns the result of discovery on some Identifier passed to <see cref="ICallbackEventHandler.RaiseCallbackEvent"/>. - /// </summary> - /// <returns>The result of the callback.</returns> - /// <value>A whitespace delimited list of URLs that can be used to initiate authentication.</value> - string ICallbackEventHandler.GetCallbackResult() { - this.Page.Response.ContentType = "text/javascript"; - return this.discoveryResult; - } - - /// <summary> - /// Performs discovery on some OpenID Identifier. Called directly from the user agent via - /// AJAX callback mechanisms. - /// </summary> - /// <param name="eventArgument">The identifier to perform discovery on.</param> - void ICallbackEventHandler.RaiseCallbackEvent(string eventArgument) { - string userSuppliedIdentifier = eventArgument; - - ErrorUtilities.VerifyNonZeroLength(userSuppliedIdentifier, "userSuppliedIdentifier"); - Logger.OpenId.InfoFormat("AJAX discovery on {0} requested.", userSuppliedIdentifier); - - // 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 { - this.Identifier = userSuppliedIdentifier; - IEnumerable<IAuthenticationRequest> requests = this.CreateRequests().CacheGeneratedResults(); - if (requests.Any()) { - discoveryResultBuilder.AppendFormat("claimedIdentifier: {0},", MessagingUtilities.GetSafeJavascriptValue(requests.First().ClaimedIdentifier)); - discoveryResultBuilder.Append("requests: ["); - foreach (IAuthenticationRequest request in requests) { - this.OnLoggingIn(request); - 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: new Array(),"); - discoveryResultBuilder.AppendFormat("error: {0}", MessagingUtilities.GetSafeJavascriptValue(OpenIdStrings.OpenIdEndpointNotFound)); - } - } catch (ProtocolException ex) { - discoveryResultBuilder.Append("requests: new Array(),"); - discoveryResultBuilder.AppendFormat("error: {0}", MessagingUtilities.GetSafeJavascriptValue(ex.Message)); - } - discoveryResultBuilder.Append("}"); - this.discoveryResult = discoveryResultBuilder.ToString(); - } - - #endregion - /// <summary> /// Creates the authentication requests for a given user-supplied Identifier. /// </summary> /// <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> CreateRequests() { + protected virtual IEnumerable<IAuthenticationRequest> CreateRequests() { Contract.Requires(this.Identifier != null, OpenIdStrings.NoIdentifierSet); ErrorUtilities.VerifyOperation(this.Identifier != null, OpenIdStrings.NoIdentifierSet); + IEnumerable<IAuthenticationRequest> requests; - // The return_to page will be a static resource that has simple javascript to pass the openid response - // to the parent frame or window. - Uri returnTo = new Uri( - this.RelyingParty.Channel.GetRequestFromContext().UrlBeforeRewriting, - this.Page.ClientScript.GetWebResourceUrl(typeof(OpenIdRelyingPartyControlBase), ReturnToStaticPageResource)); + // Approximate the returnTo (either based on the customize property or the page URL) + // so we can use it to help with Realm resolution. + Uri returnToApproximation = this.ReturnToUrl != null ? new Uri(this.RelyingParty.Channel.GetRequestFromContext().UrlBeforeRewriting, this.ReturnToUrl) : this.Page.Request.Url; // Resolve the trust root, and swap out the scheme and port if necessary to match the // return_to URL, since this match is required by OpenId, and the consumer app // may be using HTTP at some times and HTTPS at others. UriBuilder realm = OpenIdUtilities.GetResolvedRealm(this.Page, this.RealmUrl, this.RelyingParty.Channel.GetRequestFromContext()); - realm.Scheme = returnTo.Scheme; - realm.Port = returnTo.Port; + realm.Scheme = returnToApproximation.Scheme; + realm.Port = returnToApproximation.Port; // Initiate openid request // We use TryParse here to avoid throwing an exception which // might slip through our validator control if it is disabled. Realm typedRealm = new Realm(realm); - var requests = this.RelyingParty.CreateRequests(this.Identifier, typedRealm, returnTo); + if (string.IsNullOrEmpty(this.ReturnToUrl)) { + requests = this.RelyingParty.CreateRequests(this.Identifier, typedRealm); + } else { + // Since the user actually gave us a return_to value, + // the "approximation" is exactly what we want. + requests = this.RelyingParty.CreateRequests(this.Identifier, typedRealm, returnToApproximation); + } // 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 @@ -423,47 +383,20 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { requests = requests.Distinct(DuplicateRequestedHostsComparer.Instance); // Configure each generated request. - int reqIndex = 0; foreach (var req in requests) { - req.AddCallbackArguments("index", (reqIndex++).ToString(CultureInfo.InvariantCulture)); - if (this.IsPopupAppropriate(req)) { // Inform the OP that we'll be using a popup window. 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.AddCallbackArguments("dotnetopenid.popupUISupported", "1"); } // Add state that needs to survive across the redirect. if (!this.Stateless) { req.AddCallbackArguments(UsePersistentCookieCallbackKey, this.UsePersistentCookie.ToString(CultureInfo.InvariantCulture)); + req.AddCallbackArguments(ReturnToReceivingControlId, this.ClientID); } - // If the ReturnToUrl was explicitly set, we'll need to reset our first parameter - if (string.IsNullOrEmpty(HttpUtility.ParseQueryString(req.ReturnToUrl.Query)["dotnetopenid.userSuppliedIdentifier"])) { - req.AddCallbackArguments("dotnetopenid.userSuppliedIdentifier", this.Identifier); - } - - // 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.AddCallbackArguments("dotnetopenid.op_endpoint", req.Provider.Uri.AbsoluteUri); - req.AddCallbackArguments("dotnetopenid.claimed_id", (string)req.ClaimedIdentifier ?? string.Empty); - req.AddCallbackArguments("dotnetopenid.phase", "2"); - ((AuthenticationRequest)req).AssociationPreference = AssociationPreference.IfAlreadyEstablished; - this.OnLoggingIn(req); - // 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; } } @@ -480,74 +413,41 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { return; } - var response = this.RelyingParty.GetResponse(); - if (response != null) { - string persistentString = response.GetCallbackArgument(UsePersistentCookieCallbackKey); - bool persistentBool; - if (persistentString != null && bool.TryParse(persistentString, out persistentBool)) { - this.UsePersistentCookie = persistentBool; - } + // Only sniff for an OpenID response if it is targeted at this control. Note that + // Stateless mode causes no receiver to be indicated. + string receiver = this.Page.Request.QueryString[ReturnToReceivingControlId] ?? this.Page.Request.Form[ReturnToReceivingControlId]; + if (receiver == null || receiver == this.ClientID) { + var response = this.RelyingParty.GetResponse(); + if (response != null) { + string persistentString = response.GetCallbackArgument(UsePersistentCookieCallbackKey); + bool persistentBool; + if (persistentString != null && bool.TryParse(persistentString, out persistentBool)) { + this.UsePersistentCookie = persistentBool; + } - switch (response.Status) { - case AuthenticationStatus.Authenticated: - this.OnLoggedIn(response); - break; - case AuthenticationStatus.Canceled: - this.OnCanceled(response); - break; - case AuthenticationStatus.Failed: - this.OnFailed(response); - break; - case AuthenticationStatus.SetupRequired: - case AuthenticationStatus.ExtensionsOnly: - default: - // The NotApplicable (extension-only assertion) is NOT one that we support - // in this control because that scenario is primarily interesting to RPs - // that are asking a specific OP, and it is not user-initiated as this textbox - // is designed for. - throw new InvalidOperationException(MessagingStrings.UnexpectedMessageReceivedOfMany); + switch (response.Status) { + case AuthenticationStatus.Authenticated: + this.OnLoggedIn(response); + break; + case AuthenticationStatus.Canceled: + this.OnCanceled(response); + break; + case AuthenticationStatus.Failed: + this.OnFailed(response); + break; + case AuthenticationStatus.SetupRequired: + case AuthenticationStatus.ExtensionsOnly: + default: + // The NotApplicable (extension-only assertion) is NOT one that we support + // in this control because that scenario is primarily interesting to RPs + // that are asking a specific OP, and it is not user-initiated as this textbox + // is designed for. + throw new InvalidOperationException(MessagingStrings.UnexpectedMessageReceivedOfMany); + } } } } - private string GetJsCallbackConvenienceFunction(bool async) { - string argumentParameterName = "argument"; - string callbackResultParameterName = "resultFunction"; - string callbackErrorCallbackParameterName = "errorCallback"; - string callback = Page.ClientScript.GetCallbackEventReference( - this, - argumentParameterName, - callbackResultParameterName, - argumentParameterName, - callbackErrorCallbackParameterName, - async); - return string.Format( - CultureInfo.InvariantCulture, - "function({1}, {2}, {3}) {{{0}\treturn {4};{0}}};", - Environment.NewLine, - argumentParameterName, - callbackResultParameterName, - callbackErrorCallbackParameterName, - callback); - } - - /// <summary> - /// Raises the <see cref="E:System.Web.UI.Control.PreRender"/> event. - /// </summary> - /// <param name="e">An <see cref="T:System.EventArgs"/> object that contains the event data.</param> - protected override void OnPreRender(EventArgs e) { - base.OnPreRender(e); - - this.Page.ClientScript.RegisterClientScriptResource(typeof(OpenIdRelyingPartyControlBase), EmbeddedJavascriptResource); - - StringBuilder initScript = new StringBuilder(); - - initScript.AppendLine(CallbackJsFunctionAsync + " = " + GetJsCallbackConvenienceFunction(true)); - initScript.AppendLine(CallbackJsFunction + " = " + GetJsCallbackConvenienceFunction(false)); - - this.Page.ClientScript.RegisterClientScriptBlock(typeof(OpenIdRelyingPartyControlBase), "initializer", initScript.ToString(), true); - } - /// <summary> /// Fires the <see cref="LoggedIn"/> event. /// </summary> @@ -647,7 +547,7 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { /// <returns> /// <c>true</c> if a popup should be used; <c>false</c> otherwise. /// </returns> - private bool IsPopupAppropriate(IAuthenticationRequest request) { + protected virtual bool IsPopupAppropriate(IAuthenticationRequest request) { Contract.Requires(request != null); ErrorUtilities.VerifyArgumentNotNull(request, "request"); @@ -744,6 +644,5 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { #endregion } - } } |