//----------------------------------------------------------------------- // // Copyright (c) Andrew Arnott. All rights reserved. // //----------------------------------------------------------------------- [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; /// /// Methods of indicating to the rest of the web site that the user has logged in. /// [SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly", MessageId = "OnSite", Justification = "Two words intended.")] public enum LogOnSiteNotification { /// /// The rest of the web site is unaware that the user just completed an OpenID login. /// None, /// /// After the event is fired /// the control automatically calls /// with the as the username /// unless the event handler sets /// property to true. /// FormsAuthentication, } /// /// How an OpenID user session should be persisted across visits. /// public enum LogOnPersistence { /// /// 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. /// Session, /// /// 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. /// SessionAndPersistentIdentifier, /// /// The user is issued a persistent authentication ticket so that no login is /// necessary on their return visit. /// PersistentAuthentication, } /// /// A common base class for OpenID Relying Party controls. /// [DefaultProperty("Identifier"), ValidationProperty("Identifier")] [ParseChildren(true), PersistChildren(false)] public abstract class OpenIdRelyingPartyControlBase : Control, IPostBackEventHandler, IDisposable { /// /// The manifest resource name of the javascript file to include on the hosting page. /// internal const string EmbeddedJavascriptResource = Util.DefaultNamespace + ".OpenId.RelyingParty.OpenIdRelyingPartyControlBase.js"; /// /// The cookie used to persist the Identifier the user logged in with. /// internal const string PersistentIdentifierCookieName = OpenIdUtilities.CustomParameterPrefix + "OpenIDIdentifier"; /// /// The callback parameter name to use to store which control initiated the auth request. /// internal const string ReturnToReceivingControlId = OpenIdUtilities.CustomParameterPrefix + "receiver"; #region Protected internal callback parameter names /// /// The callback parameter to use for recognizing when the callback is in a popup window or hidden iframe. /// protected internal const string UIPopupCallbackKey = OpenIdUtilities.CustomParameterPrefix + "uipopup"; /// /// The parameter name to include in the formulated auth request so that javascript can know whether /// the OP advertises support for the UI extension. /// protected internal const string PopupUISupportedJSHint = OpenIdUtilities.CustomParameterPrefix + "popupUISupported"; #endregion #region Property category constants /// /// The "Appearance" category for properties. /// protected const string AppearanceCategory = "Appearance"; /// /// The "Behavior" category for properties. /// protected const string BehaviorCategory = "Behavior"; /// /// The "OpenID" category for properties and events. /// protected const string OpenIdCategory = "OpenID"; #endregion #region Private callback parameter names /// /// The callback parameter for use with persisting the property. /// private const string UsePersistentCookieCallbackKey = OpenIdUtilities.CustomParameterPrefix + "UsePersistentCookie"; /// /// The callback parameter to use for recognizing when the callback is in the parent window. /// private const string UIPopupCallbackParentKey = OpenIdUtilities.CustomParameterPrefix + "uipopupParent"; #endregion #region Property default values /// /// The default value for the property. /// private const bool StatelessDefault = false; /// /// The default value for the property. /// private const string ReturnToUrlDefault = ""; /// /// Default value of . /// private const LogOnPersistence UsePersistentCookieDefault = LogOnPersistence.Session; /// /// Default value of . /// private const LogOnSiteNotification LogOnModeDefault = LogOnSiteNotification.FormsAuthentication; /// /// The default value for the property. /// private const string RealmUrlDefault = "~/"; /// /// The default value for the property. /// private const PopupBehavior PopupDefault = PopupBehavior.Never; /// /// The default value for the property. /// private const bool RequireSslDefault = false; #endregion #region Property view state keys /// /// The viewstate key to use for storing the value of the property. /// private const string ExtensionsViewStateKey = "Extensions"; /// /// The viewstate key to use for the property. /// private const string StatelessViewStateKey = "Stateless"; /// /// The viewstate key to use for the property. /// private const string UsePersistentCookieViewStateKey = "UsePersistentCookie"; /// /// The viewstate key to use for the property. /// private const string LogOnModeViewStateKey = "LogOnMode"; /// /// The viewstate key to use for the property. /// private const string RealmUrlViewStateKey = "RealmUrl"; /// /// The viewstate key to use for the property. /// private const string ReturnToUrlViewStateKey = "ReturnToUrl"; /// /// The key under which the value for the property will be stored. /// private const string IdentifierViewStateKey = "Identifier"; /// /// The viewstate key to use for the property. /// private const string PopupViewStateKey = "Popup"; /// /// The viewstate key to use for the property. /// private const string RequireSslViewStateKey = "RequireSsl"; #endregion /// /// The lifetime of the cookie used to persist the Identifier the user logged in with. /// private static readonly TimeSpan PersistentIdentifierTimeToLiveDefault = TimeSpan.FromDays(14); /// /// Backing field for the property. /// private OpenIdRelyingParty relyingParty; /// /// A value indicating whether the field contains /// an instance that we own and should Dispose. /// private bool relyingPartyOwned; /// /// Initializes a new instance of the class. /// protected OpenIdRelyingPartyControlBase() { Reporting.RecordFeatureUse(this); } #region Events /// /// Fired when the user has typed in their identifier, discovery was successful /// and a login attempt is about to begin. /// [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 LoggingIn; /// /// Fired upon completion of a successful login. /// [Description("Fired upon completion of a successful login."), Category(OpenIdCategory)] public event EventHandler LoggedIn; /// /// Fired when a login attempt fails. /// [Description("Fired when a login attempt fails."), Category(OpenIdCategory)] public event EventHandler Failed; /// /// Fired when an authentication attempt is canceled at the OpenID Provider. /// [Description("Fired when an authentication attempt is canceled at the OpenID Provider."), Category(OpenIdCategory)] public event EventHandler Canceled; /// /// Occurs when the property is changed. /// protected event EventHandler IdentifierChanged; #endregion /// /// 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. /// [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; } } /// /// Gets the collection of extension requests this selector should include in generated requests. /// [PersistenceMode(PersistenceMode.InnerProperty)] public Collection Extensions { get { if (this.ViewState[ExtensionsViewStateKey] == null) { var extensions = new Collection(); this.ViewState[ExtensionsViewStateKey] = extensions; return extensions; } else { return (Collection)this.ViewState[ExtensionsViewStateKey]; } } } /// /// Gets or sets a value indicating whether stateless mode is used. /// [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; } } /// /// Gets or sets the OpenID of the relying party web site. /// [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 { Contract.Requires(!string.IsNullOrEmpty(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; } } /// /// Gets or sets the OpenID ReturnTo of the relying party web site. /// [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; } } /// /// 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. /// [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; } } /// /// 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 virtual LogOnSiteNotification LogOnMode { get { return (LogOnSiteNotification)(this.ViewState[LogOnModeViewStateKey] ?? LogOnModeDefault); } set { this.ViewState[LogOnModeViewStateKey] = value; } } /// /// Gets or sets a value indicating when to use a popup window to complete the login experience. /// /// The default value is . [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; } } /// /// Gets or sets a value indicating whether to enforce on high security mode, /// which requires the full authentication pipeline to be protected by SSL. /// [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; } } /// /// Gets or sets the Identifier that will be used to initiate login. /// [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(); } } /// /// Gets or sets the default association preference to set on authentication requests. /// internal AssociationPreference AssociationPreference { get; set; } /// /// Gets ancestor controls, starting with the immediate parent, and progressing to more distant ancestors. /// protected IEnumerable ParentControls { get { Control parent = this; while ((parent = parent.Parent) != null) { yield return parent; } } } /// /// Gets a value indicating whether this control is a child control of a composite OpenID control. /// /// /// true if this instance is embedded in parent OpenID control; otherwise, false. /// protected bool IsEmbeddedInParentOpenIdControl { get { return this.ParentControls.OfType().Any(); } } /// /// Clears any cookie set by this control to help the user on a returning visit next time. /// public static void LogOff() { HttpContext.Current.Response.SetCookie(CreateIdentifierPersistingCookie(null)); } /// /// Immediately redirects to the OpenID Provider to verify the Identifier /// provided in the text box. /// public void LogOn() { IAuthenticationRequest request = this.CreateRequests().FirstOrDefault(); ErrorUtilities.VerifyProtocol(request != null, OpenIdStrings.OpenIdEndpointNotFound); this.LogOn(request); } /// /// Immediately redirects to the OpenID Provider to verify the Identifier /// provided in the text box. /// /// The request. public void LogOn(IAuthenticationRequest request) { Contract.Requires(request != null); if (this.IsPopupAppropriate(request)) { this.ScriptPopupWindow(request); } else { request.RedirectToProvider(); } } #region IPostBackEventHandler Members /// /// When implemented by a class, enables a server control to process an event raised when a form is posted to the server. /// /// A that represents an optional event argument to be passed to the event handler. void IPostBackEventHandler.RaisePostBackEvent(string eventArgument) { this.RaisePostBackEvent(eventArgument); } #endregion /// /// Enables a server control to perform final clean up before it is released from memory. /// [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); } /// /// Creates the authentication requests for a given user-supplied Identifier. /// /// The identifier to create a request for. /// /// A sequence of authentication requests, any one of which may be /// used to determine the user's control of the . /// protected internal virtual IEnumerable CreateRequests(Identifier identifier) { Contract.Requires(identifier != null); // 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().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); } } /// /// Releases unmanaged and - optionally - managed resources /// /// true to release both managed and unmanaged resources; false to release only unmanaged resources. protected virtual void Dispose(bool disposing) { if (disposing) { if (this.relyingPartyOwned && this.relyingParty != null) { this.relyingParty.Dispose(); this.relyingParty = null; } } } /// /// When implemented by a class, enables a server control to process an event raised when a form is posted to the server. /// /// A that represents an optional event argument to be passed to the event handler. [SuppressMessage("Microsoft.Design", "CA1030:UseEventsWhereAppropriate", Justification = "Predefined signature.")] protected virtual void RaisePostBackEvent(string eventArgument) { } /// /// Creates the authentication requests for the value set in the property. /// /// /// A sequence of authentication requests, any one of which may be /// used to determine the user's control of the . /// protected IEnumerable CreateRequests() { Contract.Requires(this.Identifier != null, OpenIdStrings.NoIdentifierSet); return this.CreateRequests(this.Identifier); } /// /// Raises the event. /// /// The instance containing the event data. 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); } } /// /// Notifies the user agent via an AJAX response of a completed authentication attempt. /// protected virtual void ScriptClosingPopupOrIFrame() { this.RelyingParty.ProcessResponseFromPopup(); } /// /// Called when the property is changed. /// protected virtual void OnIdentifierChanged() { var identifierChanged = this.IdentifierChanged; if (identifierChanged != null) { identifierChanged(this, EventArgs.Empty); } } /// /// Processes the response. /// /// The response. 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); } } /// /// Raises the event. /// /// An object that contains the event data. protected override void OnPreRender(EventArgs e) { base.OnPreRender(e); this.Page.ClientScript.RegisterClientScriptResource(typeof(OpenIdRelyingPartyControlBase), EmbeddedJavascriptResource); } /// /// Fires the event. /// /// The response. protected virtual void OnLoggedIn(IAuthenticationResponse response) { Contract.Requires(response != null); Contract.Requires(response.Status == AuthenticationStatus.Authenticated); 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; } } } /// /// Fires the event. /// /// The request. /// /// Returns whether the login should proceed. False if some event handler canceled the request. /// protected virtual bool OnLoggingIn(IAuthenticationRequest request) { Contract.Requires(request != null); EventHandler loggingIn = this.LoggingIn; OpenIdEventArgs args = new OpenIdEventArgs(request); if (loggingIn != null) { loggingIn(this, args); } return !args.Cancel; } /// /// Fires the event. /// /// The response. protected virtual void OnCanceled(IAuthenticationResponse response) { Contract.Requires(response != null); Contract.Requires(response.Status == AuthenticationStatus.Canceled); var canceled = this.Canceled; if (canceled != null) { canceled(this, new OpenIdEventArgs(response)); } } /// /// Fires the event. /// /// The response. protected virtual void OnFailed(IAuthenticationResponse response) { Contract.Requires(response != null); Contract.Requires(response.Status == AuthenticationStatus.Failed); var failed = this.Failed; if (failed != null) { failed(this, new OpenIdEventArgs(response)); } } /// /// Creates the relying party instance used to generate authentication requests. /// /// The instantiated relying party. protected OpenIdRelyingParty CreateRelyingParty() { IOpenIdApplicationStore store = this.Stateless ? null : OpenIdElement.Configuration.RelyingParty.ApplicationStore.CreateInstance(OpenIdRelyingParty.HttpApplicationStore); return this.CreateRelyingParty(store); } /// /// Creates the relying party instance used to generate authentication requests. /// /// The store to pass to the relying party constructor. /// The instantiated relying party. protected virtual OpenIdRelyingParty CreateRelyingParty(IOpenIdApplicationStore store) { return new OpenIdRelyingParty(store); } /// /// Configures the relying party. /// /// The relying party. [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) { Contract.Requires(relyingParty != null); // 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; } } /// /// Detects whether a popup window should be used to show the Provider's UI. /// /// The request. /// /// true if a popup should be used; false otherwise. /// protected virtual bool IsPopupAppropriate(IAuthenticationRequest request) { Contract.Requires(request != null); switch (this.Popup) { case PopupBehavior.Never: return false; case PopupBehavior.Always: return true; case PopupBehavior.IfProviderSupported: return request.DiscoveryResult.IsExtensionSupported(); default: throw ErrorUtilities.ThrowInternal("Unexpected value for Popup property."); } } /// /// Adds attributes to an HTML <A> tag that will be written by the caller using /// after this method. /// /// The HTML writer. /// The outgoing authentication request. /// The text to try to display in the status bar on mouse hover. protected void RenderOpenIdMessageTransmissionAsAnchorAttributes(HtmlTextWriter writer, IAuthenticationRequest request, string windowStatus) { Contract.Requires(writer != null); Contract.Requires(request != null); // 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"); } } /// /// Creates the identifier-persisting cookie, either for saving or deleting. /// /// The positive authentication response; or null to clear the cookie. /// An persistent cookie. 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; } /// /// Creates the authentication requests for a given user-supplied Identifier. /// /// The identifier to create a request for. /// /// A sequence of authentication requests, any one of which may be /// used to determine the user's control of the . /// private IEnumerable CreateRequestsCore(Identifier identifier) { ErrorUtilities.VerifyArgumentNotNull(identifier, "identifier"); // NO CODE CONTRACTS! (yield return used here) IEnumerable 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()) { // 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().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; } } } /// /// Gets the javascript to executee to redirect or POST an OpenID message to a remote party. /// /// The authentication request to send. /// The javascript that should execute. private string CreateGetOrPostAHrefValue(IAuthenticationRequest request) { Contract.Requires(request != null); Uri directUri = request.RedirectingResponse.GetDirectUriRequest(this.RelyingParty.Channel); return "window.dnoa_internal.GetOrPost(" + MessagingUtilities.GetSafeJavascriptValue(directUri.AbsoluteUri) + ");"; } /// /// Wires the return page to immediately display a popup window with the Provider in it. /// /// The request. private void ScriptPopupWindow(IAuthenticationRequest request) { Contract.Requires(request != null); Contract.Requires(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}", UIUtilities.GetWindowPopupScript(this.RelyingParty, request, "openidPopup")); startupScript.AppendLine("};"); this.Page.ClientScript.RegisterClientScriptBlock(this.GetType(), "loginPopup", startupScript.ToString(), true); } /// /// Tries to preset the property based on a persistent /// cookie on the browser. /// /// /// A value indicating whether the property was /// successfully preset to some non-empty value. /// private bool TryPresetIdentifierWithCookie() { HttpCookie cookie = this.Page.Request.Cookies[PersistentIdentifierCookieName]; if (cookie != null) { this.Identifier = Uri.UnescapeDataString(cookie.Value); return true; } return false; } } }