//-----------------------------------------------------------------------
//
// 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);
}
}
}