diff options
4 files changed, 189 insertions, 38 deletions
diff --git a/projecttemplates/MvcRelyingParty/Controllers/AccountController.cs b/projecttemplates/MvcRelyingParty/Controllers/AccountController.cs index 52f8831..f6f4275 100644 --- a/projecttemplates/MvcRelyingParty/Controllers/AccountController.cs +++ b/projecttemplates/MvcRelyingParty/Controllers/AccountController.cs @@ -105,6 +105,20 @@ } /// <summary> + /// Handles the positive assertion that comes from Providers to Javascript running in the browser. + /// </summary> + /// <returns>The action result.</returns> + /// <remarks> + /// This method instructs ASP.NET MVC to <i>not</i> validate input + /// because some OpenID positive assertions messages otherwise look like + /// hack attempts and result in errors when validation is turned on. + /// </remarks> + [AcceptVerbs(HttpVerbs.Get | HttpVerbs.Post), ValidateInput(false)] + public ActionResult LogOnReturnToAjax() { + return RelyingPartyUtilities.AjaxReturnTo(this.Request); + } + + /// <summary> /// Handles the positive assertion that comes from Providers. /// </summary> /// <returns>The action result.</returns> @@ -157,43 +171,7 @@ throw new InvalidOperationException(); } - // 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. - // } - IEnumerable<IAuthenticationRequest> requests = this.RelyingParty.CreateRequests(identifier, Realm.AutoDetect, Url.ActionFull("LogOnReturnTo")).CacheGeneratedResults(); - if (requests.Any()) { - return new JsonResult { - Data = new { - claimedIdentifier = requests.First().ClaimedIdentifier, - requests = requests.Select(req => new { - endpoint = req.Provider.Uri.AbsoluteUri, - immediate = GetRedirectUrl(req, true), - setup = GetRedirectUrl(req, false), - }).ToArray() - }, - }; - } else { - return new JsonResult { - Data = new { - requests = new object [0], - error = "No OpenID endpoint found", - } - }; - } - } - - private Uri GetRedirectUrl(IAuthenticationRequest request, bool immediate) { - request.Mode = immediate ? AuthenticationRequestMode.Immediate : AuthenticationRequestMode.Setup; - return request.RedirectingResponse.GetDirectUriRequest(this.RelyingParty.Channel); + return RelyingPartyUtilities.AjaxDiscover(identifier, Realm.AutoDetect, Url.ActionFull("LogOnReturnToAjax")); } [Authorize] diff --git a/projecttemplates/MvcRelyingParty/Views/Account/LogOn.aspx b/projecttemplates/MvcRelyingParty/Views/Account/LogOn.aspx index bdc5411..9fe8f8b 100644 --- a/projecttemplates/MvcRelyingParty/Views/Account/LogOn.aspx +++ b/projecttemplates/MvcRelyingParty/Views/Account/LogOn.aspx @@ -39,6 +39,11 @@ <% } %> </asp:Content> <asp:Content ID="Content1" ContentPlaceHolderID="ScriptsArea" runat="server"> + <script type="text/javascript" language="javascript"><!-- + //<![CDATA[ + window.openid_visible_iframe = true; // causes the hidden iframe to show up + window.openid_trace = true; // causes lots of messages + //]]>--></script> <script type="text/javascript" src="../../Scripts/MicrosoftAjax.js"></script> <script type="text/javascript" src="../../Scripts/MicrosoftMvcAjax.js"></script> <script type="text/javascript" src="../../Scripts/jquery-1.3.2.min.js"></script> diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingPartyControlBase.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingPartyControlBase.cs index a4a60d1..2c6c59d 100644 --- a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingPartyControlBase.cs +++ b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingPartyControlBase.cs @@ -910,7 +910,9 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { // 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()); + // Only resolve the realm URL if we're actually hosted in a page, as opposed to being invoked + // from RelyingPartyUtilities. + UriBuilder realm = this.Page != null ? OpenIdUtilities.GetResolvedRealm(this.Page, this.RealmUrl, this.RelyingParty.Channel.GetRequestFromContext()) : new UriBuilder(this.RealmUrl); realm.Scheme = returnToApproximation.Scheme; realm.Port = returnToApproximation.Port; diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/RelyingPartyUtilities.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/RelyingPartyUtilities.cs new file mode 100644 index 0000000..4374fd6 --- /dev/null +++ b/src/DotNetOpenAuth/OpenId/RelyingParty/RelyingPartyUtilities.cs @@ -0,0 +1,166 @@ +//----------------------------------------------------------------------- +// <copyright file="RelyingPartyUtilities.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.Contracts; + using System.IO; + using System.Linq; + using System.Text; + using System.Web.Mvc; + using System.Web.UI; + using System.Web; + using System.Globalization; + using DotNetOpenAuth.Messaging; + + public static class RelyingPartyUtilities { + public static string OpenIdAjaxTextBox(this HtmlHelper helper, string name) { + Contract.Requires<ArgumentException>(!string.IsNullOrEmpty(name)); + + var box = new OpenIdAjaxTextBox(); + box.Name = name; + + StringWriter sw = new StringWriter(); + HtmlTextWriter writer = new HtmlTextWriter(sw); + box.RenderControl(writer); + return sw.ToString(); + } + + /// <summary> + /// Performs discovery on some identifier on behalf of Javascript running on the browser. + /// </summary> + /// <param name="identifier">The identifier on which to perform discovery.</param> + /// <param name="realm">The realm of the relying party.</param> + /// <param name="returnTo">The URL that receives OpenID responses.</param> + /// <returns>The JSON result to return to the user agent.</returns> + /// <remarks> + /// 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. + /// } + /// </remarks> + public static JsonResult AjaxDiscover(Identifier identifier, Realm realm, Uri returnTo) { + Contract.Requires<ArgumentNullException>(identifier != null); + Contract.Requires<ArgumentNullException>(realm != null); + Contract.Requires<ArgumentNullException>(returnTo != null); + + OpenIdSelector selector = new OpenIdSelector(); + selector.RealmUrl = realm; + selector.ReturnToUrl = returnTo.AbsoluteUri; + selector.ID = "MVC"; + var requests = selector.CreateRequests(identifier); + if (requests.Any()) { + return new JsonResult { + Data = new { + claimedIdentifier = requests.First().ClaimedIdentifier, + requests = requests.Select(req => new { + endpoint = req.Provider.Uri.AbsoluteUri, + immediate = GetRedirectUrl(selector.RelyingParty, req, true), + setup = GetRedirectUrl(selector.RelyingParty, req, false), + }).ToArray() + }, + }; + } else { + return new JsonResult { + Data = new { + requests = new object[0], + error = "No OpenID endpoint found", + } + }; + } + } + + public static ActionResult AjaxReturnTo(HttpRequestBase request) { + Contract.Requires<ArgumentNullException>(request != null); + + Logger.OpenId.DebugFormat("AJAX (iframe) callback from OP: {0}", request.Url); + string extensionsJson = null; + var RelyingPartyNonVerifying = OpenIdRelyingParty.CreateNonVerifying(); + var authResponse = RelyingPartyNonVerifying.GetResponse(); + Logger.Controls.DebugFormat( + "The MVC controller checked for an authentication response from a popup window or iframe using a non-verifying RP and found: {0}", + 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; + // } + //} + + 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 CallbackUserAgentMethod("dnoa_internal.processAuthorizationResult(" + payload + ")"); + } + + private static Uri GetRedirectUrl(OpenIdRelyingParty rp, IAuthenticationRequest request, bool immediate) { + request.Mode = immediate ? AuthenticationRequestMode.Immediate : AuthenticationRequestMode.Setup; + return request.RedirectingResponse.GetDirectUriRequest(rp.Channel); + } + + /// <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 static ActionResult CallbackUserAgentMethod(string 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>"); + return new ContentResult { Content = builder.ToString(), ContentType="text/html" }; + } + } +} |