//-----------------------------------------------------------------------
//
// 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.Diagnostics.Contracts;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Web;
using System.Web.Script.Serialization;
using System.Web.UI;
using DotNetOpenAuth.Configuration;
using DotNetOpenAuth.Messaging;
using DotNetOpenAuth.OpenId.Extensions;
///
/// 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 property.
///
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 completed authentication response.
///
public IAuthenticationResponse AuthenticationResponse {
get {
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;
Uri authUri = new Uri(formAuthData);
HttpRequestInfo clientResponseInfo = new HttpRequestInfo {
UrlBeforeRewriting = authUri,
};
this.authenticationResponse = this.RelyingParty.GetResponse(clientResponseInfo);
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;
}
}
///
/// 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; }
}
///
/// 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;
var serializer = new JavaScriptSerializer();
IEnumerable requests = this.CreateRequests(this.Identifier);
this.discoveryResult = serializer.Serialize(this.AjaxRelyingParty.AsJsonDiscoveryResult(requests));
}
///
/// 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(IOpenIdApplicationStore 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.
protected void PreloadDiscovery(Identifier identifier) {
this.PreloadDiscovery(new[] { identifier });
}
///
/// 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.
protected void PreloadDiscovery(IEnumerable identifiers) {
string script = this.AjaxRelyingParty.AsAjaxPreloadedDiscoveryResult(
identifiers.SelectMany(id => this.CreateRequests(id)));
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) {
if (this.AuthenticationResponse != null && !this.AuthenticationProcessedAlready) {
// Only process messages targeted at this control.
// Note that Stateless mode causes no receiver to be indicated.
string receiver = this.AuthenticationResponse.GetUntrustedCallbackArgument(ReturnToReceivingControlId);
if (receiver == null || receiver == this.ClientID) {
this.ProcessResponse(this.AuthenticationResponse);
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) {
Contract.Assume(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.
///
protected override void ScriptClosingPopupOrIFrame() {
Action callback = status => {
if (status == AuthenticationStatus.Authenticated) {
this.OnUnconfirmedPositiveAssertion(); // event handler will fill the clientScriptExtensions collection.
}
};
OutgoingWebResponse response = this.RelyingParty.ProcessResponseFromPopup(
this.RelyingParty.Channel.GetRequestFromContext(),
callback);
response.Send();
}
///
/// 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);
}
}
}