diff options
author | Andrew Arnott <andrewarnott@gmail.com> | 2012-01-29 14:32:45 -0800 |
---|---|---|
committer | Andrew Arnott <andrewarnott@gmail.com> | 2012-01-29 14:32:45 -0800 |
commit | 5fec515095ee10b522f414a03e78f282aaf520dc (patch) | |
tree | 204c75486639c23cdda2ef38b34d7e5050a1a2e3 /src/DotNetOpenAuth.OpenId.RelyingParty.UI/OpenId/RelyingParty/OpenIdRelyingPartyControlBase.cs | |
parent | f1a4155398635a4fd9f485eec817152627682704 (diff) | |
parent | 8f4165ee515728aca3faaa26e8354a40612e85e4 (diff) | |
download | DotNetOpenAuth-5fec515095ee10b522f414a03e78f282aaf520dc.zip DotNetOpenAuth-5fec515095ee10b522f414a03e78f282aaf520dc.tar.gz DotNetOpenAuth-5fec515095ee10b522f414a03e78f282aaf520dc.tar.bz2 |
Merge branch 'splitDlls'.
DNOA now builds and (in some cases) ships as many distinct assemblies.
Diffstat (limited to 'src/DotNetOpenAuth.OpenId.RelyingParty.UI/OpenId/RelyingParty/OpenIdRelyingPartyControlBase.cs')
-rw-r--r-- | src/DotNetOpenAuth.OpenId.RelyingParty.UI/OpenId/RelyingParty/OpenIdRelyingPartyControlBase.cs | 1054 |
1 files changed, 1054 insertions, 0 deletions
diff --git a/src/DotNetOpenAuth.OpenId.RelyingParty.UI/OpenId/RelyingParty/OpenIdRelyingPartyControlBase.cs b/src/DotNetOpenAuth.OpenId.RelyingParty.UI/OpenId/RelyingParty/OpenIdRelyingPartyControlBase.cs new file mode 100644 index 0000000..c86ee1d --- /dev/null +++ b/src/DotNetOpenAuth.OpenId.RelyingParty.UI/OpenId/RelyingParty/OpenIdRelyingPartyControlBase.cs @@ -0,0 +1,1054 @@ +//----------------------------------------------------------------------- +// <copyright file="OpenIdRelyingPartyControlBase.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.Collections.ObjectModel; + 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; + using System.Web.Security; + using System.Web.UI; + using DotNetOpenAuth.ComponentModel; + using DotNetOpenAuth.Configuration; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.OpenId.Extensions; + using DotNetOpenAuth.OpenId.Extensions.UI; + using DotNetOpenAuth.OpenId.Messages; + + /// <summary> + /// Methods of indicating to the rest of the web site that the user has logged in. + /// </summary> + [SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly", MessageId = "OnSite", Justification = "Two words intended.")] + public enum LogOnSiteNotification { + /// <summary> + /// The rest of the web site is unaware that the user just completed an OpenID login. + /// </summary> + None, + + /// <summary> + /// After the <see cref="OpenIdRelyingPartyControlBase.LoggedIn"/> event is fired + /// the control automatically calls <see cref="System.Web.Security.FormsAuthentication.RedirectFromLoginPage(string, bool)"/> + /// with the <see cref="IAuthenticationResponse.ClaimedIdentifier"/> as the username + /// unless the <see cref="OpenIdRelyingPartyControlBase.LoggedIn"/> event handler sets + /// <see cref="OpenIdEventArgs.Cancel"/> property to true. + /// </summary> + FormsAuthentication, + } + + /// <summary> + /// How an OpenID user session should be persisted across visits. + /// </summary> + public enum LogOnPersistence { + /// <summary> + /// The user should only be logged in as long as the browser window remains open. + /// Nothing is persisted to help the user on a return visit. Public kiosk mode. + /// </summary> + Session, + + /// <summary> + /// The user should only be logged in as long as the browser window remains open. + /// The OpenID Identifier is persisted to help expedite re-authentication when + /// the user visits the next time. + /// </summary> + SessionAndPersistentIdentifier, + + /// <summary> + /// The user is issued a persistent authentication ticket so that no login is + /// necessary on their return visit. + /// </summary> + PersistentAuthentication, + } + + /// <summary> + /// A common base class for OpenID Relying Party controls. + /// </summary> + [DefaultProperty("Identifier"), ValidationProperty("Identifier")] + [ParseChildren(true), PersistChildren(false)] + public abstract class OpenIdRelyingPartyControlBase : Control, IPostBackEventHandler, IDisposable { + /// <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 cookie used to persist the Identifier the user logged in with. + /// </summary> + internal const string PersistentIdentifierCookieName = OpenIdUtilities.CustomParameterPrefix + "OpenIDIdentifier"; + + /// <summary> + /// The callback parameter name to use to store which control initiated the auth request. + /// </summary> + internal const string ReturnToReceivingControlId = OpenIdUtilities.CustomParameterPrefix + "receiver"; + + #region Protected internal callback parameter names + + /// <summary> + /// The callback parameter to use for recognizing when the callback is in a popup window or hidden iframe. + /// </summary> + protected internal const string UIPopupCallbackKey = OpenIdUtilities.CustomParameterPrefix + "uipopup"; + + /// <summary> + /// The parameter name to include in the formulated auth request so that javascript can know whether + /// the OP advertises support for the UI extension. + /// </summary> + protected internal const string PopupUISupportedJSHint = OpenIdUtilities.CustomParameterPrefix + "popupUISupported"; + + #endregion + + #region Property category constants + + /// <summary> + /// The "Appearance" category for properties. + /// </summary> + protected const string AppearanceCategory = "Appearance"; + + /// <summary> + /// The "Behavior" category for properties. + /// </summary> + protected const string BehaviorCategory = "Behavior"; + + /// <summary> + /// The "OpenID" category for properties and events. + /// </summary> + protected const string OpenIdCategory = "OpenID"; + + #endregion + + #region Private callback parameter names + + /// <summary> + /// The callback parameter for use with persisting the <see cref="UsePersistentCookie"/> property. + /// </summary> + private const string UsePersistentCookieCallbackKey = OpenIdUtilities.CustomParameterPrefix + "UsePersistentCookie"; + + /// <summary> + /// The callback parameter to use for recognizing when the callback is in the parent window. + /// </summary> + private const string UIPopupCallbackParentKey = OpenIdUtilities.CustomParameterPrefix + "uipopupParent"; + + #endregion + + #region Property default values + + /// <summary> + /// The default value for the <see cref="Stateless"/> property. + /// </summary> + 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 LogOnPersistence UsePersistentCookieDefault = LogOnPersistence.Session; + + /// <summary> + /// Default value of <see cref="LogOnMode"/>. + /// </summary> + private const LogOnSiteNotification LogOnModeDefault = LogOnSiteNotification.FormsAuthentication; + + /// <summary> + /// The default value for the <see cref="RealmUrl"/> property. + /// </summary> + private const string RealmUrlDefault = "~/"; + + /// <summary> + /// The default value for the <see cref="Popup"/> property. + /// </summary> + private const PopupBehavior PopupDefault = PopupBehavior.Never; + + /// <summary> + /// The default value for the <see cref="RequireSsl"/> property. + /// </summary> + private const bool RequireSslDefault = false; + + #endregion + + #region Property view state keys + + /// <summary> + /// The viewstate key to use for storing the value of the <see cref="Extensions"/> property. + /// </summary> + private const string ExtensionsViewStateKey = "Extensions"; + + /// <summary> + /// The viewstate key to use for the <see cref="Stateless"/> property. + /// </summary> + private const string StatelessViewStateKey = "Stateless"; + + /// <summary> + /// The viewstate key to use for the <see cref="UsePersistentCookie"/> property. + /// </summary> + private const string UsePersistentCookieViewStateKey = "UsePersistentCookie"; + + /// <summary> + /// The viewstate key to use for the <see cref="LogOnMode"/> property. + /// </summary> + private const string LogOnModeViewStateKey = "LogOnMode"; + + /// <summary> + /// The viewstate key to use for the <see cref="RealmUrl"/> property. + /// </summary> + 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"; + + /// <summary> + /// The viewstate key to use for the <see cref="Popup"/> property. + /// </summary> + private const string PopupViewStateKey = "Popup"; + + /// <summary> + /// The viewstate key to use for the <see cref="RequireSsl"/> property. + /// </summary> + private const string RequireSslViewStateKey = "RequireSsl"; + + #endregion + + /// <summary> + /// The lifetime of the cookie used to persist the Identifier the user logged in with. + /// </summary> + private static readonly TimeSpan PersistentIdentifierTimeToLiveDefault = TimeSpan.FromDays(14); + + /// <summary> + /// Backing field for the <see cref="RelyingParty"/> property. + /// </summary> + private OpenIdRelyingParty relyingParty; + + /// <summary> + /// A value indicating whether the <see cref="relyingParty"/> field contains + /// an instance that we own and should Dispose. + /// </summary> + private bool relyingPartyOwned; + + /// <summary> + /// Initializes a new instance of the <see cref="OpenIdRelyingPartyControlBase"/> class. + /// </summary> + protected OpenIdRelyingPartyControlBase() { + Reporting.RecordFeatureUse(this); + } + + #region Events + + /// <summary> + /// Fired when the user has typed in their identifier, discovery was successful + /// and a login attempt is about to begin. + /// </summary> + [Description("Fired when the user has typed in their identifier, discovery was successful and a login attempt is about to begin."), Category(OpenIdCategory)] + public event EventHandler<OpenIdEventArgs> LoggingIn; + + /// <summary> + /// Fired upon completion of a successful login. + /// </summary> + [Description("Fired upon completion of a successful login."), Category(OpenIdCategory)] + public event EventHandler<OpenIdEventArgs> LoggedIn; + + /// <summary> + /// Fired when a login attempt fails. + /// </summary> + [Description("Fired when a login attempt fails."), Category(OpenIdCategory)] + public event EventHandler<OpenIdEventArgs> Failed; + + /// <summary> + /// Fired when an authentication attempt is canceled at the OpenID Provider. + /// </summary> + [Description("Fired when an authentication attempt is canceled at the OpenID Provider."), Category(OpenIdCategory)] + public event EventHandler<OpenIdEventArgs> Canceled; + + /// <summary> + /// Occurs when the <see cref="Identifier"/> property is changed. + /// </summary> + protected event EventHandler IdentifierChanged; + + #endregion + + /// <summary> + /// Gets or sets the <see cref="OpenIdRelyingParty"/> instance to use. + /// </summary> + /// <value>The default value is an <see cref="OpenIdRelyingParty"/> instance initialized according to the web.config file.</value> + /// <remarks> + /// 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 <see cref="Control.Load">Page.Load</see> + /// event since instantiating these instances can be expensive on + /// heavily trafficked web pages. + /// </remarks> + [Browsable(false)] + public virtual OpenIdRelyingParty RelyingParty { + get { + if (this.relyingParty == null) { + this.relyingParty = this.CreateRelyingParty(); + this.ConfigureRelyingParty(this.relyingParty); + this.relyingPartyOwned = true; + } + return this.relyingParty; + } + + set { + if (this.relyingPartyOwned && this.relyingParty != null) { + this.relyingParty.Dispose(); + } + + this.relyingParty = value; + this.relyingPartyOwned = false; + } + } + + /// <summary> + /// Gets the collection of extension requests this selector should include in generated requests. + /// </summary> + [PersistenceMode(PersistenceMode.InnerProperty)] + public Collection<IOpenIdMessageExtension> Extensions { + get { + if (this.ViewState[ExtensionsViewStateKey] == null) { + var extensions = new Collection<IOpenIdMessageExtension>(); + this.ViewState[ExtensionsViewStateKey] = extensions; + return extensions; + } else { + return (Collection<IOpenIdMessageExtension>)this.ViewState[ExtensionsViewStateKey]; + } + } + } + + /// <summary> + /// Gets or sets a value indicating whether stateless mode is used. + /// </summary> + [Bindable(true), DefaultValue(StatelessDefault), Category(OpenIdCategory)] + [Description("Controls whether stateless mode is used.")] + public bool Stateless { + get { return (bool)(ViewState[StatelessViewStateKey] ?? StatelessDefault); } + set { ViewState[StatelessViewStateKey] = value; } + } + + /// <summary> + /// Gets or sets the OpenID <see cref="Realm"/> of the relying party web site. + /// </summary> + [SuppressMessage("Microsoft.Usage", "CA1806:DoNotIgnoreMethodResults", MessageId = "System.Uri", Justification = "Using Uri.ctor for validation.")] + [SuppressMessage("Microsoft.Usage", "CA1806:DoNotIgnoreMethodResults", MessageId = "DotNetOpenAuth.OpenId.Realm", Justification = "Using ctor for validation.")] + [SuppressMessage("Microsoft.Design", "CA1056:UriPropertiesShouldNotBeStrings", Justification = "Bindable property must be simple type")] + [Bindable(true), DefaultValue(RealmUrlDefault), Category(OpenIdCategory)] + [Description("The OpenID Realm 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 RealmUrl { + get { + return (string)(ViewState[RealmUrlViewStateKey] ?? RealmUrlDefault); + } + + set { + Requires.NotNullOrEmpty(value, "value"); + + if (Page != null && !DesignMode) { + // Validate new value by trying to construct a Realm object based on it. + new Realm(OpenIdUtilities.GetResolvedRealm(this.Page, value, this.RelyingParty.Channel.GetRequestFromContext())); // 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.Replace("*.", string.Empty)); // make sure it's fully-qualified, but ignore wildcards + } else if (value.StartsWith("~/", StringComparison.Ordinal)) { + // this is valid too + } else { + throw new UriFormatException(); + } + } + ViewState[RealmUrlViewStateKey] = value; + } + } + + /// <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(OpenIdCategory)] + [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> + [Bindable(true), DefaultValue(UsePersistentCookieDefault), Category(BehaviorCategory)] + [Description("Whether to send a persistent cookie upon successful " + + "login so the user does not have to log in upon returning to this site.")] + public virtual LogOnPersistence UsePersistentCookie { + get { return (LogOnPersistence)(this.ViewState[UsePersistentCookieViewStateKey] ?? UsePersistentCookieDefault); } + set { this.ViewState[UsePersistentCookieViewStateKey] = value; } + } + + /// <summary> + /// Gets or sets the way a completed login is communicated to the rest of the web site. + /// </summary> + [Bindable(true), DefaultValue(LogOnModeDefault), Category(BehaviorCategory)] + [Description("The way a completed login is communicated to the rest of the web site.")] + public virtual LogOnSiteNotification LogOnMode { + get { return (LogOnSiteNotification)(this.ViewState[LogOnModeViewStateKey] ?? LogOnModeDefault); } + set { this.ViewState[LogOnModeViewStateKey] = value; } + } + + /// <summary> + /// Gets or sets a value indicating when to use a popup window to complete the login experience. + /// </summary> + /// <value>The default value is <see cref="PopupBehavior.Never"/>.</value> + [Bindable(true), DefaultValue(PopupDefault), Category(BehaviorCategory)] + [Description("When to use a popup window to complete the login experience.")] + public virtual PopupBehavior Popup { + get { return (PopupBehavior)(ViewState[PopupViewStateKey] ?? PopupDefault); } + set { ViewState[PopupViewStateKey] = value; } + } + + /// <summary> + /// Gets or sets a value indicating whether to enforce on high security mode, + /// which requires the full authentication pipeline to be protected by SSL. + /// </summary> + [Bindable(true), DefaultValue(RequireSslDefault), Category(OpenIdCategory)] + [Description("Turns on high security mode, requiring the full authentication pipeline to be protected by SSL.")] + public bool RequireSsl { + get { return (bool)(ViewState[RequireSslViewStateKey] ?? RequireSslDefault); } + set { ViewState[RequireSslViewStateKey] = value; } + } + + /// <summary> + /// Gets or sets the Identifier that will be used to initiate login. + /// </summary> + [Bindable(true), Category(OpenIdCategory)] + [Description("The OpenID Identifier that this button will use to initiate login.")] + [TypeConverter(typeof(IdentifierConverter))] + public virtual Identifier Identifier { + get { + return (Identifier)ViewState[IdentifierViewStateKey]; + } + + set { + ViewState[IdentifierViewStateKey] = value; + this.OnIdentifierChanged(); + } + } + + /// <summary> + /// Gets or sets the default association preference to set on authentication requests. + /// </summary> + internal AssociationPreference AssociationPreference { get; set; } + + /// <summary> + /// Gets ancestor controls, starting with the immediate parent, and progressing to more distant ancestors. + /// </summary> + protected IEnumerable<Control> ParentControls { + get { + Control parent = this; + while ((parent = parent.Parent) != null) { + yield return parent; + } + } + } + + /// <summary> + /// Gets a value indicating whether this control is a child control of a composite OpenID control. + /// </summary> + /// <value> + /// <c>true</c> if this instance is embedded in parent OpenID control; otherwise, <c>false</c>. + /// </value> + protected bool IsEmbeddedInParentOpenIdControl { + get { return this.ParentControls.OfType<OpenIdRelyingPartyControlBase>().Any(); } + } + + /// <summary> + /// Clears any cookie set by this control to help the user on a returning visit next time. + /// </summary> + public static void LogOff() { + HttpContext.Current.Response.SetCookie(CreateIdentifierPersistingCookie(null)); + } + + /// <summary> + /// Immediately redirects to the OpenID Provider to verify the Identifier + /// provided in the text box. + /// </summary> + public void LogOn() { + IAuthenticationRequest request = this.CreateRequests().FirstOrDefault(); + ErrorUtilities.VerifyProtocol(request != null, OpenIdStrings.OpenIdEndpointNotFound); + this.LogOn(request); + } + + /// <summary> + /// Immediately redirects to the OpenID Provider to verify the Identifier + /// provided in the text box. + /// </summary> + /// <param name="request">The request.</param> + public void LogOn(IAuthenticationRequest request) { + Requires.NotNull(request, "request"); + + if (this.IsPopupAppropriate(request)) { + this.ScriptPopupWindow(request); + } else { + request.RedirectToProvider(); + } + } + + #region IPostBackEventHandler Members + + /// <summary> + /// When implemented by a class, enables a server control to process an event raised when a form is posted to the server. + /// </summary> + /// <param name="eventArgument">A <see cref="T:System.String"/> that represents an optional event argument to be passed to the event handler.</param> + void IPostBackEventHandler.RaisePostBackEvent(string eventArgument) { + this.RaisePostBackEvent(eventArgument); + } + + #endregion + + /// <summary> + /// Enables a server control to perform final clean up before it is released from memory. + /// </summary> + [SuppressMessage("Microsoft.Usage", "CA2202:Do not dispose objects multiple times", Justification = "Unavoidable because base class does not expose a protected virtual Dispose(bool) method."), SuppressMessage("Microsoft.Design", "CA1063:ImplementIDisposableCorrectly", Justification = "Base class doesn't implement virtual Dispose(bool), so we must call its Dispose() method.")] + public sealed override void Dispose() { + this.Dispose(true); + base.Dispose(); + GC.SuppressFinalize(this); + } + + /// <summary> + /// Creates the authentication requests for a given user-supplied Identifier. + /// </summary> + /// <param name="identifier">The identifier to create a request for.</param> + /// <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 internal virtual IEnumerable<IAuthenticationRequest> CreateRequests(Identifier identifier) { + Requires.NotNull(identifier, "identifier"); + + // If this control is actually a member of another OpenID RP control, + // delegate creation of requests to the parent control. + var parentOwner = this.ParentControls.OfType<OpenIdRelyingPartyControlBase>().FirstOrDefault(); + if (parentOwner != null) { + return parentOwner.CreateRequests(identifier); + } else { + // Delegate to a private method to keep 'yield return' and Code Contract separate. + return this.CreateRequestsCore(identifier); + } + } + + /// <summary> + /// Releases unmanaged and - optionally - managed resources + /// </summary> + /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param> + protected virtual void Dispose(bool disposing) { + if (disposing) { + if (this.relyingPartyOwned && this.relyingParty != null) { + this.relyingParty.Dispose(); + this.relyingParty = null; + } + } + } + + /// <summary> + /// When implemented by a class, enables a server control to process an event raised when a form is posted to the server. + /// </summary> + /// <param name="eventArgument">A <see cref="T:System.String"/> that represents an optional event argument to be passed to the event handler.</param> + [SuppressMessage("Microsoft.Design", "CA1030:UseEventsWhereAppropriate", Justification = "Predefined signature.")] + protected virtual void RaisePostBackEvent(string eventArgument) { + } + + /// <summary> + /// Creates the authentication requests for the value set in the <see cref="Identifier"/> property. + /// </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 IEnumerable<IAuthenticationRequest> CreateRequests() { + Requires.ValidState(this.Identifier != null, OpenIdStrings.NoIdentifierSet); + return this.CreateRequests(this.Identifier); + } + + /// <summary> + /// Raises the <see cref="E:Load"/> event. + /// </summary> + /// <param name="e">The <see cref="System.EventArgs"/> instance containing the event data.</param> + protected override void OnLoad(EventArgs e) { + base.OnLoad(e); + + if (Page.IsPostBack) { + // OpenID responses NEVER come in the form of a postback. + return; + } + + if (this.Identifier == null) { + this.TryPresetIdentifierWithCookie(); + } + + // Take an unreliable sneek peek to see if we're in a popup and an OpenID + // assertion is coming in. We shouldn't process assertions in a popup window. + if (this.Page.Request.QueryString[UIPopupCallbackKey] == "1" && this.Page.Request.QueryString[UIPopupCallbackParentKey] == null) { + // We're in a popup window. We need to close it and pass the + // message back to the parent window for processing. + this.ScriptClosingPopupOrIFrame(); + return; // don't do any more processing on it now + } + + // Only sniff for an OpenID response if it is targeted at this control. + // Note that Stateless mode causes no receiver to be indicated, and + // we want to handle that, but only if there isn't a parent control that + // will be handling that. + string receiver = this.Page.Request.QueryString[ReturnToReceivingControlId] ?? this.Page.Request.Form[ReturnToReceivingControlId]; + if (receiver == this.ClientID || (receiver == null && !this.IsEmbeddedInParentOpenIdControl)) { + var response = this.RelyingParty.GetResponse(); + Logger.Controls.DebugFormat( + "The {0} control checked for an authentication response and found: {1}", + this.ID, + response != null ? response.Status.ToString() : "nothing"); + this.ProcessResponse(response); + } + } + + /// <summary> + /// Notifies the user agent via an AJAX response of a completed authentication attempt. + /// </summary> + protected virtual void ScriptClosingPopupOrIFrame() { + this.RelyingParty.ProcessResponseFromPopup(); + } + + /// <summary> + /// Called when the <see cref="Identifier"/> property is changed. + /// </summary> + protected virtual void OnIdentifierChanged() { + var identifierChanged = this.IdentifierChanged; + if (identifierChanged != null) { + identifierChanged(this, EventArgs.Empty); + } + } + + /// <summary> + /// Processes the response. + /// </summary> + /// <param name="response">The response.</param> + protected virtual void ProcessResponse(IAuthenticationResponse response) { + if (response == null) { + return; + } + string persistentString = response.GetUntrustedCallbackArgument(UsePersistentCookieCallbackKey); + if (persistentString != null) { + this.UsePersistentCookie = (LogOnPersistence)Enum.Parse(typeof(LogOnPersistence), persistentString); + } + + 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); + } + } + + /// <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); + } + + /// <summary> + /// Fires the <see cref="LoggedIn"/> event. + /// </summary> + /// <param name="response">The response.</param> + protected virtual void OnLoggedIn(IAuthenticationResponse response) { + Requires.NotNull(response, "response"); + Requires.True(response.Status == AuthenticationStatus.Authenticated, "response"); + + var loggedIn = this.LoggedIn; + OpenIdEventArgs args = new OpenIdEventArgs(response); + if (loggedIn != null) { + loggedIn(this, args); + } + + if (!args.Cancel) { + if (this.UsePersistentCookie == LogOnPersistence.SessionAndPersistentIdentifier) { + Page.Response.SetCookie(CreateIdentifierPersistingCookie(response)); + } + + switch (this.LogOnMode) { + case LogOnSiteNotification.FormsAuthentication: + FormsAuthentication.RedirectFromLoginPage(response.ClaimedIdentifier, this.UsePersistentCookie == LogOnPersistence.PersistentAuthentication); + break; + case LogOnSiteNotification.None: + default: + break; + } + } + } + + /// <summary> + /// Fires the <see cref="LoggingIn"/> event. + /// </summary> + /// <param name="request">The request.</param> + /// <returns> + /// Returns whether the login should proceed. False if some event handler canceled the request. + /// </returns> + protected virtual bool OnLoggingIn(IAuthenticationRequest request) { + Requires.NotNull(request, "request"); + + EventHandler<OpenIdEventArgs> loggingIn = this.LoggingIn; + + OpenIdEventArgs args = new OpenIdEventArgs(request); + if (loggingIn != null) { + loggingIn(this, args); + } + + return !args.Cancel; + } + + /// <summary> + /// Fires the <see cref="Canceled"/> event. + /// </summary> + /// <param name="response">The response.</param> + protected virtual void OnCanceled(IAuthenticationResponse response) { + Requires.NotNull(response, "response"); + Requires.True(response.Status == AuthenticationStatus.Canceled, "response"); + + var canceled = this.Canceled; + if (canceled != null) { + canceled(this, new OpenIdEventArgs(response)); + } + } + + /// <summary> + /// Fires the <see cref="Failed"/> event. + /// </summary> + /// <param name="response">The response.</param> + protected virtual void OnFailed(IAuthenticationResponse response) { + Requires.NotNull(response, "response"); + Requires.True(response.Status == AuthenticationStatus.Failed, "response"); + + var failed = this.Failed; + if (failed != null) { + failed(this, new OpenIdEventArgs(response)); + } + } + + /// <summary> + /// Creates the relying party instance used to generate authentication requests. + /// </summary> + /// <returns>The instantiated relying party.</returns> + protected OpenIdRelyingParty CreateRelyingParty() { + IOpenIdApplicationStore store = this.Stateless ? null : OpenIdElement.Configuration.RelyingParty.ApplicationStore.CreateInstance(OpenIdRelyingParty.HttpApplicationStore); + return this.CreateRelyingParty(store); + } + + /// <summary> + /// Creates the relying party instance used to generate authentication requests. + /// </summary> + /// <param name="store">The store to pass to the relying party constructor.</param> + /// <returns>The instantiated relying party.</returns> + protected virtual OpenIdRelyingParty CreateRelyingParty(IOpenIdApplicationStore store) { + return new OpenIdRelyingParty(store); + } + + /// <summary> + /// Configures the relying party. + /// </summary> + /// <param name="relyingParty">The relying party.</param> + [SuppressMessage("Microsoft.Maintainability", "CA1500:VariableNamesShouldNotMatchFieldNames", MessageId = "relyingParty", Justification = "This makes it possible for overrides to see the value before it is set on a field.")] + protected virtual void ConfigureRelyingParty(OpenIdRelyingParty relyingParty) { + Requires.NotNull(relyingParty, "relyingParty"); + + // Only set RequireSsl to true, as we don't want to override + // a .config setting of true with false. + if (this.RequireSsl) { + relyingParty.SecuritySettings.RequireSsl = true; + } + } + + /// <summary> + /// Detects whether a popup window should be used to show the Provider's UI. + /// </summary> + /// <param name="request">The request.</param> + /// <returns> + /// <c>true</c> if a popup should be used; <c>false</c> otherwise. + /// </returns> + protected virtual bool IsPopupAppropriate(IAuthenticationRequest request) { + Requires.NotNull(request, "request"); + + switch (this.Popup) { + case PopupBehavior.Never: + return false; + case PopupBehavior.Always: + return true; + case PopupBehavior.IfProviderSupported: + return request.DiscoveryResult.IsExtensionSupported<UIRequest>(); + default: + throw ErrorUtilities.ThrowInternal("Unexpected value for Popup property."); + } + } + + /// <summary> + /// Adds attributes to an HTML <A> tag that will be written by the caller using + /// <see cref="HtmlTextWriter.RenderBeginTag(HtmlTextWriterTag)"/> after this method. + /// </summary> + /// <param name="writer">The HTML writer.</param> + /// <param name="request">The outgoing authentication request.</param> + /// <param name="windowStatus">The text to try to display in the status bar on mouse hover.</param> + protected void RenderOpenIdMessageTransmissionAsAnchorAttributes(HtmlTextWriter writer, IAuthenticationRequest request, string windowStatus) { + Requires.NotNull(writer, "writer"); + Requires.NotNull(request, "request"); + + // We render a standard HREF attribute for non-javascript browsers. + writer.AddAttribute(HtmlTextWriterAttribute.Href, request.RedirectingResponse.GetDirectUriRequest(this.RelyingParty.Channel).AbsoluteUri); + + // And for the Javascript ones we do the extra work to use form POST where necessary. + writer.AddAttribute(HtmlTextWriterAttribute.Onclick, this.CreateGetOrPostAHrefValue(request) + " return false;"); + + writer.AddStyleAttribute(HtmlTextWriterStyle.Cursor, "pointer"); + if (!string.IsNullOrEmpty(windowStatus)) { + writer.AddAttribute("onMouseOver", "window.status = " + MessagingUtilities.GetSafeJavascriptValue(windowStatus)); + writer.AddAttribute("onMouseOut", "window.status = null"); + } + } + + /// <summary> + /// Creates the identifier-persisting cookie, either for saving or deleting. + /// </summary> + /// <param name="response">The positive authentication response; or <c>null</c> to clear the cookie.</param> + /// <returns>An persistent cookie.</returns> + private static HttpCookie CreateIdentifierPersistingCookie(IAuthenticationResponse response) { + HttpCookie cookie = new HttpCookie(PersistentIdentifierCookieName); + bool clearingCookie = false; + + // We'll try to store whatever it was the user originally typed in, but fallback + // to the final claimed_id. + if (response != null && response.Status == AuthenticationStatus.Authenticated) { + var positiveResponse = (PositiveAuthenticationResponse)response; + + // We must escape the value because XRIs start with =, and any leading '=' gets dropped (by ASP.NET?) + cookie.Value = Uri.EscapeDataString(positiveResponse.Endpoint.UserSuppliedIdentifier ?? response.ClaimedIdentifier); + } else { + clearingCookie = true; + cookie.Value = string.Empty; + if (HttpContext.Current.Request.Browser["supportsEmptyStringInCookieValue"] == "false") { + cookie.Value = "NoCookie"; + } + } + + if (clearingCookie) { + // mark the cookie has having already expired to cause the user agent to delete + // the old persisted cookie. + cookie.Expires = DateTime.Now.Subtract(TimeSpan.FromDays(1)); + } else { + // Make the cookie persistent by setting an expiration date + cookie.Expires = DateTime.Now + PersistentIdentifierTimeToLiveDefault; + } + + return cookie; + } + + /// <summary> + /// Creates the authentication requests for a given user-supplied Identifier. + /// </summary> + /// <param name="identifier">The identifier to create a request for.</param> + /// <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> CreateRequestsCore(Identifier identifier) { + ErrorUtilities.VerifyArgumentNotNull(identifier, "identifier"); // NO CODE CONTRACTS! (yield return used here) + IEnumerable<IAuthenticationRequest> requests; + + // 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; + if (this.ReturnToUrl != null) { + string returnToResolvedPath = this.ResolveUrl(this.ReturnToUrl); + returnToApproximation = new Uri(this.RelyingParty.Channel.GetRequestFromContext().UrlBeforeRewriting, returnToResolvedPath); + } else { + returnToApproximation = 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 = 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); + if (string.IsNullOrEmpty(this.ReturnToUrl)) { + requests = this.RelyingParty.CreateRequests(identifier, typedRealm); + } else { + // Since the user actually gave us a return_to value, + // the "approximation" is exactly what we want. + requests = this.RelyingParty.CreateRequests(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 + // and don't try it multiple times. + requests = requests.Distinct(DuplicateRequestedHostsComparer.Instance); + + // Configure each generated request. + foreach (var req in requests) { + if (this.IsPopupAppropriate(req)) { + // Inform ourselves in return_to that we're in a popup. + req.SetUntrustedCallbackArgument(UIPopupCallbackKey, "1"); + + if (req.DiscoveryResult.IsExtensionSupported<UIRequest>()) { + // Inform the OP that we'll be using a popup window consistent with the UI extension. + // But beware that the extension MAY have already been added if we're using + // the OpenIdAjaxRelyingParty class. + if (!((AuthenticationRequest)req).Extensions.OfType<UIRequest>().Any()) { + 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.SetUntrustedCallbackArgument(PopupUISupportedJSHint, "1"); + } + } + + // Add the extensions injected into the control. + foreach (var extension in this.Extensions) { + req.AddExtension(extension); + } + + // Add state that needs to survive across the redirect, but at this point + // only save those properties that are not expected to be changed by a + // LoggingIn event handler. + req.SetUntrustedCallbackArgument(ReturnToReceivingControlId, this.ClientID); + + // Apply the control's association preference to this auth request, but only if + // it is less demanding (greater ordinal value) than the existing one. + // That way, we protect against retrying an association that was already attempted. + var authReq = (AuthenticationRequest)req; + if (authReq.AssociationPreference < this.AssociationPreference) { + authReq.AssociationPreference = this.AssociationPreference; + } + + if (this.OnLoggingIn(req)) { + // We save this property after firing OnLoggingIn so that the host page can + // change its value and have that value saved. + req.SetUntrustedCallbackArgument(UsePersistentCookieCallbackKey, this.UsePersistentCookie.ToString()); + + yield return req; + } + } + } + + /// <summary> + /// Gets the javascript to executee to redirect or POST an OpenID message to a remote party. + /// </summary> + /// <param name="request">The authentication request to send.</param> + /// <returns>The javascript that should execute.</returns> + private string CreateGetOrPostAHrefValue(IAuthenticationRequest request) { + Requires.NotNull(request, "request"); + + Uri directUri = request.RedirectingResponse.GetDirectUriRequest(this.RelyingParty.Channel); + return "window.dnoa_internal.GetOrPost(" + MessagingUtilities.GetSafeJavascriptValue(directUri.AbsoluteUri) + ");"; + } + + /// <summary> + /// Wires the return page to immediately display a popup window with the Provider in it. + /// </summary> + /// <param name="request">The request.</param> + private void ScriptPopupWindow(IAuthenticationRequest request) { + Requires.NotNull(request, "request"); + Requires.ValidState(this.RelyingParty != null); + + StringBuilder startupScript = new StringBuilder(); + + // Add a callback function that the popup window can call on this, the + // parent window, to pass back the authentication result. + startupScript.AppendLine("window.dnoa_internal = {};"); + startupScript.AppendLine("window.dnoa_internal.processAuthorizationResult = function(uri) { window.location = uri; };"); + startupScript.AppendLine("window.dnoa_internal.popupWindow = function() {"); + startupScript.AppendFormat( + @"\tvar openidPopup = {0}", + OpenId.RelyingParty.Extensions.UI.UIUtilities.GetWindowPopupScript(this.RelyingParty, request, "openidPopup")); + startupScript.AppendLine("};"); + + this.Page.ClientScript.RegisterClientScriptBlock(this.GetType(), "loginPopup", startupScript.ToString(), true); + } + + /// <summary> + /// Tries to preset the <see cref="Identifier"/> property based on a persistent + /// cookie on the browser. + /// </summary> + /// <returns> + /// A value indicating whether the <see cref="Identifier"/> property was + /// successfully preset to some non-empty value. + /// </returns> + private bool TryPresetIdentifierWithCookie() { + HttpCookie cookie = this.Page.Request.Cookies[PersistentIdentifierCookieName]; + if (cookie != null) { + this.Identifier = Uri.UnescapeDataString(cookie.Value); + return true; + } + + return false; + } + } +} |