diff options
-rw-r--r-- | src/DotNetOpenAuth.Test/Mocks/MockIdentifier.cs | 2 | ||||
-rw-r--r-- | src/DotNetOpenAuth/DotNetOpenAuth.csproj | 8 | ||||
-rw-r--r-- | src/DotNetOpenAuth/OpenId/Identifier.cs | 9 | ||||
-rw-r--r-- | src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdAjaxTextBox.cs | 885 | ||||
-rw-r--r-- | src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdAjaxTextBox.js | 542 | ||||
-rw-r--r-- | src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdMobileTextBox.cs | 2 | ||||
-rw-r--r-- | src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingPartyAjaxControlBase.cs | 300 | ||||
-rw-r--r-- | src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingPartyAjaxControlBase.js | 385 | ||||
-rw-r--r-- | src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingPartyControlBase.cs | 178 | ||||
-rw-r--r-- | src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingPartyControlBase.js | 5 | ||||
-rw-r--r-- | src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdTextBox.cs | 2 | ||||
-rw-r--r-- | src/DotNetOpenAuth/OpenId/RelyingParty/PositiveAuthenticationResponseSnapshot.cs (renamed from src/DotNetOpenAuth/OpenId/RelyingParty/AuthenticationResponseSnapshot.cs) | 8 |
12 files changed, 924 insertions, 1402 deletions
diff --git a/src/DotNetOpenAuth.Test/Mocks/MockIdentifier.cs b/src/DotNetOpenAuth.Test/Mocks/MockIdentifier.cs index 669e4d3..bb52f65 100644 --- a/src/DotNetOpenAuth.Test/Mocks/MockIdentifier.cs +++ b/src/DotNetOpenAuth.Test/Mocks/MockIdentifier.cs @@ -24,7 +24,7 @@ namespace DotNetOpenAuth.Test.Mocks { private Identifier wrappedIdentifier; public MockIdentifier(Identifier wrappedIdentifier, MockHttpRequest mockHttpRequest, IEnumerable<ServiceEndpoint> endpoints) - : base(false) { + : base(wrappedIdentifier.OriginalString, false) { ErrorUtilities.VerifyArgumentNotNull(wrappedIdentifier, "wrappedIdentifier"); ErrorUtilities.VerifyArgumentNotNull(mockHttpRequest, "mockHttpRequest"); ErrorUtilities.VerifyArgumentNotNull(endpoints, "endpoints"); diff --git a/src/DotNetOpenAuth/DotNetOpenAuth.csproj b/src/DotNetOpenAuth/DotNetOpenAuth.csproj index 8f7715e..6f161be 100644 --- a/src/DotNetOpenAuth/DotNetOpenAuth.csproj +++ b/src/DotNetOpenAuth/DotNetOpenAuth.csproj @@ -147,9 +147,15 @@ <Reference Include="System.IdentityModel.Selectors"> <RequiredTargetFramework>3.0</RequiredTargetFramework> </Reference> + <Reference Include="System.Runtime.Serialization"> + <RequiredTargetFramework>3.0</RequiredTargetFramework> + </Reference> <Reference Include="System.ServiceModel"> <RequiredTargetFramework>3.0</RequiredTargetFramework> </Reference> + <Reference Include="System.ServiceModel.Web"> + <RequiredTargetFramework>3.5</RequiredTargetFramework> + </Reference> <Reference Include="System.Web" /> <Reference Include="System.Web.Abstractions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL"> <SpecificVersion>False</SpecificVersion> @@ -492,7 +498,7 @@ <Compile Include="OpenId\ProviderEndpointDescription.cs" /> <Compile Include="OpenId\Provider\ProviderSecuritySettings.cs" /> <Compile Include="OpenId\RelyingParty\IRelyingPartyApplicationStore.cs" /> - <Compile Include="OpenId\RelyingParty\AuthenticationResponseSnapshot.cs" /> + <Compile Include="OpenId\RelyingParty\PositiveAuthenticationResponseSnapshot.cs" /> <Compile Include="OpenId\RelyingParty\PrivateSecretManager.cs" /> <Compile Include="OpenId\RelyingParty\RelyingPartySecuritySettings.cs" /> <Compile Include="OpenId\RelyingParty\ServiceEndpoint.cs" /> diff --git a/src/DotNetOpenAuth/OpenId/Identifier.cs b/src/DotNetOpenAuth/OpenId/Identifier.cs index 6a03660..435dc00 100644 --- a/src/DotNetOpenAuth/OpenId/Identifier.cs +++ b/src/DotNetOpenAuth/OpenId/Identifier.cs @@ -24,17 +24,16 @@ namespace DotNetOpenAuth.OpenId { /// <summary> /// Initializes a new instance of the <see cref="Identifier"/> class. /// </summary> - /// <param name="isDiscoverySecureEndToEnd"> - /// Whether the derived class is prepared to guarantee end-to-end discovery - /// and initial redirect for authentication is performed using SSL. - /// </param> + /// <param name="originalString">The original string before any normalization.</param> + /// <param name="isDiscoverySecureEndToEnd">Whether the derived class is prepared to guarantee end-to-end discovery + /// and initial redirect for authentication is performed using SSL.</param> protected Identifier(string originalString, bool isDiscoverySecureEndToEnd) { this.OriginalString = originalString; this.IsDiscoverySecureEndToEnd = isDiscoverySecureEndToEnd; } /// <summary> - /// Gets or sets the original string that was normalized to create this Identifier. + /// Gets the original string that was normalized to create this Identifier. /// </summary> public string OriginalString { get; private set; } diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdAjaxTextBox.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdAjaxTextBox.cs index 2da537c..b3fe500 100644 --- a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdAjaxTextBox.cs +++ b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdAjaxTextBox.cs @@ -18,19 +18,11 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { using System.Collections.Specialized; using System.ComponentModel; using System.Diagnostics; - using System.Diagnostics.CodeAnalysis; - using System.Drawing.Design; + using System.Diagnostics.Contracts; using System.Globalization; - using System.Linq; using System.Text; - using System.Text.RegularExpressions; - using System.Web; using System.Web.UI; - using System.Web.UI.WebControls; using DotNetOpenAuth.Messaging; - using DotNetOpenAuth.OpenId.ChannelElements; - using DotNetOpenAuth.OpenId.Extensions; - using DotNetOpenAuth.OpenId.Extensions.UI; /// <summary> /// An ASP.NET control that provides a minimal text box that is OpenID-aware and uses AJAX for @@ -38,7 +30,7 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { /// </summary> [DefaultProperty("Text"), ValidationProperty("Text")] [ToolboxData("<{0}:OpenIdAjaxTextBox runat=\"server\" />")] - public sealed class OpenIdAjaxTextBox : WebControl, ICallbackEventHandler, ITextControl { + public class OpenIdAjaxTextBox : OpenIdRelyingPartyAjaxControlBase, ICallbackEventHandler, IEditableTextControl, ITextControl, IPostBackDataHandler { /// <summary> /// The name of the manifest stream containing the OpenIdAjaxTextBox.js file. /// </summary> @@ -72,19 +64,14 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { private const string ColumnsViewStateKey = "Columns"; /// <summary> - /// The viewstate key to use for storing the value of the <see cref="OnClientAssertionReceived"/> property. - /// </summary> - private const string OnClientAssertionReceivedViewStateKey = "OnClientAssertionReceived"; - - /// <summary> - /// The viewstate key to use for storing the value of the <see cref="AuthenticationResponse"/> property. + /// The viewstate key to use for the <see cref="CssClass"/> property. /// </summary> - private const string AuthenticationResponseViewStateKey = "AuthenticationResponse"; + private const string CssClassViewStateKey = "CssClass"; /// <summary> - /// The viewstate key to use for storing the value of the a successful authentication. + /// The viewstate key to use for storing the value of the <see cref="OnClientAssertionReceived"/> property. /// </summary> - private const string AuthDataViewStateKey = "AuthData"; + private const string OnClientAssertionReceivedViewStateKey = "OnClientAssertionReceived"; /// <summary> /// The viewstate key to use for storing the value of the <see cref="AuthenticatedAsToolTip"/> property. @@ -97,16 +84,6 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { private const string AuthenticationSucceededToolTipViewStateKey = "AuthenticationSucceededToolTip"; /// <summary> - /// The viewstate key to use for storing the value of the <see cref="ReturnToUrl"/> property. - /// </summary> - private const string ReturnToUrlViewStateKey = "ReturnToUrl"; - - /// <summary> - /// The viewstate key to use for storing the value of the <see cref="RealmUrl"/> property. - /// </summary> - private const string RealmUrlViewStateKey = "RealmUrl"; - - /// <summary> /// The viewstate key to use for storing the value of the <see cref="LogOnInProgressMessage"/> property. /// </summary> private const string LogOnInProgressMessageViewStateKey = "BusyToolTip"; @@ -152,14 +129,14 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { private const string TimeoutViewStateKey = "Timeout"; /// <summary> - /// The viewstate key to use for storing the value of the <see cref="Text"/> property. + /// The viewstate key to use for storing the value of the <see cref="TabIndex"/> property. /// </summary> - private const string TextViewStateKey = "Text"; + private const string TabIndexViewStateKey = "TabIndex"; /// <summary> - /// The viewstate key to use for storing the value of the <see cref="TabIndex"/> property. + /// The viewstate key to use for the <see cref="Enabled"/> property. /// </summary> - private const string TabIndexViewStateKey = "TabIndex"; + private const string EnabledViewStateKey = "Enabled"; /// <summary> /// The viewstate key to use for storing the value of the <see cref="RetryToolTip"/> property. @@ -181,14 +158,9 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { private const int ColumnsDefault = 40; /// <summary> - /// The default value for the <see cref="ReturnToUrl"/> property. - /// </summary> - private const string ReturnToUrlDefault = ""; - - /// <summary> - /// The default value for the <see cref="RealmUrl"/> property. + /// The default value for the <see cref="CssClass"/> property. /// </summary> - private const string RealmUrlDefault = "~/"; + private const string CssClassDefault = "openid"; /// <summary> /// The default value for the <see cref="LogOnInProgressMessage"/> property. @@ -258,26 +230,6 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { #endregion /// <summary> - /// Backing field for the <see cref="RelyingParty"/> property. - /// </summary> - private OpenIdRelyingParty relyingParty; - - /// <summary> - /// Backing field for the <see cref="RelyingPartyNonVerifying"/> property. - /// </summary> - private OpenIdRelyingParty relyingPartyNonVerifying; - - /// <summary> - /// Tracks whether the text box should receive input focus when the page is rendered. - /// </summary> - private bool focusCalled; - - /// <summary> - /// The authentication response that just came in. - /// </summary> - private IAuthenticationResponse authenticationResponse; - - /// <summary> /// A dictionary of extension response types and the javascript member /// name to map them to on the user agent. /// </summary> @@ -292,30 +244,10 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { #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.")] - public event EventHandler<OpenIdEventArgs> LoggingIn; - - /// <summary> - /// Fired when a Provider sends back a positive assertion to this control, - /// but the authentication has not yet been verified. - /// </summary> - /// <remarks> - /// <b>No security critical decisions should be made within event handlers - /// for this event</b> as the authenticity of the assertion has not been - /// verified yet. All security related code should go in the event handler - /// for the <see cref="LoggedIn"/> event. - /// </remarks> - [Description("Fired when a Provider sends back a positive assertion to this control, but the authentication has not yet been verified.")] - public event EventHandler<OpenIdEventArgs> UnconfirmedPositiveAssertion; - - /// <summary> - /// Fired when authentication has completed successfully. + /// Fired when the content of the text changes between posts to the server. /// </summary> - [Description("Fired when authentication has completed successfully.")] - public event EventHandler<OpenIdEventArgs> LoggedIn; + [Description("Occurs when the content of the text changes between posts to the server."), Category(BehaviorCategory)] + public event EventHandler TextChanged; /// <summary> /// Gets or sets the client-side script that executes when an authentication @@ -331,10 +263,10 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { /// the authentication has not been verified and may have been spoofed. /// No security-sensitive operations should take place in this javascript code. /// The authentication is verified on the server by the time the - /// <see cref="LoggedIn"/> server-side event fires.</para> + /// <see cref="OpenIdRelyingPartyControlBase.LoggedIn"/> server-side event fires.</para> /// </remarks> [Description("Gets or sets the client-side script that executes when an authentication assertion is received (but before it is verified).")] - [Bindable(true), DefaultValue(""), Category("Behavior")] + [Bindable(true), DefaultValue(""), Category(BehaviorCategory)] public string OnClientAssertionReceived { get { return this.ViewState[OnClientAssertionReceivedViewStateKey] as string; } set { this.ViewState[OnClientAssertionReceivedViewStateKey] = value; } @@ -345,54 +277,19 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { #region Properties /// <summary> - /// Gets the completed authentication response. - /// </summary> - 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); - - // Save out the authentication response to viewstate so we can find it on - // a subsequent postback. - this.ViewState[AuthenticationResponseViewStateKey] = new AuthenticationResponseSnapshot(this.authenticationResponse); - } else { - this.authenticationResponse = viewstateResponse; - } - } - return this.authenticationResponse; - } - } - - /// <summary> /// Gets or sets the value in the text field, completely unprocessed or normalized. /// </summary> - [Bindable(true), DefaultValue(""), Category("Appearance")] - [Description("The value in the text field, completely unprocessed or normalized.")] + [Bindable(true), DefaultValue(""), Category(AppearanceCategory)] + [Description("The content of the text box.")] public string Text { - get { return (string)(this.ViewState[TextViewStateKey] ?? string.Empty); } - set { this.ViewState[TextViewStateKey] = value ?? string.Empty; } + get { return this.Identifier != null ? this.Identifier.OriginalString : string.Empty; } + set { this.Identifier = value; } } /// <summary> /// Gets or sets the width of the text box in characters. /// </summary> - [Bindable(true), Category("Appearance"), DefaultValue(ColumnsDefault)] + [Bindable(true), Category(AppearanceCategory), DefaultValue(ColumnsDefault)] [Description("The width of the text box in characters.")] public int Columns { get { @@ -400,22 +297,45 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { } set { + Contract.Requires<ArgumentOutOfRangeException>(value >= 0); ErrorUtilities.VerifyArgumentInRange(value >= 0, "value"); this.ViewState[ColumnsViewStateKey] = value; } } /// <summary> + /// Gets or sets the CSS class assigned to the text box. + /// </summary> + [Bindable(true), DefaultValue(CssClassDefault), Category(AppearanceCategory)] + [Description("The CSS class assigned to the text box.")] + public string CssClass { + get { return (string)this.ViewState[CssClassViewStateKey]; } + set { this.ViewState[CssClassViewStateKey] = value; } + } + + /// <summary> /// Gets or sets the tab index of the text box control. Use 0 to omit an explicit tabindex. /// </summary> - [Bindable(true), Category("Behavior"), DefaultValue(TabIndexDefault)] + [Bindable(true), Category(BehaviorCategory), DefaultValue(TabIndexDefault)] [Description("The tab index of the text box control. Use 0 to omit an explicit tabindex.")] - public override short TabIndex { + public virtual short TabIndex { get { return (short)(this.ViewState[TabIndexViewStateKey] ?? TabIndexDefault); } set { this.ViewState[TabIndexViewStateKey] = value; } } /// <summary> + /// Gets or sets a value indicating whether this <see cref="OpenIdTextBox"/> is enabled + /// in the browser for editing and will respond to incoming OpenID messages. + /// </summary> + /// <value><c>true</c> if enabled; otherwise, <c>false</c>.</value> + [Bindable(true), DefaultValue(true), Category(BehaviorCategory)] + [Description("Whether the control is editable in the browser and will respond to OpenID messages.")] + public bool Enabled { + get { return (bool)(this.ViewState[EnabledViewStateKey] ?? true); } + set { this.ViewState[EnabledViewStateKey] = value; } + } + + /// <summary> /// Gets or sets the HTML name to assign to the text field. /// </summary> [Bindable(true), DefaultValue(NameDefault), Category("Misc")] @@ -434,7 +354,7 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { /// <summary> /// Gets or sets the time duration for the AJAX control to wait for an OP to respond before reporting failure to the user. /// </summary> - [Browsable(true), DefaultValue(typeof(TimeSpan), "00:00:01"), Category("Behavior")] + [Browsable(true), DefaultValue(typeof(TimeSpan), "00:00:08"), Category(BehaviorCategory)] [Description("The time duration for the AJAX control to wait for an OP to respond before reporting failure to the user.")] public TimeSpan Timeout { get { @@ -450,7 +370,7 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { /// <summary> /// Gets or sets the maximum number of OpenID Providers to simultaneously try to authenticate with. /// </summary> - [Browsable(true), DefaultValue(ThrottleDefault), Category("Behavior")] + [Browsable(true), DefaultValue(ThrottleDefault), Category(BehaviorCategory)] [Description("The maximum number of OpenID Providers to simultaneously try to authenticate with.")] public int Throttle { get { @@ -466,7 +386,7 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { /// <summary> /// Gets or sets the text that appears on the LOG IN button in cases where immediate (invisible) authentication fails. /// </summary> - [Bindable(true), DefaultValue(LogOnTextDefault), Localizable(true), Category("Appearance")] + [Bindable(true), DefaultValue(LogOnTextDefault), Localizable(true), Category(AppearanceCategory)] [Description("The text that appears on the LOG IN button in cases where immediate (invisible) authentication fails.")] public string LogOnText { get { @@ -482,7 +402,7 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { /// <summary> /// Gets or sets the rool tip text that appears on the LOG IN button in cases where immediate (invisible) authentication fails. /// </summary> - [Bindable(true), DefaultValue(LogOnToolTipDefault), Localizable(true), Category("Appearance")] + [Bindable(true), DefaultValue(LogOnToolTipDefault), Localizable(true), Category(AppearanceCategory)] [Description("The tool tip text that appears on the LOG IN button in cases where immediate (invisible) authentication fails.")] public string LogOnToolTip { get { return (string)(this.ViewState[LogOnToolTipViewStateKey] ?? LogOnToolTipDefault); } @@ -492,7 +412,7 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { /// <summary> /// Gets or sets the text that appears on the RETRY button in cases where authentication times out. /// </summary> - [Bindable(true), DefaultValue(RetryTextDefault), Localizable(true), Category("Appearance")] + [Bindable(true), DefaultValue(RetryTextDefault), Localizable(true), Category(AppearanceCategory)] [Description("The text that appears on the RETRY button in cases where authentication times out.")] public string RetryText { get { @@ -508,7 +428,7 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { /// <summary> /// Gets or sets the tool tip text that appears on the RETRY button in cases where authentication times out. /// </summary> - [Bindable(true), DefaultValue(RetryToolTipDefault), Localizable(true), Category("Appearance")] + [Bindable(true), DefaultValue(RetryToolTipDefault), Localizable(true), Category(AppearanceCategory)] [Description("The tool tip text that appears on the RETRY button in cases where authentication times out.")] public string RetryToolTip { get { return (string)(this.ViewState[RetryToolTipViewStateKey] ?? RetryToolTipDefault); } @@ -518,7 +438,7 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { /// <summary> /// Gets or sets the tool tip text that appears when authentication succeeds. /// </summary> - [Bindable(true), DefaultValue(AuthenticationSucceededToolTipDefault), Localizable(true), Category("Appearance")] + [Bindable(true), DefaultValue(AuthenticationSucceededToolTipDefault), Localizable(true), Category(AppearanceCategory)] [Description("The tool tip text that appears when authentication succeeds.")] public string AuthenticationSucceededToolTip { get { return (string)(this.ViewState[AuthenticationSucceededToolTipViewStateKey] ?? AuthenticationSucceededToolTipDefault); } @@ -528,7 +448,7 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { /// <summary> /// Gets or sets the tool tip text that appears on the green checkmark when authentication succeeds. /// </summary> - [Bindable(true), DefaultValue(AuthenticatedAsToolTipDefault), Localizable(true), Category("Appearance")] + [Bindable(true), DefaultValue(AuthenticatedAsToolTipDefault), Localizable(true), Category(AppearanceCategory)] [Description("The tool tip text that appears on the green checkmark when authentication succeeds.")] public string AuthenticatedAsToolTip { get { return (string)(this.ViewState[AuthenticatedAsToolTipViewStateKey] ?? AuthenticatedAsToolTipDefault); } @@ -538,7 +458,7 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { /// <summary> /// Gets or sets the tool tip text that appears when authentication fails. /// </summary> - [Bindable(true), DefaultValue(AuthenticationFailedToolTipDefault), Localizable(true), Category("Appearance")] + [Bindable(true), DefaultValue(AuthenticationFailedToolTipDefault), Localizable(true), Category(AppearanceCategory)] [Description("The tool tip text that appears when authentication fails.")] public string AuthenticationFailedToolTip { get { return (string)(this.ViewState[AuthenticationFailedToolTipViewStateKey] ?? AuthenticationFailedToolTipDefault); } @@ -548,7 +468,7 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { /// <summary> /// Gets or sets the tool tip text that appears over the text box when it is discovering and authenticating. /// </summary> - [Bindable(true), DefaultValue(BusyToolTipDefault), Localizable(true), Category("Appearance")] + [Bindable(true), DefaultValue(BusyToolTipDefault), Localizable(true), Category(AppearanceCategory)] [Description("The tool tip text that appears over the text box when it is discovering and authenticating.")] public string BusyToolTip { get { return (string)(this.ViewState[BusyToolTipViewStateKey] ?? BusyToolTipDefault); } @@ -558,7 +478,7 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { /// <summary> /// Gets or sets the message that is displayed if a postback is about to occur before the identifier has been supplied. /// </summary> - [Bindable(true), DefaultValue(IdentifierRequiredMessageDefault), Localizable(true), Category("Appearance")] + [Bindable(true), DefaultValue(IdentifierRequiredMessageDefault), Localizable(true), Category(AppearanceCategory)] [Description("The message that is displayed if a postback is about to occur before the identifier has been supplied.")] public string IdentifierRequiredMessage { get { return (string)(this.ViewState[IdentifierRequiredMessageViewStateKey] ?? IdentifierRequiredMessageDefault); } @@ -568,230 +488,25 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { /// <summary> /// Gets or sets the message that is displayed if a postback is attempted while login is in process. /// </summary> - [Bindable(true), DefaultValue(LogOnInProgressMessageDefault), Localizable(true), Category("Appearance")] + [Bindable(true), DefaultValue(LogOnInProgressMessageDefault), Localizable(true), Category(AppearanceCategory)] [Description("The message that is displayed if a postback is attempted while login is in process.")] public string LogOnInProgressMessage { get { return (string)(this.ViewState[LogOnInProgressMessageViewStateKey] ?? LogOnInProgressMessageDefault); } set { this.ViewState[LogOnInProgressMessageViewStateKey] = value ?? string.Empty; } } - /// <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.Usage", "CA2234:PassSystemUriObjectsInsteadOfStrings", Justification = "Property grid on form designer only supports primitive types.")] - [SuppressMessage("Microsoft.Design", "CA1056:UriPropertiesShouldNotBeStrings", Justification = "Property grid on form designer only supports primitive types.")] - [Bindable(true)] - [Category("Behavior")] - [DefaultValue(RealmUrlDefault)] - [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)(this.ViewState[RealmUrlViewStateKey] ?? RealmUrlDefault); - } - - set { - 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("*.", "")); // 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[RealmUrlViewStateKey] = value; - } - } - - /// <summary> - /// Gets or sets the OpenID ReturnTo of the relying party web site. - /// </summary> - [SuppressMessage("Microsoft.Usage", "CA1806:DoNotIgnoreMethodResults", MessageId = "System.Uri", Justification = "Using Uri.ctor for validation.")] - [SuppressMessage("Microsoft.Usage", "CA2234:PassSystemUriObjectsInsteadOfStrings", Justification = "Property grid on form designer only supports primitive types.")] - [SuppressMessage("Microsoft.Design", "CA1056:UriPropertiesShouldNotBeStrings", Justification = "Property grid on form designer only supports primitive types.")] - [Bindable(true)] - [Category("Behavior")] - [DefaultValue(ReturnToUrlDefault)] - [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 (Page != null && !DesignMode) { - // Validate new value by trying to construct a Uri based on it. - new Uri(this.RelyingParty.Channel.GetRequestFromContext().UrlBeforeRewriting, 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; - } - } - #endregion - #region Properties to hide - /// <summary> - /// Gets or sets the foreground color (typically the color of the text) of the Web server control. - /// </summary> - /// <returns> - /// A <see cref="T:System.Drawing.Color"/> that represents the foreground color of the control. The default is <see cref="F:System.Drawing.Color.Empty"/>. - /// </returns> - [Obsolete("This property does not do anything."), Browsable(false), Bindable(false)] - public override System.Drawing.Color ForeColor { - get { throw new NotSupportedException(); } - set { throw new NotSupportedException(); } - } - - /// <summary> - /// Gets or sets the background color of the Web server control. - /// </summary> - /// <returns> - /// A <see cref="T:System.Drawing.Color"/> that represents the background color of the control. The default is <see cref="F:System.Drawing.Color.Empty"/>, which indicates that this property is not set. - /// </returns> - [Obsolete("This property does not do anything."), Browsable(false), Bindable(false)] - public override System.Drawing.Color BackColor { - get { throw new NotSupportedException(); } - set { throw new NotSupportedException(); } - } - - /// <summary> - /// Gets or sets the border color of the Web control. - /// </summary> - /// <returns> - /// A <see cref="T:System.Drawing.Color"/> that represents the border color of the control. The default is <see cref="F:System.Drawing.Color.Empty"/>, which indicates that this property is not set. - /// </returns> - [Obsolete("This property does not do anything."), Browsable(false), Bindable(false)] - public override System.Drawing.Color BorderColor { - get { throw new NotSupportedException(); } - set { throw new NotSupportedException(); } - } - - /// <summary> - /// Gets or sets the border width of the Web server control. - /// </summary> - /// <returns> - /// A <see cref="T:System.Web.UI.WebControls.Unit"/> that represents the border width of a Web server control. The default value is <see cref="F:System.Web.UI.WebControls.Unit.Empty"/>, which indicates that this property is not set. - /// </returns> - /// <exception cref="T:System.ArgumentException"> - /// The specified border width is a negative value. - /// </exception> - [Obsolete("This property does not do anything."), Browsable(false), Bindable(false)] - public override Unit BorderWidth { - get { return Unit.Empty; } - set { throw new NotSupportedException(); } - } - - /// <summary> - /// Gets or sets the border style of the Web server control. - /// </summary> - /// <returns> - /// One of the <see cref="T:System.Web.UI.WebControls.BorderStyle"/> enumeration values. The default is NotSet. - /// </returns> - [Obsolete("This property does not do anything."), Browsable(false), Bindable(false)] - public override BorderStyle BorderStyle { - get { return BorderStyle.None; } - set { throw new NotSupportedException(); } - } - - /// <summary> - /// Gets the font properties associated with the Web server control. - /// </summary> - /// <returns> - /// A <see cref="T:System.Web.UI.WebControls.FontInfo"/> that represents the font properties of the Web server control. - /// </returns> - [Obsolete("This property does not do anything."), Browsable(false), Bindable(false)] - public override FontInfo Font { - get { return null; } - } - - /// <summary> - /// Gets or sets the height of the Web server control. - /// </summary> - /// <returns> - /// A <see cref="T:System.Web.UI.WebControls.Unit"/> that represents the height of the control. The default is <see cref="F:System.Web.UI.WebControls.Unit.Empty"/>. - /// </returns> - /// <exception cref="T:System.ArgumentException"> - /// The height was set to a negative value. - /// </exception> - [Obsolete("This property does not do anything."), Browsable(false), Bindable(false)] - public override Unit Height { - get { return Unit.Empty; } - set { throw new NotSupportedException(); } - } - - /// <summary> - /// Gets or sets the width of the Web server control. - /// </summary> - /// <returns> - /// A <see cref="T:System.Web.UI.WebControls.Unit"/> that represents the width of the control. The default is <see cref="F:System.Web.UI.WebControls.Unit.Empty"/>. - /// </returns> - /// <exception cref="T:System.ArgumentException"> - /// The width of the Web server control was set to a negative value. - /// </exception> - [Obsolete("This property does not do anything."), Browsable(false), Bindable(false)] - public override Unit Width { - get { return Unit.Empty; } - set { throw new NotSupportedException(); } - } - - /// <summary> - /// Gets or sets the text displayed when the mouse pointer hovers over the Web server control. - /// </summary> - /// <returns> - /// The text displayed when the mouse pointer hovers over the Web server control. The default is <see cref="F:System.String.Empty"/>. - /// </returns> - [Obsolete("This property does not do anything."), Browsable(false), Bindable(false)] - public override string ToolTip { - get { return string.Empty; } - set { throw new NotSupportedException(); } - } - - /// <summary> - /// Gets or sets the skin to apply to the control. - /// </summary> - /// <returns> - /// The name of the skin to apply to the control. The default is <see cref="F:System.String.Empty"/>. - /// </returns> - /// <exception cref="T:System.ArgumentException"> - /// The skin specified in the <see cref="P:System.Web.UI.WebControls.WebControl.SkinID"/> property does not exist in the theme. - /// </exception> - [Obsolete("This property does not do anything."), Browsable(false), Bindable(false)] - public override string SkinID { - get { return string.Empty; } - set { throw new NotSupportedException(); } - } - - /// <summary> - /// Gets or sets a value indicating whether themes apply to this control. + /// Gets the name of the open id auth data form key. /// </summary> - /// <returns>true to use themes; otherwise, false. The default is false. - /// </returns> - [Obsolete("This property does not do anything."), Browsable(false), Bindable(false)] - public override bool EnableTheming { - get { return false; } - set { throw new NotSupportedException(); } + /// <value> + /// A concatenation of <see cref="Name"/> and <c>"_openidAuthData"</c>. + /// </value> + protected override string OpenIdAuthDataFormKey { + get { return this.Name + "_openidAuthData"; } } - #endregion - /// <summary> /// Gets the default value for the <see cref="Timeout"/> property. /// </summary> @@ -807,171 +522,37 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { } } - /// <summary> - /// Gets the relying party to use when verification of incoming messages is needed. - /// </summary> - private OpenIdRelyingParty RelyingParty { - get { - if (this.relyingParty == null) { - this.relyingParty = CreateRelyingParty(true); - } - return this.relyingParty; - } - } + #region IPostBackDataHandler Members /// <summary> - /// Gets the relying party to use when verification of incoming messages is NOT wanted. + /// When implemented by a class, processes postback data for an ASP.NET server control. /// </summary> - private OpenIdRelyingParty RelyingPartyNonVerifying { - get { - if (this.relyingPartyNonVerifying == null) { - this.relyingPartyNonVerifying = CreateRelyingParty(false); - } - return this.relyingPartyNonVerifying; - } - } - - /// <summary> - /// Gets the name of the open id auth data form key. - /// </summary> - /// <value>A concatenation of <see cref="Name"/> and <c>"_openidAuthData"</c>.</value> - private string OpenIdAuthDataFormKey { - get { return this.Name + "_openidAuthData"; } - } - - /// <summary> - /// Places focus on the text box when the page is rendered on the browser. - /// </summary> - public override void Focus() { - // we don't emit the code to focus the control immediately, in case the control - // is never rendered to the page because its Visible property is false or that - // of any of its parent containers. - this.focusCalled = true; - } - - /// <summary> - /// 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. - /// </summary> - /// <typeparam name="T">The extension <i>response</i> type that will read data from the assertion.</typeparam> - /// <param name="propertyName">The property name on the openid_identifier input box object that will be used to store the extension data. For example: sreg</param> - /// <remarks> - /// This method should be called from the <see cref="UnconfirmedPositiveAssertion"/> event handler. - /// </remarks> - [SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter", Justification = "By design")] - public void RegisterClientScriptExtension<T>(string propertyName) where T : IClientScriptExtensionResponse { - ErrorUtilities.VerifyNonZeroLength(propertyName, "propertyName"); - ErrorUtilities.VerifyArgumentNamed(!this.clientScriptExtensions.ContainsValue(propertyName), "propertyName", OpenIdStrings.ClientScriptExtensionPropertyNameCollision, propertyName); - foreach (var ext in this.clientScriptExtensions.Keys) { - ErrorUtilities.VerifyArgument(ext != typeof(T), OpenIdStrings.ClientScriptExtensionTypeCollision, typeof(T).FullName); - } - this.clientScriptExtensions.Add(typeof(T), propertyName); + /// <param name="postDataKey">The key identifier for the control.</param> + /// <param name="postCollection">The collection of all incoming name values.</param> + /// <returns> + /// true if the server control's state changes as a result of the postback; otherwise, false. + /// </returns> + bool IPostBackDataHandler.LoadPostData(string postDataKey, NameValueCollection postCollection) { + return this.LoadPostData(postDataKey, postCollection); } - #region ICallbackEventHandler Members - /// <summary> - /// Returns the result of discovery on some Identifier passed to <see cref="ICallbackEventHandler.RaiseCallbackEvent"/>. + /// When implemented by a class, signals the server control to notify the ASP.NET application that the state of the control has changed. /// </summary> - /// <returns>The result of the callback.</returns> - /// <value>A whitespace delimited list of URLs that can be used to initiate authentication.</value> - string ICallbackEventHandler.GetCallbackResult() { - this.Page.Response.ContentType = "text/javascript"; - return this.discoveryResult; - } - - /// <summary> - /// Performs discovery on some OpenID Identifier. Called directly from the user agent via - /// AJAX callback mechanisms. - /// </summary> - /// <param name="eventArgument">The identifier to perform discovery on.</param> - void ICallbackEventHandler.RaiseCallbackEvent(string eventArgument) { - string userSuppliedIdentifier = eventArgument; - - ErrorUtilities.VerifyNonZeroLength(userSuppliedIdentifier, "userSuppliedIdentifier"); - Logger.OpenId.InfoFormat("AJAX discovery on {0} requested.", userSuppliedIdentifier); - - // We prepare a JSON object with this interface: - // class jsonResponse { - // string claimedIdentifier; - // Array requests; // never null - // string error; // null if no error - // } - // Each element in the requests array looks like this: - // class jsonAuthRequest { - // string endpoint; // URL to the OP endpoint - // string immediate; // URL to initiate an immediate request - // string setup; // URL to initiate a setup request. - // } - StringBuilder discoveryResultBuilder = new StringBuilder(); - discoveryResultBuilder.Append("{"); - try { - List<IAuthenticationRequest> requests = this.CreateRequests(userSuppliedIdentifier, true).Where(req => this.OnLoggingIn(req)).ToList(); - if (requests.Count > 0) { - discoveryResultBuilder.AppendFormat("claimedIdentifier: {0},", MessagingUtilities.GetSafeJavascriptValue(requests[0].ClaimedIdentifier)); - discoveryResultBuilder.Append("requests: ["); - foreach (IAuthenticationRequest request in requests) { - discoveryResultBuilder.Append("{"); - discoveryResultBuilder.AppendFormat("endpoint: {0},", MessagingUtilities.GetSafeJavascriptValue(request.Provider.Uri.AbsoluteUri)); - request.Mode = AuthenticationRequestMode.Immediate; - OutgoingWebResponse response = request.RedirectingResponse; - discoveryResultBuilder.AppendFormat("immediate: {0},", MessagingUtilities.GetSafeJavascriptValue(response.GetDirectUriRequest(this.RelyingParty.Channel).AbsoluteUri)); - request.Mode = AuthenticationRequestMode.Setup; - response = request.RedirectingResponse; - discoveryResultBuilder.AppendFormat("setup: {0}", MessagingUtilities.GetSafeJavascriptValue(response.GetDirectUriRequest(this.RelyingParty.Channel).AbsoluteUri)); - discoveryResultBuilder.Append("},"); - } - discoveryResultBuilder.Length -= 1; // trim off last comma - discoveryResultBuilder.Append("]"); - } else { - discoveryResultBuilder.Append("requests: new Array(),"); - discoveryResultBuilder.AppendFormat("error: {0}", MessagingUtilities.GetSafeJavascriptValue(OpenIdStrings.OpenIdEndpointNotFound)); - } - } catch (ProtocolException ex) { - discoveryResultBuilder.Append("requests: new Array(),"); - discoveryResultBuilder.AppendFormat("error: {0}", MessagingUtilities.GetSafeJavascriptValue(ex.Message)); - } - discoveryResultBuilder.Append("}"); - this.discoveryResult = discoveryResultBuilder.ToString(); + void IPostBackDataHandler.RaisePostDataChangedEvent() { + this.RaisePostDataChangedEvent(); } #endregion /// <summary> - /// Enables a server control to perform final clean up before it is released from memory. + /// Raises the <see cref="E:Load"/> event. /// </summary> - public sealed override void Dispose() { - this.Dispose(true); - base.Dispose(); - GC.SuppressFinalize(this); - } - - /// <summary> - /// Prepares the control for loading. - /// </summary> - /// <param name="e">The <see cref="T:System.EventArgs"/> object that contains the event data.</param> + /// <param name="e">The <see cref="System.EventArgs"/> instance containing the event data.</param> protected override void OnLoad(EventArgs e) { base.OnLoad(e); - if (this.Page.IsPostBack) { - // If the control was temporarily hidden, it won't be in the Form data, - // and we'll just implicitly keep the last Text setting. - if (this.Page.Request.Form[this.Name] != null) { - this.Text = this.Page.Request.Form[this.Name]; - } - - // If there is a response, and it is fresh (live object, not a snapshot object)... - if (this.AuthenticationResponse != null && this.AuthenticationResponse.Status == AuthenticationStatus.Authenticated) { - this.OnLoggedIn(this.AuthenticationResponse); - } - } else { - NameValueCollection query = this.RelyingParty.Channel.GetRequestFromContext().GetQueryOrFormFromContext(); - string userSuppliedIdentifier = query["dotnetopenid.userSuppliedIdentifier"]; - if (!string.IsNullOrEmpty(userSuppliedIdentifier) && query["dotnetopenid.phase"] == "2") { - this.ReportAuthenticationResult(); - } - } + this.Page.RegisterRequiresPostBack(this); } /// <summary> @@ -988,183 +569,85 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { /// Renders the control. /// </summary> /// <param name="writer">The <see cref="T:System.Web.UI.HtmlTextWriter"/> object that receives the control content.</param> - protected override void Render(System.Web.UI.HtmlTextWriter writer) { + protected override void Render(HtmlTextWriter writer) { + base.Render(writer); + // We surround the textbox with a span so that the .js file can inject a // login button within the text box with easy placement. - writer.WriteBeginTag("span"); - writer.WriteAttribute("class", this.CssClass); - writer.Write(" style='"); - writer.WriteStyleAttribute("display", "inline-block"); - writer.WriteStyleAttribute("position", "relative"); - writer.WriteStyleAttribute("font-size", "16px"); - writer.Write("'>"); - - writer.WriteBeginTag("input"); - writer.WriteAttribute("name", this.Name); - writer.WriteAttribute("id", this.ClientID); - writer.WriteAttribute("value", this.Text, true); - writer.WriteAttribute("size", this.Columns.ToString(CultureInfo.InvariantCulture)); + if (!string.IsNullOrEmpty(this.CssClass)) { + writer.AddAttribute(HtmlTextWriterAttribute.Class, this.CssClass); + } + + writer.AddStyleAttribute(HtmlTextWriterStyle.Display, "inline-block"); + writer.AddStyleAttribute(HtmlTextWriterStyle.Position, "relative"); + writer.AddStyleAttribute(HtmlTextWriterStyle.FontSize, "16px"); + writer.RenderBeginTag(HtmlTextWriterTag.Span); + + writer.AddAttribute(HtmlTextWriterAttribute.Name, this.Name); + writer.AddAttribute(HtmlTextWriterAttribute.Id, this.ClientID); + writer.AddAttribute(HtmlTextWriterAttribute.Size, this.Columns.ToString(CultureInfo.InvariantCulture)); + if (!string.IsNullOrEmpty(this.Text)) { + writer.AddAttribute(HtmlTextWriterAttribute.Value, this.Text, true); + } + if (this.TabIndex > 0) { - writer.WriteAttribute("tabindex", this.TabIndex.ToString(CultureInfo.InvariantCulture)); + writer.AddAttribute(HtmlTextWriterAttribute.Tabindex, this.TabIndex.ToString(CultureInfo.InvariantCulture)); } if (!this.Enabled) { - writer.WriteAttribute("disabled", "true"); + writer.AddAttribute(HtmlTextWriterAttribute.Disabled, "true"); } if (!string.IsNullOrEmpty(this.CssClass)) { - writer.WriteAttribute("class", this.CssClass); - } - writer.Write(" style='"); - writer.WriteStyleAttribute("padding-left", "18px"); - writer.WriteStyleAttribute("border-style", "solid"); - writer.WriteStyleAttribute("border-width", "1px"); - writer.WriteStyleAttribute("border-color", "lightgray"); - writer.Write("'"); - writer.Write(" />"); - - writer.WriteEndTag("span"); - - // 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.WriteBeginTag("input"); - writer.WriteAttribute("type", "hidden"); - writer.WriteAttribute("name", this.OpenIdAuthDataFormKey); - writer.WriteAttribute("value", viewstateAuthData, true); - writer.Write(" />"); + writer.AddAttribute(HtmlTextWriterAttribute.Class, this.CssClass); } + writer.AddStyleAttribute(HtmlTextWriterStyle.PaddingLeft, "18px"); + writer.AddStyleAttribute(HtmlTextWriterStyle.BorderStyle, "solid"); + writer.AddStyleAttribute(HtmlTextWriterStyle.BorderWidth, "1px"); + writer.AddStyleAttribute(HtmlTextWriterStyle.BorderColor, "lightgray"); + writer.RenderBeginTag(HtmlTextWriterTag.Input); + writer.RenderEndTag(); // </input> + writer.RenderEndTag(); // </span> } /// <summary> - /// Filters a sequence of OP endpoints so that an OP hostname only appears once in the list. + /// When implemented by a class, processes postback data for an ASP.NET server control. /// </summary> - /// <param name="requests">The authentication requests against those OP endpoints.</param> - /// <returns>The filtered list.</returns> - private static List<IAuthenticationRequest> RemoveDuplicateEndpoints(List<IAuthenticationRequest> requests) { - var filteredRequests = new List<IAuthenticationRequest>(requests.Count); - foreach (IAuthenticationRequest request in requests) { - // We'll distinguish based on the host name only, which - // admittedly is only a heuristic, but if we remove one that really wasn't a duplicate, well, - // this multiple OP attempt thing was just a convenience feature anyway. - if (!filteredRequests.Any(req => string.Equals(req.Provider.Uri.Host, request.Provider.Uri.Host, StringComparison.OrdinalIgnoreCase))) { - filteredRequests.Add(request); + /// <param name="postDataKey">The key identifier for the control.</param> + /// <param name="postCollection">The collection of all incoming name values.</param> + /// <returns> + /// true if the server control's state changes as a result of the postback; otherwise, false. + /// </returns> + protected virtual bool LoadPostData(string postDataKey, NameValueCollection postCollection) { + // If the control was temporarily hidden, it won't be in the Form data, + // and we'll just implicitly keep the last Text setting. + if (postCollection[this.Name] != null) { + Identifier identifier = postCollection[this.Name].Length == 0 ? null : postCollection[this.Name]; + if (identifier != this.Identifier) { + this.Identifier = identifier; + return true; } } - return filteredRequests; + return false; } /// <summary> - /// Creates the relying party. + /// When implemented by a class, signals the server control to notify the ASP.NET application that the state of the control has changed. /// </summary> - /// <param name="verifySignature"> - /// A value indicating whether message protections should be applied to the processed messages. - /// Use <c>false</c> to postpone verification to a later time without invalidating nonces. - /// </param> - /// <returns>The newly instantiated relying party.</returns> - private static OpenIdRelyingParty CreateRelyingParty(bool verifySignature) { - return verifySignature ? new OpenIdRelyingParty() : OpenIdRelyingParty.CreateNonVerifying(); + protected virtual void RaisePostDataChangedEvent() { + this.OnTextChanged(); } /// <summary> - /// Releases unmanaged and - optionally - managed resources + /// Called on a postback when the Text property has changed. /// </summary> - /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param> - private void Dispose(bool disposing) { - if (disposing) { - if (this.relyingParty != null) { - this.relyingParty.Dispose(); - this.relyingParty = null; - } - - if (this.relyingPartyNonVerifying != null) { - this.relyingPartyNonVerifying.Dispose(); - this.relyingPartyNonVerifying = null; - } + protected virtual void OnTextChanged() { + EventHandler textChanged = this.TextChanged; + if (textChanged != null) { + textChanged(this, EventArgs.Empty); } } /// <summary> - /// Fires the <see cref="LoggingIn"/> event. - /// </summary> - /// <param name="request">The request.</param> - /// <returns><c>true</c> if the login should proceed; <c>false</c> otherwise.</returns> - private bool OnLoggingIn(IAuthenticationRequest request) { - var loggingIn = this.LoggingIn; - if (loggingIn != null) { - var args = new OpenIdEventArgs(request); - loggingIn(this, args); - return !args.Cancel; - } - - return true; - } - - /// <summary> - /// Fires the <see cref="UnconfirmedPositiveAssertion"/> event. - /// </summary> - private void OnUnconfirmedPositiveAssertion() { - var unconfirmedPositiveAssertion = this.UnconfirmedPositiveAssertion; - if (unconfirmedPositiveAssertion != null) { - unconfirmedPositiveAssertion(this, null); - } - } - - /// <summary> - /// Fires the <see cref="LoggedIn"/> event. - /// </summary> - /// <param name="response">The response.</param> - private void OnLoggedIn(IAuthenticationResponse response) { - var loggedIn = this.LoggedIn; - if (loggedIn != null) { - loggedIn(this, new OpenIdEventArgs(response)); - } - } - - /// <summary> - /// Invokes a method on a parent frame/window's OpenIdAjaxTextBox, - /// and closes the calling popup window if applicable. - /// </summary> - /// <param name="methodCall">The method to call on the OpenIdAjaxTextBox, including - /// parameters. (i.e. "callback('arg1', 2)"). No escaping is done by this method.</param> - private void CallbackUserAgentMethod(string methodCall) { - this.CallbackUserAgentMethod(methodCall, null); - } - - /// <summary> - /// Invokes a method on a parent frame/window's OpenIdAjaxTextBox, - /// and closes the calling popup window if applicable. - /// </summary> - /// <param name="methodCall">The method to call on the OpenIdAjaxTextBox, including - /// parameters. (i.e. "callback('arg1', 2)"). No escaping is done by this method.</param> - /// <param name="preAssignments">An optional list of assignments to make to the input box object before placing the method call.</param> - private void CallbackUserAgentMethod(string methodCall, string[] preAssignments) { - Logger.OpenId.InfoFormat("Sending Javascript callback: {0}", methodCall); - Page.Response.Write(@"<html><body><script language='javascript'> - var inPopup = !window.frameElement; - var objSrc = inPopup ? window.opener.waiting_openidBox : window.frameElement.openidBox; -"); - if (preAssignments != null) { - foreach (string assignment in preAssignments) { - Page.Response.Write(string.Format(CultureInfo.InvariantCulture, " objSrc.{0};\n", assignment)); - } - } - - // Something about calling objSrc.{0} can somehow cause FireFox to forget about the inPopup variable, - // so we have to actually put the test for it ABOVE the call to objSrc.{0} so that it already - // whether to call window.self.close() after the call. - string htmlFormat = @" if (inPopup) {{ - objSrc.{0}; - window.self.close(); -}} else {{ - objSrc.{0}; -}} -</script></body></html>"; - Page.Response.Write(string.Format(CultureInfo.InvariantCulture, htmlFormat, methodCall)); - Page.Response.End(); - } - - /// <summary> /// Assembles the javascript to send to the client and registers it with ASP.NET for transmission. /// </summary> private void PrepareClientJavascript() { @@ -1186,9 +669,6 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { StringBuilder startupScript = new StringBuilder(); startupScript.AppendLine("<script language='javascript'>"); startupScript.AppendFormat("var box = document.getElementsByName('{0}')[0];{1}", this.Name, Environment.NewLine); - if (this.focusCalled) { - startupScript.AppendLine("box.focus();"); - } startupScript.AppendFormat( CultureInfo.InvariantCulture, "initAjaxOpenId(box, {0}, {1}, {2}, {3}, {4}, {5}, {6}, {7}, {8}, {9}, {10}, {11}, {12}, {13}, {14}, {15}, {16}, {17}, function({18}, {19}, {20}) {{{21}}});{22}", @@ -1228,106 +708,5 @@ if (!openidbox.dnoi_internal.onSubmit()) {{ return false; }} "loginvalidation", string.Format(CultureInfo.InvariantCulture, htmlFormat, this.Name)); } - - /// <summary> - /// Creates the authentication requests for a given user-supplied Identifier. - /// </summary> - /// <param name="userSuppliedIdentifier">The user supplied identifier.</param> - /// <param name="immediate">A value indicating whether the authentication - /// requests should be initialized for use in invisible iframes for background authentication.</param> - /// <returns>The list 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> CreateRequests(string userSuppliedIdentifier, bool immediate) { - var requests = new List<IAuthenticationRequest>(); - - // 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 = this.ReturnToUrl != null ? new Uri(this.RelyingParty.Channel.GetRequestFromContext().UrlBeforeRewriting, this.ReturnToUrl) : 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.AddRange(this.RelyingParty.CreateRequests(userSuppliedIdentifier, typedRealm)); - } else { - // Since the user actually gave us a return_to value, - // the "approximation" is exactly what we want. - requests.AddRange(this.RelyingParty.CreateRequests(userSuppliedIdentifier, 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 = RemoveDuplicateEndpoints(requests); - - // Configure each generated request. - int reqIndex = 0; - foreach (var req in requests) { - req.AddCallbackArguments("index", (reqIndex++).ToString(CultureInfo.InvariantCulture)); - - if (req.Provider.IsExtensionSupported<UIRequest>()) { - // Inform the OP that we'll be using a popup window. - 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.AddCallbackArguments("dotnetopenid.popupUISupported", "1"); - } - - // If the ReturnToUrl was explicitly set, we'll need to reset our first parameter - if (string.IsNullOrEmpty(HttpUtility.ParseQueryString(req.ReturnToUrl.Query)["dotnetopenid.userSuppliedIdentifier"])) { - req.AddCallbackArguments("dotnetopenid.userSuppliedIdentifier", userSuppliedIdentifier); - } - - // Our javascript needs to let the user know which endpoint responded. So we force it here. - // This gives us the info even for 1.0 OPs and 2.0 setup_required responses. - req.AddCallbackArguments("dotnetopenid.op_endpoint", req.Provider.Uri.AbsoluteUri); - req.AddCallbackArguments("dotnetopenid.claimed_id", (string)req.ClaimedIdentifier ?? string.Empty); - req.AddCallbackArguments("dotnetopenid.phase", "2"); - if (immediate) { - req.Mode = AuthenticationRequestMode.Immediate; - ((AuthenticationRequest)req).AssociationPreference = AssociationPreference.IfAlreadyEstablished; - } - } - - return requests; - } - - /// <summary> - /// Notifies the user agent via an AJAX response of a completed authentication attempt. - /// </summary> - private void ReportAuthenticationResult() { - Logger.OpenId.InfoFormat("AJAX (iframe) callback from OP: {0}", this.Page.Request.Url); - List<string> assignments = new List<string>(); - - var authResponse = this.RelyingPartyNonVerifying.GetResponse(); - if (authResponse.Status == AuthenticationStatus.Authenticated) { - this.OnUnconfirmedPositiveAssertion(); - foreach (var pair in this.clientScriptExtensions) { - IClientScriptExtensionResponse extension = (IClientScriptExtensionResponse)authResponse.GetExtension(pair.Key); - if (extension == null) { - continue; - } - var positiveResponse = (PositiveAuthenticationResponse)authResponse; - string js = extension.InitializeJavaScriptData(positiveResponse.Response); - if (string.IsNullOrEmpty(js)) { - js = "null"; - } - assignments.Add(pair.Value + " = " + js); - } - } - - this.CallbackUserAgentMethod("dnoi_internal.processAuthorizationResult(document.URL)", assignments.ToArray()); - } } } diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdAjaxTextBox.js b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdAjaxTextBox.js index 1078003..c549b17 100644 --- a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdAjaxTextBox.js +++ b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdAjaxTextBox.js @@ -4,46 +4,12 @@ // </copyright> //----------------------------------------------------------------------- -// Options that can be set on the host page: -//window.openid_visible_iframe = true; // causes the hidden iframe to show up -//window.openid_trace = true; // causes lots of messages - -function trace(msg) { - if (window.openid_trace) { - if (!window.tracediv) { - window.tracediv = document.createElement("ol"); - document.body.appendChild(window.tracediv); - } - var el = document.createElement("li"); - el.appendChild(document.createTextNode(msg)); - window.tracediv.appendChild(el); - //alert(msg); - } -} - -/// <summary>Removes a given element from the array.</summary> -/// <returns>True if the element was in the array, or false if it was not found.</returns> -Array.prototype.remove = function(element) { - function elementToRemoveLast(a, b) { - if (a == element) { return 1; } - if (b == element) { return -1; } - return 0; - } - this.sort(elementToRemoveLast); - if (this[this.length - 1] == element) { - this.pop(); - return true; - } else { - return false; - } -}; - function initAjaxOpenId(box, openid_logo_url, dotnetopenid_logo_url, spinner_url, success_icon_url, failure_icon_url, throttle, timeout, assertionReceivedCode, loginButtonText, loginButtonToolTip, retryButtonText, retryButtonToolTip, busyToolTip, identifierRequiredMessage, loginInProgressMessage, authenticatedByToolTip, authenticatedAsToolTip, authenticationFailedToolTip, - discoverCallback, discoveryFailedCallback) { + discoverCallback/*removeme*/, discoveryFailedCallback) { box.dnoi_internal = new Object(); if (assertionReceivedCode) { box.dnoi_internal.onauthenticated = function(sender, e) { eval(assertionReceivedCode); } @@ -51,79 +17,8 @@ function initAjaxOpenId(box, openid_logo_url, dotnetopenid_logo_url, spinner_url box.dnoi_internal.originalBackground = box.style.background; box.timeout = timeout; - box.dnoi_internal.discoverIdentifier = discoverCallback; - box.dnoi_internal.authenticationRequests = new Array(); - - // The possible authentication results - var authSuccess = new Object(); - var authRefused = new Object(); - var timedOut = new Object(); - - function FrameManager(maxFrames) { - this.queuedWork = new Array(); - this.frames = new Array(); - this.maxFrames = maxFrames; - - /// <summary>Called to queue up some work that will use an iframe as soon as it is available.</summary> - /// <param name="job"> - /// A delegate that must return the url to point to iframe to. - /// Its first parameter is the iframe created to service the request. - /// It will only be called when the work actually begins. - /// </param> - this.enqueueWork = function(job) { - // Assign an iframe to this task immediately if there is one available. - if (this.frames.length < this.maxFrames) { - this.createIFrame(job); - } else { - this.queuedWork.unshift(job); - } - }; - /// <summary>Clears the job queue and immediately closes all iframes.</summary> - this.cancelAllWork = function() { - trace('Canceling all open and pending iframes.'); - while (this.queuedWork.pop()); - this.closeFrames(); - }; - - /// <summary>An event fired when a frame is closing.</summary> - this.onJobCompleted = function() { - // If there is a job in the queue, go ahead and start it up. - if (job = this.queuedWork.pop()) { - this.createIFrame(job); - } - } - - this.createIFrame = function(job) { - var iframe = document.createElement("iframe"); - if (!window.openid_visible_iframe) { - iframe.setAttribute("width", 0); - iframe.setAttribute("height", 0); - iframe.setAttribute("style", "display: none"); - } - iframe.setAttribute("src", job(iframe)); - iframe.openidBox = box; - box.parentNode.insertBefore(iframe, box); - this.frames.push(iframe); - return iframe; - }; - this.closeFrames = function() { - if (this.frames.length == 0) { return false; } - for (var i = 0; i < this.frames.length; i++) { - if (this.frames[i].parentNode) { this.frames[i].parentNode.removeChild(this.frames[i]); } - } - while (this.frames.length > 0) { this.frames.pop(); } - return true; - }; - this.closeFrame = function(frame) { - if (frame.parentNode) { frame.parentNode.removeChild(frame); } - var removed = this.frames.remove(frame); - this.onJobCompleted(); - return removed; - }; - } - - box.dnoi_internal.authenticationIFrames = new FrameManager(throttle); + box.dnoi_internal.authenticationIFrames = new window.dnoa_internal.FrameManager(throttle); box.dnoi_internal.constructButton = function(text, tooltip, onclick) { var button = document.createElement('input'); @@ -199,14 +94,14 @@ function initAjaxOpenId(box, openid_logo_url, dotnetopenid_logo_url, spinner_url }; box.dnoi_internal.loginButton = box.dnoi_internal.constructButton(loginButtonText, loginButtonToolTip, function() { - var discoveryInfo = box.dnoi_internal.authenticationRequests[box.lastDiscoveredIdentifier]; + var discoveryInfo = window.dnoa_internal.discoveryResults[box.lastDiscoveredIdentifier]; if (discoveryInfo == null) { trace('Ooops! Somehow the login button click event was invoked, but no openid discovery information for ' + box.lastDiscoveredIdentifier + ' is available.'); return; } // The login button always sends a setup message to the first OP. var selectedProvider = discoveryInfo[0]; - selectedProvider.trySetup(); + selectedProvider.loginPopup(box.dnoi_internal.onAuthSuccess, box.dnoi_internal.onAuthFailed); return false; }); box.dnoi_internal.retryButton = box.dnoi_internal.constructButton(retryButtonText, retryButtonToolTip, function() { @@ -250,13 +145,14 @@ function initAjaxOpenId(box, openid_logo_url, dotnetopenid_logo_url, spinner_url box.dnoi_internal.op_logo.style.visibility = 'visible'; box.dnoi_internal.op_logo.title = box.dnoi_internal.op_logo.originalTitle.replace('{0}', authenticatedBy.getHost()); } - trace("OP icon size: " + box.dnoi_internal.op_logo.fileSize); - if (opLogo == null || box.dnoi_internal.op_logo.fileSize == -1 /*IE*/ || box.dnoi_internal.op_logo.fileSize === undefined /* FF */) { - trace('recovering from missing OP icon'); - box.dnoi_internal.op_logo.style.visibility = 'hidden'; - box.dnoi_internal.openid_logo.style.visibility = 'visible'; - box.dnoi_internal.openid_logo.title = box.dnoi_internal.op_logo.originalTitle.replace('{0}', authenticatedBy.getHost()); - } + //trace("OP icon size: " + box.dnoi_internal.op_logo.fileSize); + // This just doesn't seem to work any more. +// if (opLogo == null || box.dnoi_internal.op_logo.fileSize == -1 /*IE*/ || box.dnoi_internal.op_logo.fileSize === undefined /* FF */) { +// trace('recovering from missing OP icon'); +// box.dnoi_internal.op_logo.style.visibility = 'hidden'; +// box.dnoi_internal.openid_logo.style.visibility = 'visible'; +// box.dnoi_internal.openid_logo.title = box.dnoi_internal.op_logo.originalTitle.replace('{0}', authenticatedBy.getHost()); +// } box.dnoi_internal.success_icon.style.visibility = 'visible'; box.dnoi_internal.success_icon.title = box.dnoi_internal.success_icon.originalTitle.replace('{0}', authenticatedAs); box.title = box.dnoi_internal.claimedIdentifier; @@ -291,20 +187,20 @@ function initAjaxOpenId(box, openid_logo_url, dotnetopenid_logo_url, spinner_url } box.dnoi_internal.isBusy = function() { - var lastDiscovery = box.dnoi_internal.authenticationRequests[box.lastDiscoveredIdentifier]; + var lastDiscovery = window.dnoa_internal.discoveryResults[box.lastDiscoveredIdentifier]; return box.dnoi_internal.state == 'discovering' || (lastDiscovery && lastDiscovery.busy()); }; box.dnoi_internal.canAttemptLogin = function() { if (box.value.length == 0) return false; - if (box.dnoi_internal.authenticationRequests[box.value] == null) return false; + if (window.dnoa_internal.discoveryResults[box.value] == null) return false; if (box.dnoi_internal.state == 'failed') return false; return true; }; box.dnoi_internal.getUserSuppliedIdentifierResults = function() { - return box.dnoi_internal.authenticationRequests[box.value]; + return window.dnoa_internal.discoveryResults[box.value]; } box.dnoi_internal.isAuthenticated = function() { @@ -316,7 +212,7 @@ function initAjaxOpenId(box, openid_logo_url, dotnetopenid_logo_url, spinner_url var hiddenField = findOrCreateHiddenField(); if (box.dnoi_internal.isAuthenticated()) { // stick the result in a hidden field so the RP can verify it - hiddenField.setAttribute("value", box.dnoi_internal.authenticationRequests[box.value].successAuthData); + hiddenField.setAttribute("value", window.dnoa_internal.discoveryResults[box.value].successAuthData); } else { hiddenField.setAttribute("value", ''); if (box.dnoi_internal.isBusy()) { @@ -380,180 +276,26 @@ function initAjaxOpenId(box, openid_logo_url, dotnetopenid_logo_url, spinner_url /// <summary> /// Returns the URL of the authenticating OP's logo so it can be displayed to the user. /// </summary> - box.dnoi_internal.deriveOPFavIcon = function() { - var response = box.dnoi_internal.getUserSuppliedIdentifierResults().successAuthData; - if (!response || response.length == 0) return; - var authResult = new Uri(response); - var opUri; - if (authResult.getQueryArgValue("openid.op_endpoint")) { - opUri = new Uri(authResult.getQueryArgValue("openid.op_endpoint")); - } if (authResult.getQueryArgValue("dotnetopenid.op_endpoint")) { - opUri = new Uri(authResult.getQueryArgValue("dotnetopenid.op_endpoint")); - } else if (authResult.getQueryArgValue("openid.user_setup_url")) { - opUri = new Uri(authResult.getQueryArgValue("openid.user_setup_url")); - } else return null; - var favicon = opUri.getAuthority() + "/favicon.ico"; - return favicon; - }; - - box.dnoi_internal.createDiscoveryInfo = function(discoveryInfo, identifier) { - this.identifier = identifier; - // The claimed identifier may be null if the user provided an OP Identifier. - this.claimedIdentifier = discoveryInfo.claimedIdentifier; - trace('Discovered claimed identifier: ' + this.claimedIdentifier); - - // Add extra tracking bits and behaviors. - this.findByEndpoint = function(opEndpoint) { - for (var i = 0; i < this.length; i++) { - if (this[i].endpoint == opEndpoint) { - return this[i]; - } + /// <param name="opUri">The OP Endpoint, if known.</param> + box.dnoi_internal.deriveOPFavIcon = function(opUri) { + if (!opUri) { + var response = box.dnoi_internal.getUserSuppliedIdentifierResults().successAuthData; + if (!response || response.length == 0) { + trace('No favicon because no successAuthData.'); + return; } - }; - this.findSuccessfulRequest = function() { - for (var i = 0; i < this.length; i++) { - if (this[i].result == authSuccess) { - return this[i]; - } - } - }; - this.busy = function() { - for (var i = 0; i < this.length; i++) { - if (this[i].busy()) { - return true; - } - } - }; - this.abortAll = function() { - // Abort all other asynchronous authentication attempts that may be in progress. - box.dnoi_internal.authenticationIFrames.cancelAllWork(); - for (var i = 0; i < this.length; i++) { - this[i].abort(); - } - }; - this.tryImmediate = function() { - if (this.length > 0) { - for (var i = 0; i < this.length; i++) { - box.dnoi_internal.authenticationIFrames.enqueueWork(this[i].tryImmediate); - } - } else { - box.dnoi_internal.discoveryFailed(null, this.identifier); - } - }; - - this.length = discoveryInfo.requests.length; - for (var i = 0; i < discoveryInfo.requests.length; i++) { - this[i] = new box.dnoi_internal.createTrackingRequest(discoveryInfo.requests[i], identifier); - } - }; - - box.dnoi_internal.createTrackingRequest = function(requestInfo, identifier) { - // It's possible during a postback that discovered request URLs are not available. - this.immediate = requestInfo.immediate ? new Uri(requestInfo.immediate) : null; - this.setup = requestInfo.setup ? new Uri(requestInfo.setup) : null; - this.endpoint = new Uri(requestInfo.endpoint); - this.identifier = identifier; - var self = this; // closure so that delegates have the right instance - - this.host = self.endpoint.getHost(); - - this.getDiscoveryInfo = function() { - return box.dnoi_internal.authenticationRequests[self.identifier]; + var authResult = new window.dnoa_internal.Uri(response); + if (authResult.getQueryArgValue("openid.op_endpoint")) { + opUri = new window.dnoa_internal.Uri(authResult.getQueryArgValue("openid.op_endpoint")); + } else if (authResult.getQueryArgValue("dnoa.op_endpoint")) { + opUri = new window.dnoa_internal.Uri(authResult.getQueryArgValue("dnoa.op_endpoint")); + } else if (authResult.getQueryArgValue("openid.user_setup_url")) { + opUri = new window.dnoa_internal.Uri(authResult.getQueryArgValue("openid.user_setup_url")); + } else return null; } - - this.busy = function() { - return self.iframe != null || self.popup != null; - }; - - this.completeAttempt = function() { - if (!self.busy()) return false; - if (self.iframe) { - trace('iframe hosting ' + self.endpoint + ' now CLOSING.'); - box.dnoi_internal.authenticationIFrames.closeFrame(self.iframe); - self.iframe = null; - } - if (self.popup) { - self.popup.close(); - self.popup = null; - } - if (self.timeout) { - window.clearTimeout(self.timeout); - self.timeout = null; - } - - if (!self.getDiscoveryInfo().busy() && self.getDiscoveryInfo().findSuccessfulRequest() == null) { - trace('No asynchronous authentication attempt is in progress. Display setup view.'); - // visual cue that auth failed - box.dnoi_internal.setVisualCue('setup'); - } - - return true; - }; - - this.authenticationTimedOut = function() { - if (self.completeAttempt()) { - trace(self.host + " timed out"); - self.result = timedOut; - } - }; - this.authSuccess = function(authUri) { - if (self.completeAttempt()) { - trace(self.host + " authenticated!"); - self.result = authSuccess; - self.response = authUri; - box.dnoi_internal.authenticationRequests[self.identifier].abortAll(); - } - }; - this.authFailed = function() { - if (self.completeAttempt()) { - //trace(self.host + " failed authentication"); - self.result = authRefused; - } - }; - this.abort = function() { - if (self.completeAttempt()) { - trace(self.host + " aborted"); - // leave the result as whatever it was before. - } - }; - - this.tryImmediate = function(iframe) { - self.abort(); // ensure no concurrent attempts - self.timeout = setTimeout(function() { self.authenticationTimedOut(); }, box.timeout); - trace('iframe hosting ' + self.endpoint + ' now OPENING.'); - self.iframe = iframe; - //trace('initiating auth attempt with: ' + self.immediate); - return self.immediate.toString(); - }; - this.trySetup = function() { - self.abort(); // ensure no concurrent attempts - window.waiting_openidBox = box; - var width = 800; - var height = 600; - if (self.setup.getQueryArgValue("openid.return_to").indexOf("dotnetopenid.popupUISupported") >= 0) { - width = 450; - height = 500; - } - - var left = (screen.width - width) / 2; - var top = (screen.height - height) / 2; - self.popup = window.open(self.setup, 'opLogin', 'status=0,toolbar=0,location=1,resizable=1,scrollbars=1,left=' + left + ',top=' + top + ',width=' + width + ',height=' + height); - - // If the OP supports the UI extension it MAY close its own window - // for a negative assertion. We must be able to recover from that scenario. - var localSelf = self; - self.popupCloseChecker = window.setInterval(function() { - if (localSelf.popup && localSelf.popup.closed) { - // So the user canceled and the window closed. - // It turns out we hae nothing special to do. - // If we were graying out the entire page while the child window was up, - // we would probably revert that here. - trace('User or OP canceled by closing the window.'); - window.clearInterval(localSelf.popupCloseChecker); - localSelf.popup = null; - } - }, 250); - }; + var favicon = opUri.getAuthority() + "/favicon.ico"; + trace('Guessing favicon location of: ' + favicon); + return favicon; }; /***************************************** @@ -565,7 +307,8 @@ function initAjaxOpenId(box, openid_logo_url, dotnetopenid_logo_url, spinner_url box.dnoi_internal.authenticationIFrames.closeFrames(); box.dnoi_internal.setVisualCue('discovering'); box.lastDiscoveredIdentifier = identifier; - box.dnoi_internal.discoverIdentifier(identifier, box.dnoi_internal.discoveryResult, box.dnoi_internal.discoveryFailed); + var openid = new window.OpenIdIdentifier(identifier); + openid.discover(box.dnoi_internal.discoverySuccess, box.dnoi_internal.discoveryFailed); }; /// <summary>Callback that is invoked when discovery fails.</summary> @@ -577,95 +320,64 @@ function initAjaxOpenId(box, openid_logo_url, dotnetopenid_logo_url, spinner_url /// <summary>Callback that is invoked when discovery results are available.</summary> /// <param name="discoveryResult">The JSON object containing the OpenID auth requests.</param> /// <param name="identifier">The identifier that discovery was performed on.</param> - box.dnoi_internal.discoveryResult = function(discoveryResult, identifier) { - // Deserialize the JSON object and store the result if it was a successful discovery. - discoveryResult = eval('(' + discoveryResult + ')'); - // Store the discovery results and added behavior for later use. - box.dnoi_internal.authenticationRequests[identifier] = discoveryBehavior = new box.dnoi_internal.createDiscoveryInfo(discoveryResult, identifier); - + box.dnoi_internal.discoverySuccess = function(discoveryResult) { // Only act on the discovery event if we're still interested in the result. // If the user already changed the identifier since discovery was initiated, // we aren't interested in it any more. - if (identifier == box.lastDiscoveredIdentifier) { - discoveryBehavior.tryImmediate(); + if (discoveryResult.userSuppliedIdentifier === box.lastDiscoveredIdentifier) { + // Start pre-fetching the OP favicons + for (var i = 0; i < discoveryResult.length; i++) { + var favicon = box.dnoi_internal.deriveOPFavIcon(discoveryResult[i].endpoint); + if (favicon) { + trace('Prefetching ' + favicon); + box.dnoi_internal.prefetchImage(favicon); + } + } + discoveryResult.loginBackground( + box.dnoi_internal.authenticationIFrames, + box.dnoi_internal.onAuthSuccess, + box.dnoi_internal.onAuthFailed, + box.dnoi_internal.lastAuthenticationFailed, + box.timeout); } } - /// <summary>Invoked by RP web server when an authentication has completed.</summary> - /// <remarks>The duty of this method is to distribute the notification to the appropriate tracking object.</remarks> - box.dnoi_internal.processAuthorizationResult = function(resultUrl) { - self.waiting_openidBox = null; - //trace('processAuthorizationResult ' + resultUrl); - var resultUri = new Uri(resultUrl); + box.dnoi_internal.lastAuthenticationFailed = function() { + trace('No asynchronous authentication attempt is in progress. Display setup view.'); + // visual cue that auth failed + box.dnoi_internal.setVisualCue('setup'); + }; - // Find the tracking object responsible for this request. - var discoveryInfo = box.dnoi_internal.authenticationRequests[resultUri.getQueryArgValue('dotnetopenid.userSuppliedIdentifier')]; - if (discoveryInfo == null) { - trace('processAuthorizationResult called but no userSuppliedIdentifier parameter was found. Exiting function.'); - return; + box.dnoi_internal.onAuthSuccess = function(discoveryResult, respondingEndpoint) { + // visual cue that auth was successful + box.dnoi_internal.claimedIdentifier = discoveryResult.claimedIdentifier; + box.dnoi_internal.setVisualCue('authenticated', respondingEndpoint.endpoint, discoveryResult.claimedIdentifier); + if (box.dnoi_internal.onauthenticated) { + box.dnoi_internal.onauthenticated(box); } - var opEndpoint = resultUri.getQueryArgValue("openid.op_endpoint") ? resultUri.getQueryArgValue("openid.op_endpoint") : resultUri.getQueryArgValue("dotnetopenid.op_endpoint"); - var tracker = discoveryInfo.findByEndpoint(opEndpoint); - //trace('Auth result for ' + tracker.host + ' received:\n' + resultUrl); - - if (isAuthSuccessful(resultUri)) { - tracker.authSuccess(resultUri); - - discoveryInfo.successAuthData = resultUrl; - var claimed_id = resultUri.getQueryArgValue("openid.claimed_id"); - if (claimed_id && claimed_id != discoveryInfo.claimedIdentifier) { - discoveryInfo.claimedIdentifier = resultUri.getQueryArgValue("openid.claimed_id"); - trace('Authenticated as ' + claimed_id); - } - // visual cue that auth was successful - box.dnoi_internal.claimedIdentifier = discoveryInfo.claimedIdentifier; - box.dnoi_internal.setVisualCue('authenticated', tracker.endpoint, discoveryInfo.claimedIdentifier); - if (box.dnoi_internal.onauthenticated) { - box.dnoi_internal.onauthenticated(box); - } - if (box.dnoi_internal.submitPending) { - // We submit the form BEFORE resetting the submitPending so - // the submit handler knows we've already tried this route. - if (box.dnoi_internal.submitPending == true) { - box.parentForm.submit(); - } else { - box.dnoi_internal.submitPending.click(); - } + if (box.dnoi_internal.submitPending) { + // We submit the form BEFORE resetting the submitPending so + // the submit handler knows we've already tried this route. + if (box.dnoi_internal.submitPending == true) { + box.parentForm.submit(); + } else { + box.dnoi_internal.submitPending.click(); } - } else { - tracker.authFailed(); - } - box.dnoi_internal.submitPending = null; - }; - - function isAuthSuccessful(resultUri) { - if (isOpenID2Response(resultUri)) { - return resultUri.getQueryArgValue("openid.mode") == "id_res"; - } else { - return resultUri.getQueryArgValue("openid.mode") == "id_res" && !resultUri.containsQueryArg("openid.user_setup_url"); + box.dnoi_internal.submitPending = null; } }; - function isOpenID2Response(resultUri) { - return resultUri.containsQueryArg("openid.ns"); + box.dnoi_internal.onAuthFailed = function() { + box.dnoi_internal.submitPending = null; }; - + box.onblur = function(event) { - var discoveryInfo = box.dnoi_internal.authenticationRequests[box.value]; - if (discoveryInfo == null) { - if (box.value.length > 0) { - box.dnoi_internal.performDiscovery(box.value); - } else { - box.dnoi_internal.setVisualCue(); - } + if (box.value.length > 0) { + box.dnoi_internal.performDiscovery(box.value); } else { - if ((priorSuccess = discoveryInfo.findSuccessfulRequest())) { - box.dnoi_internal.setVisualCue('authenticated', priorSuccess.endpoint, discoveryInfo.claimedIdentifier); - } else { - discoveryInfo.tryImmediate(); - } + box.dnoi_internal.setVisualCue(); } return true; }; @@ -679,99 +391,25 @@ function initAjaxOpenId(box, openid_logo_url, dotnetopenid_logo_url, spinner_url // Restore a previously achieved state (from pre-postback) if it is given. var oldAuth = findOrCreateHiddenField().value; if (oldAuth.length > 0) { - var oldAuthResult = new Uri(oldAuth); + var oldAuthResult = new window.dnoa_internal.Uri(oldAuth); // The control ensures that we ALWAYS have an OpenID 2.0-style claimed_id attribute, even against // 1.0 Providers via the return_to URL mechanism. - var claimedId = oldAuthResult.getQueryArgValue("dotnetopenid.claimed_id"); - var endpoint = oldAuthResult.getQueryArgValue("dotnetopenid.op_endpoint"); + var claimedId = oldAuthResult.getQueryArgValue("dnoa.claimed_id"); + var endpoint = oldAuthResult.getQueryArgValue("dnoa.op_endpoint"); + var userSuppliedIdentifier = oldAuthResult.getQueryArgValue('dnoa.userSuppliedIdentifier'); + +// TODO: // We weren't given a full discovery history, but we can spoof this much from the // authentication assertion. - box.dnoi_internal.authenticationRequests[box.value] = new box.dnoi_internal.createDiscoveryInfo({ + //new window.OpenIdIdentifier(userSuppliedIdentifier).discover + var discoveryResult = { claimedIdentifier: claimedId, requests: [{ endpoint: endpoint }] - }, box.value); - - box.dnoi_internal.processAuthorizationResult(oldAuthResult.toString()); - } -} - -function Uri(url) { - this.originalUri = url; - - this.toString = function() { - return this.originalUri; - }; + }; - this.getAuthority = function() { - var authority = this.getScheme() + "://" + this.getHost(); - return authority; - } + // window.dnoa_internal.discoveryResults[box.value] = discoveryResult; - this.getHost = function() { - var hostStartIdx = this.originalUri.indexOf("://") + 3; - var hostEndIndex = this.originalUri.indexOf("/", hostStartIdx); - if (hostEndIndex < 0) hostEndIndex = this.originalUri.length; - var host = this.originalUri.substr(hostStartIdx, hostEndIndex - hostStartIdx); - return host; + // restore old state from before postback + //window.dnoa_internal.processAuthorizationResult(oldAuthResult.toString()); } - - this.getScheme = function() { - var schemeStartIdx = this.indexOf("://"); - return this.originalUri.substr(this.originalUri, schemeStartIdx); - } - - this.trimFragment = function() { - var hashmark = this.originalUri.indexOf('#'); - if (hashmark >= 0) { - return new Uri(this.originalUri.substr(0, hashmark)); - } - return this; - }; - - this.appendQueryVariable = function(name, value) { - var pair = encodeURI(name) + "=" + encodeURI(value); - if (this.originalUri.indexOf('?') >= 0) { - this.originalUri = this.originalUri + "&" + pair; - } else { - this.originalUri = this.originalUri + "?" + pair; - } - }; - - function KeyValuePair(key, value) { - this.key = key; - this.value = value; - }; - - this.Pairs = new Array(); - - var queryBeginsAt = this.originalUri.indexOf('?'); - if (queryBeginsAt >= 0) { - this.queryString = url.substr(queryBeginsAt + 1); - var queryStringPairs = this.queryString.split('&'); - - for (var i = 0; i < queryStringPairs.length; i++) { - var equalsAt = queryStringPairs[i].indexOf('='); - left = (equalsAt >= 0) ? queryStringPairs[i].substring(0, equalsAt) : null; - right = (equalsAt >= 0) ? queryStringPairs[i].substring(equalsAt + 1) : queryStringPairs[i]; - this.Pairs.push(new KeyValuePair(unescape(left), unescape(right))); - } - }; - - this.getQueryArgValue = function(key) { - for (var i = 0; i < this.Pairs.length; i++) { - if (this.Pairs[i].key == key) { - return this.Pairs[i].value; - } - } - }; - - this.containsQueryArg = function(key) { - return this.getQueryArgValue(key); - }; - - this.indexOf = function(args) { - return this.originalUri.indexOf(args); - }; - - return this; -}; +} diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdMobileTextBox.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdMobileTextBox.cs index a917e24..a56f257 100644 --- a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdMobileTextBox.cs +++ b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdMobileTextBox.cs @@ -610,7 +610,7 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { } // Add state that needs to survive across the redirect. - this.Request.AddCallbackArguments(UsePersistentCookieCallbackKey, this.UsePersistentCookie.ToString(CultureInfo.InvariantCulture)); + this.Request.SetCallbackArgument(UsePersistentCookieCallbackKey, this.UsePersistentCookie.ToString(CultureInfo.InvariantCulture)); } else { Logger.OpenId.WarnFormat("An invalid identifier was entered ({0}), but not caught by any validation routine.", this.Text); this.Request = null; diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingPartyAjaxControlBase.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingPartyAjaxControlBase.cs index 5ab8053..8c0a7a7 100644 --- a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingPartyAjaxControlBase.cs +++ b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingPartyAjaxControlBase.cs @@ -12,24 +12,18 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { 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; /// <summary> /// A common base class for OpenID Relying Party controls. /// </summary> - internal abstract class OpenIdRelyingPartyAjaxControlBase : OpenIdRelyingPartyControlBase, ICallbackEventHandler { + public abstract class OpenIdRelyingPartyAjaxControlBase : OpenIdRelyingPartyControlBase, ICallbackEventHandler { /// <summary> /// The manifest resource name of the javascript file to include on the hosting page. /// </summary> @@ -46,6 +40,45 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { protected const string CallbackJsFunctionAsync = "window.dnoa_internal.callbackAsync"; /// <summary> + /// The "dnoa.op_endpoint" string. + /// </summary> + private const string OPEndpointParameterName = OpenIdUtilities.CustomParameterPrefix + "op_endpoint"; + + /// <summary> + /// The "dnoa.claimed_id" string. + /// </summary> + private const string ClaimedIdParameterName = OpenIdUtilities.CustomParameterPrefix + "claimed_id"; + + #region Property viewstate keys + + /// <summary> + /// The viewstate key to use for storing the value of the a successful authentication. + /// </summary> + private const string AuthDataViewStateKey = "AuthData"; + + /// <summary> + /// The viewstate key to use for storing the value of the <see cref="AuthenticationResponse"/> property. + /// </summary> + private const string AuthenticationResponseViewStateKey = "AuthenticationResponse"; + + /// <summary> + /// The viewstate key to use for storing the value of the <see cref="AuthenticationProcessedAlready"/> property. + /// </summary> + private const string AuthenticationProcessedAlreadyViewStateKey = "AuthenticationProcessedAlready"; + + #endregion + + /// <summary> + /// The authentication response that just came in. + /// </summary> + private IAuthenticationResponse authenticationResponse; + + /// <summary> + /// Backing field for the <see cref="RelyingPartyNonVerifying"/> property. + /// </summary> + private static OpenIdRelyingParty relyingPartyNonVerifying; + + /// <summary> /// Stores the result of a AJAX callback discovery. /// </summary> private string discoveryResult; @@ -61,10 +94,26 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { /// </summary> protected OpenIdRelyingPartyAjaxControlBase() { // The AJAX login style always uses popups (or invisible iframes). - this.Popup = PopupBehavior.Always; + base.Popup = PopupBehavior.Always; + + // The expected use case for the AJAX login box is for comments... not logging in. + this.LoginMode = LoginSiteNotification.None; } /// <summary> + /// Fired when a Provider sends back a positive assertion to this control, + /// but the authentication has not yet been verified. + /// </summary> + /// <remarks> + /// <b>No security critical decisions should be made within event handlers + /// for this event</b> as the authenticity of the assertion has not been + /// verified yet. All security related code should go in the event handler + /// for the <see cref="LoggedIn"/> event. + /// </remarks> + [Description("Fired when a Provider sends back a positive assertion to this control, but the authentication has not yet been verified.")] + public event EventHandler<OpenIdEventArgs> UnconfirmedPositiveAssertion; + + /// <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> @@ -74,6 +123,90 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { set { ErrorUtilities.VerifySupported(value == base.Popup, OpenIdStrings.PropertyValueNotSupported); } } + /// <summary> + /// Gets the completed authentication response. + /// </summary> + 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); + 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; + } + } + + /// <summary> + /// Gets the name of the open id auth data form key. + /// </summary> + /// <value>Usually a concatenation of the control's name and <c>"_openidAuthData"</c>.</value> + protected abstract string OpenIdAuthDataFormKey { get; } + + /// <summary> + /// Gets the relying party to use when verification of incoming messages is NOT wanted. + /// </summary> + private static OpenIdRelyingParty RelyingPartyNonVerifying { + get { + if (relyingPartyNonVerifying == null) { + relyingPartyNonVerifying = OpenIdRelyingParty.CreateNonVerifying(); + } + return relyingPartyNonVerifying; + } + } + + /// <summary> + /// Gets or sets a value indicating whether an authentication in the page's view state + /// has already been processed and appropriate events fired. + /// </summary> + private bool AuthenticationProcessedAlready { + get { return (bool)(ViewState[AuthenticationProcessedAlreadyViewStateKey] ?? false); } + set { ViewState[AuthenticationProcessedAlreadyViewStateKey] = value; } + } + + /// <summary> + /// 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. + /// </summary> + /// <typeparam name="T">The extension <i>response</i> type that will read data from the assertion.</typeparam> + /// <param name="propertyName">The property name on the openid_identifier input box object that will be used to store the extension data. For example: sreg</param> + /// <remarks> + /// This method should be called from the <see cref="UnconfirmedPositiveAssertion"/> event handler. + /// </remarks> + [SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter", Justification = "By design")] + public void RegisterClientScriptExtension<T>(string propertyName) where T : IClientScriptExtensionResponse { + ErrorUtilities.VerifyNonZeroLength(propertyName, "propertyName"); + ErrorUtilities.VerifyArgumentNamed(!this.clientScriptExtensions.ContainsValue(propertyName), "propertyName", OpenIdStrings.ClientScriptExtensionPropertyNameCollision, propertyName); + foreach (var ext in this.clientScriptExtensions.Keys) { + ErrorUtilities.VerifyArgument(ext != typeof(T), OpenIdStrings.ClientScriptExtensionTypeCollision, typeof(T).FullName); + } + this.clientScriptExtensions.Add(typeof(T), propertyName); + } + #region ICallbackEventHandler Members /// <summary> @@ -118,7 +251,6 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { discoveryResultBuilder.AppendFormat("claimedIdentifier: {0},", MessagingUtilities.GetSafeJavascriptValue(requests.First().ClaimedIdentifier)); discoveryResultBuilder.Append("requests: ["); foreach (IAuthenticationRequest request in requests) { - this.OnLoggingIn(request); discoveryResultBuilder.Append("{"); discoveryResultBuilder.AppendFormat("endpoint: {0},", MessagingUtilities.GetSafeJavascriptValue(request.Provider.Uri.AbsoluteUri)); request.Mode = AuthenticationRequestMode.Immediate; @@ -139,6 +271,7 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { discoveryResultBuilder.Append("requests: new Array(),"); discoveryResultBuilder.AppendFormat("error: {0}", MessagingUtilities.GetSafeJavascriptValue(ex.Message)); } + discoveryResultBuilder.Append("}"); this.discoveryResult = discoveryResultBuilder.ToString(); } @@ -146,6 +279,39 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { #endregion /// <summary> + /// Fires the <see cref="UnconfirmedPositiveAssertion"/> event. + /// </summary> + protected virtual void OnUnconfirmedPositiveAssertion() { + var unconfirmedPositiveAssertion = this.UnconfirmedPositiveAssertion; + if (unconfirmedPositiveAssertion != null) { + unconfirmedPositiveAssertion(this, null); + } + } + + /// <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); + + // 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.AuthenticationProcessedAlready && this.AuthenticationResponse != null) { + // Only process messages targeted at this control. + // Note that Stateless mode causes no receiver to be indicated. + string receiver = this.AuthenticationResponse.GetCallbackArgument(ReturnToReceivingControlId); + if (receiver == null || receiver == this.ClientID) { + this.ProcessResponse(this.AuthenticationResponse); + this.AuthenticationProcessedAlready = true; + } + } + } + } + + /// <summary> /// Creates the authentication requests for a given user-supplied Identifier. /// </summary> /// <returns>A sequence of authentication requests, any one of which may be @@ -177,6 +343,62 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { } /// <summary> + /// Sends server control content to a provided <see cref="T:System.Web.UI.HtmlTextWriter"/> object, which writes the content to be rendered on the client. + /// </summary> + /// <param name="writer">The <see cref="T:System.Web.UI.HtmlTextWriter"/> object that receives the server control content.</param> + protected override void Render(HtmlTextWriter writer) { + 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(); + } + } + + /// <summary> + /// Notifies the user agent via an AJAX response of a completed authentication attempt. + /// </summary> + protected override void ScriptClosingPopupOrIFrame() { + Logger.OpenId.InfoFormat("AJAX (iframe) callback from OP: {0}", this.Page.Request.Url); + List<string> assignments = new List<string>(); + + var authResponse = RelyingPartyNonVerifying.GetResponse(); + if (authResponse.Status == AuthenticationStatus.Authenticated) { + this.OnUnconfirmedPositiveAssertion(); // event handler will fill the clientScriptExtensions collection. + foreach (var pair in this.clientScriptExtensions) { + IClientScriptExtensionResponse extension = (IClientScriptExtensionResponse)authResponse.GetExtension(pair.Key); + if (extension == null) { + continue; + } + var positiveResponse = (PositiveAuthenticationResponse)authResponse; + string js = extension.InitializeJavaScriptData(positiveResponse.Response); + if (string.IsNullOrEmpty(js)) { + js = "null"; + } + assignments.Add(pair.Value + " = " + js); + } + } + + string payload = "document.URL"; + if (Page.Request.HttpMethod == "POST") { + // Promote all form variables to the query string, but since it won't be passed + // to any server (this is a javascript window-to-window transfer) the length of + // it can be arbitrarily long, whereas it was POSTed here probably because it + // was too long for HTTP transit. + UriBuilder payloadUri = new UriBuilder(Page.Request.Url); + payloadUri.AppendQueryArgs(Page.Request.Form.ToDictionary()); + payload = MessagingUtilities.GetSafeJavascriptValue(payloadUri.Uri.AbsoluteUri); + } + this.CallbackUserAgentMethod("dnoa_internal.processAuthorizationResult(" + payload + ")", assignments.ToArray()); + } + + /// <summary> /// Creates the authentication requests for a given user-supplied Identifier. /// </summary> /// <param name="requests">The authentication requests to prepare.</param> @@ -190,25 +412,20 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { // Configure each generated request. int reqIndex = 0; foreach (var req in requests) { - req.AddCallbackArguments("index", (reqIndex++).ToString(CultureInfo.InvariantCulture)); - - if (req.Provider.IsExtensionSupported<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.AddCallbackArguments("dotnetopenid.popupUISupported", "1"); - } + req.SetCallbackArgument("index", (reqIndex++).ToString(CultureInfo.InvariantCulture)); // If the ReturnToUrl was explicitly set, we'll need to reset our first parameter - if (string.IsNullOrEmpty(HttpUtility.ParseQueryString(req.ReturnToUrl.Query)["dotnetopenid.userSuppliedIdentifier"])) { - req.AddCallbackArguments("dotnetopenid.userSuppliedIdentifier", this.Identifier); + if (string.IsNullOrEmpty(HttpUtility.ParseQueryString(req.ReturnToUrl.Query)[AuthenticationRequest.UserSuppliedIdentifierParameterName])) { + req.SetCallbackArgument(AuthenticationRequest.UserSuppliedIdentifierParameterName, this.Identifier.OriginalString); } // Our javascript needs to let the user know which endpoint responded. So we force it here. // This gives us the info even for 1.0 OPs and 2.0 setup_required responses. - req.AddCallbackArguments("dotnetopenid.op_endpoint", req.Provider.Uri.AbsoluteUri); - req.AddCallbackArguments("dotnetopenid.claimed_id", (string)req.ClaimedIdentifier ?? string.Empty); + req.SetCallbackArgument(OPEndpointParameterName, req.Provider.Uri.AbsoluteUri); + req.SetCallbackArgument(ClaimedIdParameterName, (string)req.ClaimedIdentifier ?? string.Empty); + + // Inform ourselves in return_to that we're in a popup or iframe. + req.SetCallbackArgument(UIPopupCallbackKey, "1"); // We append a # at the end so that if the OP happens to support it, // the OpenID response "query string" is appended after the hash rather than before, resulting in the @@ -248,33 +465,6 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { } /// <summary> - /// Notifies the user agent via an AJAX response of a completed authentication attempt. - /// </summary> - private void ReportAuthenticationResult() { - Logger.OpenId.InfoFormat("AJAX (iframe) callback from OP: {0}", this.Page.Request.Url); - List<string> assignments = new List<string>(); - - var authResponse = this.RelyingParty.GetResponse(); - if (authResponse.Status == AuthenticationStatus.Authenticated) { - this.OnLoggedIn(authResponse); - foreach (var pair in this.clientScriptExtensions) { - IClientScriptExtensionResponse extension = (IClientScriptExtensionResponse)authResponse.GetExtension(pair.Key); - if (extension == null) { - continue; - } - var positiveResponse = (PositiveAuthenticationResponse)authResponse; - string js = extension.InitializeJavaScriptData(positiveResponse.Response); - if (string.IsNullOrEmpty(js)) { - js = "null"; - } - assignments.Add(pair.Value + " = " + js); - } - } - - this.CallbackUserAgentMethod("dnoi_internal.processAuthorizationResult(document.URL)", assignments.ToArray()); - } - - /// <summary> /// Invokes a method on a parent frame/window's OpenIdAjaxTextBox, /// and closes the calling popup window if applicable. /// </summary> @@ -295,7 +485,7 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { Logger.OpenId.InfoFormat("Sending Javascript callback: {0}", methodCall); Page.Response.Write(@"<html><body><script language='javascript'> var inPopup = !window.frameElement; - var objSrc = inPopup ? window.opener.waiting_openidBox : window.frameElement.openidBox; + var objSrc = inPopup ? window.opener : window.frameElement; "); if (preAssignments != null) { foreach (string assignment in preAssignments) { @@ -307,11 +497,11 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { // so we have to actually put the test for it ABOVE the call to objSrc.{0} so that it already // whether to call window.self.close() after the call. string htmlFormat = @" if (inPopup) {{ - objSrc.{0}; - window.self.close(); -}} else {{ - objSrc.{0}; -}} + objSrc.{0}; + window.self.close(); + }} else {{ + objSrc.{0}; + }} </script></body></html>"; Page.Response.Write(string.Format(CultureInfo.InvariantCulture, htmlFormat, methodCall)); Page.Response.End(); diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingPartyAjaxControlBase.js b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingPartyAjaxControlBase.js index 65b1b99..e0fcd98 100644 --- a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingPartyAjaxControlBase.js +++ b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingPartyAjaxControlBase.js @@ -1,5 +1,5 @@ //----------------------------------------------------------------------- -// <copyright file="OpenIdRelyingPartyControlBase.js" company="Andrew Arnott"> +// <copyright file="OpenIdRelyingPartyAjaxControlBase.js" company="Andrew Arnott"> // Copyright (c) Andrew Arnott. All rights reserved. // </copyright> //----------------------------------------------------------------------- @@ -8,15 +8,109 @@ if (window.dnoa_internal === undefined) { window.dnoa_internal = new Object(); }; +/// <summary>Removes a given element from the array.</summary> +/// <returns>True if the element was in the array, or false if it was not found.</returns> +Array.prototype.remove = function(element) { + function elementToRemoveLast(a, b) { + if (a == element) { return 1; } + if (b == element) { return -1; } + return 0; + } + this.sort(elementToRemoveLast); + if (this[this.length - 1] == element) { + this.pop(); + return true; + } else { + return false; + } +}; + window.dnoa_internal.discoveryResults = new Array(); // user supplied identifiers and discovery results +// The possible authentication results +window.dnoa_internal.authSuccess = new Object(); +window.dnoa_internal.authRefused = new Object(); +window.dnoa_internal.timedOut = new Object(); + +/// <summary>Instantiates a new FrameManager.</summary> +/// <param name="maxFrames">The maximum number of concurrent 'jobs' (authentication attempts).</param> +window.dnoa_internal.FrameManager = function(maxFrames) { + this.queuedWork = new Array(); + this.frames = new Array(); + this.maxFrames = maxFrames; + + /// <summary>Called to queue up some work that will use an iframe as soon as it is available.</summary> + /// <param name="job"> + /// A delegate that must return the url to point the iframe to. + /// Its first parameter is the iframe created to service the request. + /// It will only be called when the work actually begins. + /// </param> + /// <param name="p1">Arbitrary additional parameter to pass to the job.</param> + this.enqueueWork = function(job, p1) { + // Assign an iframe to this task immediately if there is one available. + if (this.frames.length < this.maxFrames) { + this.createIFrame(job, p1); + } else { + this.queuedWork.unshift({ job: job, p1: p1 }); + } + }; + + /// <summary>Clears the job queue and immediately closes all iframes.</summary> + this.cancelAllWork = function() { + trace('Canceling all open and pending iframes.'); + while (this.queuedWork.pop()); + this.closeFrames(); + }; + + /// <summary>An event fired when a frame is closing.</summary> + this.onJobCompleted = function() { + // If there is a job in the queue, go ahead and start it up. + if (jobDesc = this.queuedWork.pop()) { + this.createIFrame(jobDesc.job, jobDesc.p1); + } + } + + this.createIFrame = function(job, p1) { + var iframe = document.createElement("iframe"); + if (!window.openid_visible_iframe) { + iframe.setAttribute("width", 0); + iframe.setAttribute("height", 0); + iframe.setAttribute("style", "display: none"); + } + iframe.setAttribute("src", job(iframe, p1)); + iframe.dnoa_internal = window.dnoa_internal; + box.parentNode.insertBefore(iframe, box); + this.frames.push(iframe); + return iframe; + }; + + this.closeFrames = function() { + if (this.frames.length == 0) { return false; } + for (var i = 0; i < this.frames.length; i++) { + if (this.frames[i].parentNode) { this.frames[i].parentNode.removeChild(this.frames[i]); } + } + while (this.frames.length > 0) { this.frames.pop(); } + return true; + }; + + this.closeFrame = function(frame) { + if (frame.parentNode) { frame.parentNode.removeChild(frame); } + var removed = this.frames.remove(frame); + this.onJobCompleted(); + return removed; + }; +}; + /// <summary>Instantiates an object that represents an OpenID Identifier.</summary> -window.OpenId = function(identifier) { +window.OpenIdIdentifier = function(identifier) { /// <summary>Performs discovery on the identifier.</summary> - /// <param name="onCompleted">A function(DiscoveryResult) callback to be called when discovery has completed.</param> - this.discover = function(onCompleted) { + /// <param name="onDiscoverSuccess">A function(DiscoveryResult) callback to be called when discovery has completed successfully.</param> + /// <param name="onDiscoverFailure">A function callback to be called when discovery has completed in failure.</param> + this.discover = function(onDiscoverSuccess, onDiscoverFailure) { /// <summary>Instantiates an object that stores discovery results of some identifier.</summary> function DiscoveryResult(identifier, discoveryInfo) { + var thisDiscoveryResult = this; + /// <summary> /// Instantiates an object that describes an OpenID service endpoint and facilitates /// initiating and tracking an authentication request. @@ -25,68 +119,136 @@ window.OpenId = function(identifier) { this.immediate = requestInfo.immediate ? new window.dnoa_internal.Uri(requestInfo.immediate) : null; this.setup = requestInfo.setup ? new window.dnoa_internal.Uri(requestInfo.setup) : null; this.endpoint = new window.dnoa_internal.Uri(requestInfo.endpoint); + this.host = this.endpoint.getHost(); this.userSuppliedIdentifier = userSuppliedIdentifier; - var self = this; // closure so that delegates have the right instance - this.loginPopup = function(onSuccess, onFailure) { - //self.abort(); // ensure no concurrent attempts - window.dnoa_internal.processAuthorizationResult = function(childLocation) { - window.dnoa_internal.processAuthorizationResult = null; - trace('Received event from child window: ' + childLocation); - var success = true; // TODO: discern between success and failure, and fire the correct event. - - if (success) { - if (onSuccess) { - onSuccess(); - } - } else { - if (onFailure) { - onFailure(); - } - } - }; + var thisServiceEndpoint = this; // closure so that delegates have the right instance + this.loginPopup = function(onAuthSuccess, onAuthFailed) { + thisServiceEndpoint.abort(); // ensure no concurrent attempts + thisDiscoveryResult.onAuthSuccess = onAuthSuccess; + thisDiscoveryResult.onAuthFailed = onAuthFailed; var width = 800; var height = 600; - if (self.setup.getQueryArgValue("openid.return_to").indexOf("dotnetopenid.popupUISupported") >= 0) { + if (thisServiceEndpoint.setup.getQueryArgValue("openid.return_to").indexOf("dnoa.popupUISupported") >= 0) { + trace('This OP supports the UI extension. Using smaller window size.'); width = 450; height = 500; + } else { + trace("This OP doesn't appear to support the UI extension. Using larger window size."); } var left = (screen.width - width) / 2; var top = (screen.height - height) / 2; - self.popup = window.open(self.setup, 'opLogin', 'status=0,toolbar=0,location=1,resizable=1,scrollbars=1,left=' + left + ',top=' + top + ',width=' + width + ',height=' + height); + thisServiceEndpoint.popup = window.open(thisServiceEndpoint.setup, 'opLogin', 'status=0,toolbar=0,location=1,resizable=1,scrollbars=1,left=' + left + ',top=' + top + ',width=' + width + ',height=' + height); // If the OP supports the UI extension it MAY close its own window // for a negative assertion. We must be able to recover from that scenario. - var localSelf = self; - self.popupCloseChecker = window.setInterval(function() { - if (localSelf.popup && localSelf.popup.closed) { + var thisServiceEndpointLocal = thisServiceEndpoint; + thisServiceEndpoint.popupCloseChecker = window.setInterval(function() { + if (thisServiceEndpointLocal.popup && thisServiceEndpointLocal.popup.closed) { // The window closed, either because the user closed it, canceled at the OP, // or approved at the OP and the popup window closed itself due to our script. // If we were graying out the entire page while the child window was up, // we would probably revert that here. - window.clearInterval(localSelf.popupCloseChecker); - localSelf.popup = null; + window.clearInterval(thisServiceEndpointLocal.popupCloseChecker); + thisServiceEndpointLocal.popup = null; // The popup may have managed to inform us of the result already, // so check whether the callback method was cleared already, which // would indicate we've already processed this. if (window.dnoa_internal.processAuthorizationResult) { trace('User or OP canceled by closing the window.'); - if (onFailure) { - onFailure(); + if (thisDiscoveryResult.onAuthFailed) { + thisDiscoveryResult.onAuthFailed(thisDiscoveryResult, thisServiceEndpoint); } window.dnoa_internal.processAuthorizationResult = null; } } }, 250); }; + + this.loginBackgroundJob = function(iframe, timeout) { + thisServiceEndpoint.abort(); // ensure no concurrent attempts + if (timeout) { + thisServiceEndpoint.timeout = setTimeout(function() { thisServiceEndpoint.onAuthenticationTimedOut(); }, timeout); + } + trace('iframe hosting ' + thisServiceEndpoint.endpoint + ' now OPENING (timeout ' + timeout + ').'); + //trace('initiating auth attempt with: ' + thisServiceEndpoint.immediate); + thisServiceEndpoint.iframe = iframe; + return thisServiceEndpoint.immediate.toString(); + }; + + this.busy = function() { + return thisServiceEndpoint.iframe != null || thisServiceEndpoint.popup != null; + }; + + this.completeAttempt = function(successful) { + if (!thisServiceEndpoint.busy()) return false; + window.clearInterval(thisServiceEndpoint.timeout); + if (thisServiceEndpoint.iframe) { + trace('iframe hosting ' + thisServiceEndpoint.endpoint + ' now CLOSING.'); + thisDiscoveryResult.frameManager.closeFrame(thisServiceEndpoint.iframe); + thisServiceEndpoint.iframe = null; + } + if (thisServiceEndpoint.popup) { + thisServiceEndpoint.popup.close(); + thisServiceEndpoint.popup = null; + } + if (thisServiceEndpoint.timeout) { + window.clearTimeout(thisServiceEndpoint.timeout); + thisServiceEndpoint.timeout = null; + } + + if (!successful && !thisDiscoveryResult.busy() && thisDiscoveryResult.findSuccessfulRequest() == null) { + if (thisDiscoveryResult.onLastAttemptFailed) { + thisDiscoveryResult.onLastAttemptFailed(); + } + } + + return true; + }; + + this.onAuthenticationTimedOut = function() { + if (thisServiceEndpoint.completeAttempt()) { + trace(thisServiceEndpoint.host + " timed out"); + thisServiceEndpoint.result = window.dnoa_internal.timedOut; + } + }; + + this.onAuthSuccess = function(authUri) { + if (thisServiceEndpoint.completeAttempt(true)) { + trace(thisServiceEndpoint.host + " authenticated!"); + thisServiceEndpoint.result = window.dnoa_internal.authSuccess; + thisServiceEndpoint.response = authUri; + thisDiscoveryResult.abortAll(); + if (thisDiscoveryResult.onAuthSuccess) { + thisDiscoveryResult.onAuthSuccess(thisDiscoveryResult, thisServiceEndpoint); + } + } + }; + + this.onAuthFailed = function() { + if (thisServiceEndpoint.completeAttempt()) { + trace(thisServiceEndpoint.host + " failed authentication"); + thisServiceEndpoint.result = window.dnoa_internal.authRefused; + if (thisDiscoveryResult.onAuthFailed) { + thisDiscoveryResult.onAuthFailed(thisDiscoveryResult, thisServiceEndpoint); + } + } + }; + + this.abort = function() { + if (thisServiceEndpoint.completeAttempt()) { + trace(thisServiceEndpoint.host + " aborted"); + // leave the result as whatever it was before. + } + }; + }; this.userSuppliedIdentifier = identifier; - this.claimedIdentifier = discoveryInfo.claimedIdentifier; // The claimed identifier may be null if the user provided an OP Identifier. - trace('Discovered claimed identifier: ' + (this.claimedIdentifier ? this.claimedIdentifier : "(directed identity)")); if (discoveryInfo) { + this.claimedIdentifier = discoveryInfo.claimedIdentifier; // The claimed identifier may be null if the user provided an OP Identifier. this.length = discoveryInfo.requests.length; for (var i = 0; i < discoveryInfo.requests.length; i++) { this[i] = new ServiceEndpoint(discoveryInfo.requests[i], identifier); @@ -94,10 +256,75 @@ window.OpenId = function(identifier) { } else { this.length = 0; } + + trace('Discovered claimed identifier: ' + (this.claimedIdentifier ? this.claimedIdentifier : "(directed identity)")); + + // Add extra tracking bits and behaviors. + this.findByEndpoint = function(opEndpoint) { + for (var i = 0; i < thisDiscoveryResult.length; i++) { + if (thisDiscoveryResult[i].endpoint == opEndpoint) { + return thisDiscoveryResult[i]; + } + } + }; + + this.busy = function() { + for (var i = 0; i < thisDiscoveryResult.length; i++) { + if (thisDiscoveryResult[i].busy()) { + return true; + } + } + }; + + // Add extra tracking bits and behaviors. + this.findSuccessfulRequest = function() { + for (var i = 0; i < thisDiscoveryResult.length; i++) { + if (thisDiscoveryResult[i].result === window.dnoa_internal.authSuccess) { + return thisDiscoveryResult[i]; + } + } + }; + + this.abortAll = function() { + if (thisDiscoveryResult.frameManager) { + // Abort all other asynchronous authentication attempts that may be in progress. + thisDiscoveryResult.frameManager.cancelAllWork(); + for (var i = 0; i < thisDiscoveryResult.length; i++) { + thisDiscoveryResult[i].abort(); + } + } else { + trace('abortAll called without a frameManager being previously set.'); + } + }; + + /// <summary>Initiates an asynchronous checkid_immediate login attempt against all possible service endpoints for an Identifier.</summary> + /// <param name="frameManager">The work queue for authentication iframes.</param> + /// <param name="onAuthSuccess">Fired when an endpoint responds affirmatively.</param> + /// <param name="onAuthFailed">Fired when an endpoint responds negatively.</param> + /// <param name="onLastAuthFailed">Fired when all authentication attempts have responded negatively or timed out.</param> + /// <param name="timeout">Timeout for an individual service endpoint to respond before the iframe closes.</param> + this.loginBackground = function(frameManager, onAuthSuccess, onAuthFailed, onLastAuthFailed, timeout) { + if (!frameManager) { + throw "No frameManager specified."; + } + if (thisDiscoveryResult.findSuccessfulRequest() != null) { + onAuthSuccess(thisDiscoveryResult, thisDiscoveryResult.findSuccessfulRequest()); + } else { + thisDiscoveryResult.frameManager = frameManager; + thisDiscoveryResult.onAuthSuccess = onAuthSuccess; + thisDiscoveryResult.onAuthFailed = onAuthFailed; + thisDiscoveryResult.onLastAttemptFailed = onLastAuthFailed; + if (thisDiscoveryResult.length > 0) { + for (var i = 0; i < thisDiscoveryResult.length; i++) { + thisDiscoveryResult.frameManager.enqueueWork(thisDiscoveryResult[i].loginBackgroundJob, timeout); + } + } + } + }; }; /// <summary>Receives the results of a successful discovery (even if it yielded 0 results).</summary> - function successCallback(discoveryResult, identifier) { + function discoverSuccessCallback(discoveryResult, identifier) { trace('Discovery completed for: ' + identifier); // Deserialize the JSON object and store the result if it was a successful discovery. @@ -107,40 +334,60 @@ window.OpenId = function(identifier) { discoveryResult = new DiscoveryResult(identifier, discoveryResult); window.dnoa_internal.discoveryResults[identifier] = discoveryResult; - if (onCompleted) { - onCompleted(discoveryResult); + if (onDiscoverSuccess) { + onDiscoverSuccess(discoveryResult); } }; /// <summary>Receives the discovery failure notification.</summary> - failureCallback = function(message, userSuppliedIdentifier) { + function discoverFailureCallback(message, userSuppliedIdentifier) { trace('Discovery failed for: ' + identifier); - if (onCompleted) { - onCompleted(); + if (onDiscoverFailure) { + onDiscoverFailure(); } }; if (window.dnoa_internal.discoveryResults[identifier]) { trace("We've already discovered " + identifier + " so we're skipping it this time."); - onCompleted(window.dnoa_internal.discoveryResults[identifier]); + if (onDiscoverSuccess) { + onDiscoverSuccess(window.dnoa_internal.discoveryResults[identifier]); + } + return; } trace('starting discovery on ' + identifier); - window.dnoa_internal.callbackAsync(identifier, successCallback, failureCallback); + window.dnoa_internal.callbackAsync(identifier, discoverSuccessCallback, discoverFailureCallback); }; /// <summary>Performs discovery and immediately begins checkid_setup to authenticate the user using a given identifier.</summary> - this.login = function(onSuccess, onFailure) { + this.login = function(onSuccess, onLoginFailure) { + this.discover(function(discoveryResult) { + if (discoveryResult) { + trace('Discovery succeeded and found ' + discoveryResult.length + ' OpenID service endpoints.'); + if (discoveryResult.length > 0) { + discoveryResult[0].loginPopup(onSuccess, onLoginFailure); + } else { + trace("This doesn't look like an OpenID Identifier. Aborting login."); + if (onLoginFailure) { + onLoginFailure(); + } + } + } + }); + }; + + /// <summary>Performs discovery and immediately begins checkid_immediate on all discovered endpoints.</summary> + this.loginBackground = function(frameManager, onLoginSuccess, onLoginFailure, timeout) { this.discover(function(discoveryResult) { if (discoveryResult) { trace('Discovery succeeded and found ' + discoveryResult.length + ' OpenID service endpoints.'); if (discoveryResult.length > 0) { - discoveryResult[0].loginPopup(onSuccess, onFailure); + discoveryResult.loginBackground(frameManager, onLoginSuccess, onLoginFailure, onLoginFailure, timeout); } else { trace("This doesn't look like an OpenID Identifier. Aborting login."); - if (onFailure) { - onFailure(); + if (onLoginFailure) { + onLoginFailure(); } } } @@ -148,3 +395,51 @@ window.OpenId = function(identifier) { }; }; +/// <summary>Invoked by RP web server when an authentication has completed.</summary> +/// <remarks>The duty of this method is to distribute the notification to the appropriate tracking object.</remarks> +window.dnoa_internal.processAuthorizationResult = function(resultUrl) { + //trace('processAuthorizationResult ' + resultUrl); + var resultUri = new window.dnoa_internal.Uri(resultUrl); + + // Find the tracking object responsible for this request. + var userSuppliedIdentifier = resultUri.getQueryArgValue('dnoa.userSuppliedIdentifier'); + if (!userSuppliedIdentifier) { + trace('processAuthorizationResult called but no userSuppliedIdentifier parameter was found. Exiting function.'); + return; + } + var discoveryResult = window.dnoa_internal.discoveryResults[userSuppliedIdentifier]; + if (discoveryResult == null) { + trace('processAuthorizationResult called but no discovery result matching user supplied identifier ' + userSuppliedIdentifier + ' was found. Exiting function.'); + return; + } + + var opEndpoint = resultUri.getQueryArgValue("openid.op_endpoint") ? resultUri.getQueryArgValue("openid.op_endpoint") : resultUri.getQueryArgValue("dnoa.op_endpoint"); + var respondingEndpoint = discoveryResult.findByEndpoint(opEndpoint); + trace('Auth result for ' + respondingEndpoint.host + ' received.');//: ' + resultUrl); + + if (window.dnoa_internal.isAuthSuccessful(resultUri)) { + discoveryResult.successAuthData = resultUrl; + respondingEndpoint.onAuthSuccess(resultUri); + + var claimed_id = resultUri.getQueryArgValue("openid.claimed_id"); + if (claimed_id && claimed_id != discoveryResult.claimedIdentifier) { + discoveryResult.claimedIdentifier = resultUri.getQueryArgValue("openid.claimed_id"); + trace('Authenticated as ' + claimed_id); + } + } else { + respondingEndpoint.onAuthFailed(); + } +}; + +window.dnoa_internal.isAuthSuccessful = function(resultUri) { + if (window.dnoa_internal.isOpenID2Response(resultUri)) { + return resultUri.getQueryArgValue("openid.mode") == "id_res"; + } else { + return resultUri.getQueryArgValue("openid.mode") == "id_res" && !resultUri.containsQueryArg("openid.user_setup_url"); + } +}; + +window.dnoa_internal.isOpenID2Response = function(resultUri) { + return resultUri.containsQueryArg("openid.ns"); +}; + diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingPartyControlBase.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingPartyControlBase.cs index 30e7f8b..45390dd 100644 --- a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingPartyControlBase.cs +++ b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingPartyControlBase.cs @@ -65,6 +65,31 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { #endregion + #region Callback parameter names + + /// <summary> + /// The callback parameter to use for recognizing when the callback is in a popup window or hidden iframe. + /// </summary> + protected 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 const string PopupUISupportedJsHint = OpenIdUtilities.CustomParameterPrefix + "popupUISupported"; + + /// <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> @@ -148,31 +173,6 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { #endregion - #region 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 a popup window. - /// </summary> - private const string UIPopupCallbackKey = OpenIdUtilities.CustomParameterPrefix + "uipopup"; - - /// <summary> - /// The callback parameter to use for recognizing when the callback is in the parent window. - /// </summary> - private const string UIPopupCallbackParentKey = OpenIdUtilities.CustomParameterPrefix + "uipopupParent"; - - /// <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> - private const string PopupUISupportedJsHint = "dotnetopenid.popupUISupported"; - - #endregion - /// <summary> /// The lifetime of the cookie used to persist the Identifier the user logged in with. /// </summary> @@ -198,11 +198,10 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { #region Events /// <summary> - /// Fired after the user clicks the log in button, but before the authentication - /// process begins. Offers a chance for the web application to disallow based on - /// OpenID URL before redirecting the user to the OpenID Provider. + /// Fired when the user has typed in their identifier, discovery was successful + /// and a login attempt is about to begin. /// </summary> - [Description("Fired after the user clicks the log in button, but before the authentication process begins. Offers a chance for the web application to disallow based on OpenID URL before redirecting the user to the OpenID Provider."), Category(OpenIdCategory)] + [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> @@ -391,7 +390,7 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { /// </summary> [Bindable(true), DefaultValue(LoginModeDefault), Category(BehaviorCategory)] [Description("The way a completed login is communicated to the rest of the web site.")] - public LoginSiteNotification LoginMode { + public virtual LoginSiteNotification LoginMode { get { return (LoginSiteNotification)(this.ViewState[LoginModeViewStateKey] ?? LoginModeDefault); } set { this.ViewState[LoginModeViewStateKey] = value; } } @@ -532,25 +531,25 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { // Configure each generated request. foreach (var req in requests) { if (this.IsPopupAppropriate(req)) { - // Inform the OP that we'll be using a popup window. - req.AddExtension(new UIRequest()); - // Inform ourselves in return_to that we're in a popup. - req.AddCallbackArguments(UIPopupCallbackKey, "1"); + req.SetCallbackArgument(UIPopupCallbackKey, "1"); if (req.Provider.IsExtensionSupported<UIRequest>()) { + // Inform the OP that we'll be using a popup window consistent with the UI extension. + 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.AddCallbackArguments(PopupUISupportedJsHint, "1"); + req.SetCallbackArgument(PopupUISupportedJsHint, "1"); } } // Add state that needs to survive across the redirect. if (!this.Stateless) { - req.AddCallbackArguments(UsePersistentCookieCallbackKey, this.UsePersistentCookie.ToString()); - req.AddCallbackArguments(ReturnToReceivingControlId, this.ClientID); + req.SetCallbackArgument(UsePersistentCookieCallbackKey, this.UsePersistentCookie.ToString()); + req.SetCallbackArgument(ReturnToReceivingControlId, this.ClientID); } ((AuthenticationRequest)req).AssociationPreference = this.AssociationPreference; @@ -581,7 +580,7 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { 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.ScriptClosingPopup(); + this.ScriptClosingPopupOrIFrame(); return; // don't do any more processing on it now } @@ -590,32 +589,41 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { string receiver = this.Page.Request.QueryString[ReturnToReceivingControlId] ?? this.Page.Request.Form[ReturnToReceivingControlId]; if (receiver == null || receiver == this.ClientID) { var response = this.RelyingParty.GetResponse(); - if (response != null) { - string persistentString = response.GetCallbackArgument(UsePersistentCookieCallbackKey); - if (persistentString != null) { - this.UsePersistentCookie = (LoginPersistence)Enum.Parse(typeof(LoginPersistence), persistentString); - } + this.ProcessResponse(response); + } + } - 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> + /// Processes the response. + /// </summary> + /// <param name="response">The response.</param> + protected virtual void ProcessResponse(IAuthenticationResponse response) { + if (response == null) { + return; + } + string persistentString = response.GetCallbackArgument(UsePersistentCookieCallbackKey); + if (persistentString != null) { + this.UsePersistentCookie = (LoginPersistence)Enum.Parse(typeof(LoginPersistence), 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); } } @@ -719,8 +727,20 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { /// </summary> /// <returns>The instantiated relying party.</returns> protected virtual OpenIdRelyingParty CreateRelyingParty() { + return this.CreateRelyingParty(true); + } + + /// <summary> + /// Creates the relying party instance used to generate authentication requests. + /// </summary> + /// <param name="verifySignature"> + /// A value indicating whether message protections should be applied to the processed messages. + /// Use <c>false</c> to postpone verification to a later time without invalidating nonces. + /// </param> + /// <returns>The instantiated relying party.</returns> + protected virtual OpenIdRelyingParty CreateRelyingParty(bool verifySignature) { IRelyingPartyApplicationStore store = this.Stateless ? null : DotNetOpenAuthSection.Configuration.OpenId.RelyingParty.ApplicationStore.CreateInstance(OpenIdRelyingParty.HttpApplicationStore); - var rp = new OpenIdRelyingParty(store); + var rp = verifySignature ? new OpenIdRelyingParty(store) : OpenIdRelyingParty.CreateNonVerifying(); // Only set RequireSsl to true, as we don't want to override // a .config setting of true with false. @@ -781,6 +801,21 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { } /// <summary> + /// Wires the popup window to close itself and pass the authentication result to the parent window. + /// </summary> + protected virtual void ScriptClosingPopupOrIFrame() { + StringBuilder startupScript = new StringBuilder(); + startupScript.AppendLine("window.opener.dnoa_internal.processAuthorizationResult(document.URL);"); + startupScript.AppendLine("window.close();"); + + this.Page.ClientScript.RegisterStartupScript(typeof(OpenIdRelyingPartyControlBase), "loginPopupClose", startupScript.ToString(), true); + + // TODO: alternately we should probably take over rendering this page here to avoid + // a lot of unnecessary work on the server and possible momentary display of the + // page in the popup window. + } + + /// <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> @@ -853,21 +888,6 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { } /// <summary> - /// Wires the popup window to close itself and pass the authentication result to the parent window. - /// </summary> - private void ScriptClosingPopup() { - StringBuilder startupScript = new StringBuilder(); - startupScript.AppendLine("window.opener.dnoa_internal.processAuthorizationResult(document.URL + '&" + UIPopupCallbackParentKey + "=1');"); - startupScript.AppendLine("window.close();"); - - this.Page.ClientScript.RegisterStartupScript(typeof(OpenIdRelyingPartyControlBase), "loginPopupClose", startupScript.ToString(), true); - - // TODO: alternately we should probably take over rendering this page here to avoid - // a lot of unnecessary work on the server and possible momentary display of the - // page in the popup window. - } - - /// <summary> /// Tries to preset the <see cref="Identifier"/> property based on a persistent /// cookie on the browser. /// </summary> diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingPartyControlBase.js b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingPartyControlBase.js index 3a17b7b..4496445 100644 --- a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingPartyControlBase.js +++ b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingPartyControlBase.js @@ -25,11 +25,6 @@ if (window.dnoa_internal === undefined) { window.dnoa_internal = new Object(); }; -// The possible authentication results -window.dnoa_internal.authSuccess = new Object(); -window.dnoa_internal.authRefused = new Object(); -window.dnoa_internal.timedOut = new Object(); - /// <summary>Instantiates an object that provides string manipulation services for URIs.</summary> window.dnoa_internal.Uri = function(url) { this.originalUri = url.toString(); diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdTextBox.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdTextBox.cs index 48e51dd..2dca459 100644 --- a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdTextBox.cs +++ b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdTextBox.cs @@ -277,7 +277,7 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { [Bindable(true), DefaultValue(""), Category(AppearanceCategory)] [Description("The content of the text box.")] public string Text { - get { return this.Identifier; } + get { return this.Identifier.OriginalString; } set { this.Identifier = value; } } diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/AuthenticationResponseSnapshot.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/PositiveAuthenticationResponseSnapshot.cs index 3fd7d20..04a403c 100644 --- a/src/DotNetOpenAuth/OpenId/RelyingParty/AuthenticationResponseSnapshot.cs +++ b/src/DotNetOpenAuth/OpenId/RelyingParty/PositiveAuthenticationResponseSnapshot.cs @@ -1,5 +1,5 @@ //----------------------------------------------------------------------- -// <copyright file="AuthenticationResponseSnapshot.cs" company="Andrew Arnott"> +// <copyright file="PositiveAuthenticationResponseSnapshot.cs" company="Andrew Arnott"> // Copyright (c) Andrew Arnott. All rights reserved. // </copyright> //----------------------------------------------------------------------- @@ -16,17 +16,17 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { /// A serializable snapshot of a verified authentication message. /// </summary> [Serializable] - internal class AuthenticationResponseSnapshot : IAuthenticationResponse { + internal class PositiveAuthenticationResponseSnapshot : IAuthenticationResponse { /// <summary> /// The callback arguments that came with the authentication response. /// </summary> private IDictionary<string, string> callbackArguments; /// <summary> - /// Initializes a new instance of the <see cref="AuthenticationResponseSnapshot"/> class. + /// Initializes a new instance of the <see cref="PositiveAuthenticationResponseSnapshot"/> class. /// </summary> /// <param name="copyFrom">The authentication response to copy from.</param> - internal AuthenticationResponseSnapshot(IAuthenticationResponse copyFrom) { + internal PositiveAuthenticationResponseSnapshot(IAuthenticationResponse copyFrom) { ErrorUtilities.VerifyArgumentNotNull(copyFrom, "copyFrom"); this.ClaimedIdentifier = copyFrom.ClaimedIdentifier; |