//----------------------------------------------------------------------- // // Copyright (c) Outercurve Foundation. All rights reserved. // //----------------------------------------------------------------------- [assembly: System.Web.UI.WebResource(DotNetOpenAuth.OpenId.RelyingParty.OpenIdRelyingPartyAjaxControlBase.EmbeddedAjaxJavascriptResource, "text/javascript")] namespace DotNetOpenAuth.OpenId.RelyingParty { using System; using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Linq; using System.Net.Http; using System.Text; using System.Threading; using System.Threading.Tasks; using System.Web; using System.Web.Script.Serialization; using System.Web.UI; using DotNetOpenAuth.Configuration; using DotNetOpenAuth.Logging; using DotNetOpenAuth.Messaging; using DotNetOpenAuth.Messaging.Bindings; using DotNetOpenAuth.OpenId.Extensions; using Validation; /// /// A common base class for OpenID Relying Party controls. /// public abstract class OpenIdRelyingPartyAjaxControlBase : OpenIdRelyingPartyControlBase, ICallbackEventHandler { /// /// The manifest resource name of the javascript file to include on the hosting page. /// internal const string EmbeddedAjaxJavascriptResource = Util.DefaultNamespace + ".OpenId.RelyingParty.OpenIdRelyingPartyAjaxControlBase.js"; /// /// The "dnoa.op_endpoint" string. /// internal const string OPEndpointParameterName = OpenIdUtilities.CustomParameterPrefix + "op_endpoint"; /// /// The "dnoa.claimed_id" string. /// internal const string ClaimedIdParameterName = OpenIdUtilities.CustomParameterPrefix + "claimed_id"; /// /// The name of the javascript field that stores the maximum time a positive assertion is /// good for before it must be refreshed. /// internal const string MaxPositiveAssertionLifetimeJsName = "window.dnoa_internal.maxPositiveAssertionLifetime"; /// /// The name of the javascript function that will initiate an asynchronous callback. /// protected internal const string CallbackJSFunctionAsync = "window.dnoa_internal.callbackAsync"; /// /// The name of the javascript function that will initiate a synchronous callback. /// protected const string CallbackJSFunction = "window.dnoa_internal.callback"; #region Property viewstate keys /// /// The viewstate key to use for storing the value of a successful authentication. /// private const string AuthDataViewStateKey = "AuthData"; /// /// The viewstate key to use for storing the value of the method. /// private const string AuthenticationResponseViewStateKey = "AuthenticationResponse"; /// /// The viewstate key to use for storing the value of the property. /// private const string AuthenticationProcessedAlreadyViewStateKey = "AuthenticationProcessedAlready"; #endregion /// /// Default value of the property. /// private const PopupBehavior PopupDefault = PopupBehavior.Always; /// /// Default value of property.. /// private const LogOnSiteNotification LogOnModeDefault = LogOnSiteNotification.None; /// /// The authentication response that just came in. /// private IAuthenticationResponse authenticationResponse; /// /// Stores the result of an AJAX discovery request while it is waiting /// to be picked up by ASP.NET on the way down to the user agent. /// private string discoveryResult; /// /// Initializes a new instance of the class. /// protected OpenIdRelyingPartyAjaxControlBase() { // The AJAX login style always uses popups (or invisible iframes). base.Popup = PopupDefault; // The expected use case for the AJAX login box is for comments... not logging in. this.LogOnMode = LogOnModeDefault; } /// /// Fired when a Provider sends back a positive assertion to this control, /// but the authentication has not yet been verified. /// /// /// No security critical decisions should be made within event handlers /// for this event as the authenticity of the assertion has not been /// verified yet. All security related code should go in the event handler /// for the event. /// [Description("Fired when a Provider sends back a positive assertion to this control, but the authentication has not yet been verified.")] public event EventHandler UnconfirmedPositiveAssertion; /// /// Gets or sets a value indicating when to use a popup window to complete the login experience. /// /// The default value is . [Bindable(false), Browsable(false), DefaultValue(PopupDefault)] public override PopupBehavior Popup { get { return base.Popup; } set { ErrorUtilities.VerifySupported(value == base.Popup, OpenIdStrings.PropertyValueNotSupported); } } /// /// Gets or sets the way a completed login is communicated to the rest of the web site. /// [Bindable(true), DefaultValue(LogOnModeDefault), Category(BehaviorCategory)] [Description("The way a completed login is communicated to the rest of the web site.")] public override LogOnSiteNotification LogOnMode { // override to set new DefaultValue get { return base.LogOnMode; } set { base.LogOnMode = value; } } /// /// Gets or sets the instance to use. /// /// /// The default value is an instance initialized according to the web.config file. /// /// /// 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 Page.Load /// event since instantiating these instances can be expensive on /// heavily trafficked web pages. /// 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; } } /// /// Gets the relying party as its AJAX type. /// protected OpenIdAjaxRelyingParty AjaxRelyingParty { get { return (OpenIdAjaxRelyingParty)this.RelyingParty; } } /// /// Gets the name of the open id auth data form key (for the value as stored at the user agent as a FORM field). /// /// Usually a concatenation of the control's name and "_openidAuthData". protected abstract string OpenIdAuthDataFormKey { get; } /// /// Gets or sets a value indicating whether an authentication in the page's view state /// has already been processed and appropriate events fired. /// private bool AuthenticationProcessedAlready { get { return (bool)(ViewState[AuthenticationProcessedAlreadyViewStateKey] ?? false); } set { ViewState[AuthenticationProcessedAlreadyViewStateKey] = value; } } /// /// Gets the completed authentication response. /// /// The cancellation token. /// The response message. public async Task GetAuthenticationResponseAsync(CancellationToken cancellationToken) { if (this.authenticationResponse == null) { // We will either validate a new response and return a live AuthenticationResponse // or we will try to deserialize a previous IAuthenticationResponse (snapshot) // from viewstate and return that. IAuthenticationResponse viewstateResponse = this.ViewState[AuthenticationResponseViewStateKey] as IAuthenticationResponse; string viewstateAuthData = this.ViewState[AuthDataViewStateKey] as string; string formAuthData = this.Page.Request.Form[this.OpenIdAuthDataFormKey]; // First see if there is fresh auth data to be processed into a response. if (!string.IsNullOrEmpty(formAuthData) && !string.Equals(viewstateAuthData, formAuthData, StringComparison.Ordinal)) { this.ViewState[AuthDataViewStateKey] = formAuthData; HttpRequestBase clientResponseInfo = new HttpRequestInfo("GET", new Uri(formAuthData)); this.authenticationResponse = await this.RelyingParty.GetResponseAsync(clientResponseInfo, cancellationToken); Logger.Controls.DebugFormat( "The {0} control checked for an authentication response and found: {1}", this.ID, this.authenticationResponse.Status); this.AuthenticationProcessedAlready = false; // Save out the authentication response to viewstate so we can find it on // a subsequent postback. this.ViewState[AuthenticationResponseViewStateKey] = new PositiveAuthenticationResponseSnapshot(this.authenticationResponse); } else { this.authenticationResponse = viewstateResponse; } } return this.authenticationResponse; } /// /// 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. /// /// The extension response type that will read data from the assertion. /// The property name on the openid_identifier input box object that will be used to store the extension data. For example: sreg /// /// This method should be called from the event handler. /// [SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter", Justification = "By design")] public void RegisterClientScriptExtension(string propertyName) where T : IClientScriptExtensionResponse { Requires.NotNullOrEmpty(propertyName, "propertyName"); this.RelyingParty.RegisterClientScriptExtension(propertyName); } #region ICallbackEventHandler Members /// /// Returns the result of discovery on some Identifier passed to . /// /// The result of the callback. /// A whitespace delimited list of URLs that can be used to initiate authentication. string ICallbackEventHandler.GetCallbackResult() { return this.GetCallbackResult(); } /// /// Performs discovery on some OpenID Identifier. Called directly from the user agent via /// AJAX callback mechanisms. /// /// The identifier to perform discovery on. [SuppressMessage("Microsoft.Design", "CA1030:UseEventsWhereAppropriate", Justification = "We want to preserve the signature of the interface.")] void ICallbackEventHandler.RaiseCallbackEvent(string eventArgument) { this.RaiseCallbackEvent(eventArgument); } #endregion /// /// Returns the results of a callback event that targets a control. /// /// The result of the callback. [SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate", Justification = "We want to preserve the signature of the interface.")] protected virtual string GetCallbackResult() { this.Page.Response.ContentType = "text/javascript"; return this.discoveryResult; } /// /// Processes a callback event that targets a control. /// /// A string that represents an event argument to pass to the event handler. [SuppressMessage("Microsoft.Design", "CA1030:UseEventsWhereAppropriate", Justification = "We want to preserve the signature of the interface.")] protected virtual void RaiseCallbackEvent(string eventArgument) { string userSuppliedIdentifier = eventArgument; ErrorUtilities.VerifyNonZeroLength(userSuppliedIdentifier, "userSuppliedIdentifier"); Logger.OpenId.InfoFormat("AJAX discovery on {0} requested.", userSuppliedIdentifier); this.Identifier = userSuppliedIdentifier; this.Page.RegisterAsyncTask(new PageAsyncTask(async ct => { var serializer = new JavaScriptSerializer(); IEnumerable requests = await this.CreateRequestsAsync(this.Identifier, ct); this.discoveryResult = serializer.Serialize(await this.AjaxRelyingParty.AsJsonDiscoveryResultAsync(requests, ct)); })); } /// /// Creates the relying party instance used to generate authentication requests. /// /// The store to pass to the relying party constructor. /// The instantiated relying party. protected override OpenIdRelyingParty CreateRelyingParty(ICryptoKeyAndNonceStore store) { return new OpenIdAjaxRelyingParty(store); } /// /// Pre-discovers an identifier and makes the results available to the /// user agent for javascript as soon as the page loads. /// /// The identifier. /// The cancellation token. /// /// A task that completes with the asynchronous operation. /// protected Task PreloadDiscoveryAsync(Identifier identifier, CancellationToken cancellationToken) { return this.PreloadDiscoveryAsync(new[] { identifier }, cancellationToken); } /// /// Pre-discovers a given set of identifiers and makes the results available to the /// user agent for javascript as soon as the page loads. /// /// The identifiers to perform discovery on. /// The cancellation token. /// /// A task that completes with the asynchronous operation. /// protected async Task PreloadDiscoveryAsync(IEnumerable identifiers, CancellationToken cancellationToken) { var requests = await Task.WhenAll(identifiers.Select(id => this.CreateRequestsAsync(id, cancellationToken))); string script = await this.AjaxRelyingParty.AsAjaxPreloadedDiscoveryResultAsync(requests.SelectMany(r => r), cancellationToken); this.Page.ClientScript.RegisterClientScriptBlock(typeof(OpenIdRelyingPartyAjaxControlBase), this.ClientID, script, true); } /// /// Fires the event. /// protected virtual void OnUnconfirmedPositiveAssertion() { var unconfirmedPositiveAssertion = this.UnconfirmedPositiveAssertion; if (unconfirmedPositiveAssertion != null) { unconfirmedPositiveAssertion(this, null); } } /// /// Raises the event. /// /// The instance containing the event data. protected override void OnLoad(EventArgs e) { base.OnLoad(e); // Our parent control ignores all OpenID messages included in a postback, // but our AJAX controls hide an old OpenID message in a postback payload, // so we deserialize it and process it when appropriate. if (this.Page.IsPostBack) { this.Page.RegisterAsyncTask(new PageAsyncTask(async ct => { var response = await this.GetAuthenticationResponseAsync(ct); if (response != null && !this.AuthenticationProcessedAlready) { // Only process messages targeted at this control. // Note that Stateless mode causes no receiver to be indicated. string receiver = response.GetUntrustedCallbackArgument(ReturnToReceivingControlId); if (receiver == null || receiver == this.ClientID) { this.ProcessResponse(response); this.AuthenticationProcessedAlready = true; } } })); } } /// /// Called when the property is changed. /// protected override void OnIdentifierChanged() { base.OnIdentifierChanged(); // Since the identifier changed, make sure we reset any cached authentication on the user agent. this.ViewState.Remove(AuthDataViewStateKey); } /// /// Raises the event. /// /// An object that contains the event data. protected override void OnPreRender(EventArgs e) { base.OnPreRender(e); this.SetWebAppPathOnUserAgent(); this.Page.ClientScript.RegisterClientScriptResource(typeof(OpenIdRelyingPartyAjaxControlBase), EmbeddedAjaxJavascriptResource); StringBuilder initScript = new StringBuilder(); initScript.AppendLine(CallbackJSFunctionAsync + " = " + this.GetJsCallbackConvenienceFunction(true)); initScript.AppendLine(CallbackJSFunction + " = " + this.GetJsCallbackConvenienceFunction(false)); // 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(OpenIdElement.Configuration.MaxAuthenticationTime.TotalMilliseconds, DotNetOpenAuthSection.Messaging.MaximumMessageLifetime.TotalMilliseconds)); initScript.AppendLine(MaxPositiveAssertionLifetimeJsName + " = " + assertionLifetimeInMilliseconds.ToString(CultureInfo.InvariantCulture) + ";"); // We register this callback code explicitly with a specific type rather than the derived-type of the control // to ensure that this discovery callback function is only set ONCE for the HTML document. this.Page.ClientScript.RegisterClientScriptBlock(typeof(OpenIdRelyingPartyControlBase), "initializer", initScript.ToString(), true); } /// /// Sends server control content to a provided object, which writes the content to be rendered on the client. /// /// The object that receives the server control content. protected override void Render(HtmlTextWriter writer) { Assumes.True(writer != null, "Missing contract."); base.Render(writer); // Emit a hidden field to let the javascript on the user agent know if an // authentication has already successfully taken place. string viewstateAuthData = this.ViewState[AuthDataViewStateKey] as string; if (!string.IsNullOrEmpty(viewstateAuthData)) { writer.AddAttribute(HtmlTextWriterAttribute.Name, this.OpenIdAuthDataFormKey); writer.AddAttribute(HtmlTextWriterAttribute.Value, viewstateAuthData, true); writer.AddAttribute(HtmlTextWriterAttribute.Type, "hidden"); writer.RenderBeginTag(HtmlTextWriterTag.Input); writer.RenderEndTag(); } } /// /// Notifies the user agent via an AJAX response of a completed authentication attempt. /// /// The cancellation token. /// /// A task that completes with the asynchronous operation. /// protected override async Task ScriptClosingPopupOrIFrameAsync(CancellationToken cancellationToken) { Action callback = status => { if (status == AuthenticationStatus.Authenticated) { this.OnUnconfirmedPositiveAssertion(); // event handler will fill the clientScriptExtensions collection. } }; HttpResponseMessage response = await this.RelyingParty.ProcessResponseFromPopupAsync( new HttpRequestWrapper(this.Context.Request).AsHttpRequestMessage(), callback, cancellationToken); await response.SendAsync(new HttpContextWrapper(this.Context), cancellationToken); this.Context.Response.End(); } /// /// Constructs a function that will initiate an AJAX callback. /// /// if set to true causes the AJAX callback to be a little more asynchronous. Note that false does not mean the call is absolutely synchronous. /// The string defining a javascript anonymous function that initiates a callback. 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); } /// /// Sets the window.aspnetapppath variable on the user agent so that cookies can be set with the proper path. /// private void SetWebAppPathOnUserAgent() { string script = "window.aspnetapppath = " + MessagingUtilities.GetSafeJavascriptValue(this.Page.Request.ApplicationPath) + ";"; this.Page.ClientScript.RegisterClientScriptBlock(typeof(OpenIdRelyingPartyAjaxControlBase), "webapppath", script, true); } } }