diff options
author | Andrew Arnott <andrewarnott@gmail.com> | 2009-08-25 16:44:13 -0700 |
---|---|---|
committer | Andrew Arnott <andrewarnott@gmail.com> | 2009-08-25 16:44:13 -0700 |
commit | f480b463c326a60603ab79d7e30da45b5e866d78 (patch) | |
tree | 98303f69cfe726bf14b0c57c1d64867a6f5470f1 | |
parent | c6aaeeb97c42cbda35497bbfb2bfeb6c9114e693 (diff) | |
parent | eec666a9918cb449dbe438c64aed393cb4c73136 (diff) | |
download | DotNetOpenAuth-f480b463c326a60603ab79d7e30da45b5e866d78.zip DotNetOpenAuth-f480b463c326a60603ab79d7e30da45b5e866d78.tar.gz DotNetOpenAuth-f480b463c326a60603ab79d7e30da45b5e866d78.tar.bz2 |
Merge branch 'rpControlBase'
30 files changed, 1685 insertions, 2369 deletions
diff --git a/samples/OAuthConsumer/App_Code/InMemoryTokenManager.cs b/samples/OAuthConsumer/App_Code/InMemoryTokenManager.cs index fede300..2ecd045 100644 --- a/samples/OAuthConsumer/App_Code/InMemoryTokenManager.cs +++ b/samples/OAuthConsumer/App_Code/InMemoryTokenManager.cs @@ -28,14 +28,6 @@ public class InMemoryTokenManager : IConsumerTokenManager { #region ITokenManager Members - public string GetConsumerSecret(string consumerKey) { - if (consumerKey == this.ConsumerKey) { - return this.ConsumerSecret; - } else { - throw new ArgumentException("Unrecognized consumer key.", "consumerKey"); - } - } - public string GetTokenSecret(string token) { return this.tokensAndSecrets[token]; } diff --git a/samples/OpenIdRelyingPartyWebForms/login.aspx b/samples/OpenIdRelyingPartyWebForms/login.aspx index 281725c..bca0676 100644 --- a/samples/OpenIdRelyingPartyWebForms/login.aspx +++ b/samples/OpenIdRelyingPartyWebForms/login.aspx @@ -7,8 +7,7 @@ <rp:OpenIdLogin ID="OpenIdLogin1" runat="server" CssClass="openid_login" RequestCountry="Request" RequestEmail="Require" RequestGender="Require" RequestPostalCode="Require" RequestTimeZone="Require" RememberMeVisible="True" PolicyUrl="~/PrivacyPolicy.aspx" TabIndex="1" - OnLoggedIn="OpenIdLogin1_LoggedIn" OnLoggingIn="OpenIdLogin1_LoggingIn" - OnSetupRequired="OpenIdLogin1_SetupRequired" /> + OnLoggedIn="OpenIdLogin1_LoggedIn" OnLoggingIn="OpenIdLogin1_LoggingIn" /> <fieldset title="Knobs"> <asp:CheckBox ID="requireSslCheckBox" runat="server" Text="RequireSsl (high security) mode" @@ -22,9 +21,6 @@ </asp:CheckBoxList> <p>Try the PPID identifier functionality against the OpenIDProviderMvc sample.</p> </fieldset> - <br /> - <asp:Label ID="setupRequiredLabel" runat="server" EnableViewState="False" Text="You must log into your Provider first to use Immediate mode." - Visible="False" /> <p> <rp:OpenIdButton runat="server" ImageUrl="~/images/yahoo.png" Text="Login with Yahoo!" ID="yahooLoginButton" Identifier="https://me.yahoo.com/" /> diff --git a/samples/OpenIdRelyingPartyWebForms/login.aspx.cs b/samples/OpenIdRelyingPartyWebForms/login.aspx.cs index 1de942a..6721e9b 100644 --- a/samples/OpenIdRelyingPartyWebForms/login.aspx.cs +++ b/samples/OpenIdRelyingPartyWebForms/login.aspx.cs @@ -35,10 +35,6 @@ namespace OpenIdRelyingPartyWebForms { State.PapePolicies = e.Response.GetExtension<PolicyResponse>(); } - protected void OpenIdLogin1_SetupRequired(object sender, OpenIdEventArgs e) { - this.setupRequiredLabel.Visible = true; - } - private void prepareRequest(IAuthenticationRequest request) { // Collect the PAPE policies requested by the user. List<string> policies = new List<string>(); diff --git a/samples/OpenIdRelyingPartyWebForms/login.aspx.designer.cs b/samples/OpenIdRelyingPartyWebForms/login.aspx.designer.cs index 944f5ff..4a2521c 100644 --- a/samples/OpenIdRelyingPartyWebForms/login.aspx.designer.cs +++ b/samples/OpenIdRelyingPartyWebForms/login.aspx.designer.cs @@ -41,15 +41,6 @@ namespace OpenIdRelyingPartyWebForms { protected global::System.Web.UI.WebControls.CheckBoxList papePolicies; /// <summary> - /// setupRequiredLabel control. - /// </summary> - /// <remarks> - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// </remarks> - protected global::System.Web.UI.WebControls.Label setupRequiredLabel; - - /// <summary> /// yahooLoginButton control. /// </summary> /// <remarks> diff --git a/samples/OpenIdRelyingPartyWebForms/logout.aspx b/samples/OpenIdRelyingPartyWebForms/logout.aspx index 71c0433..156f800 100644 --- a/samples/OpenIdRelyingPartyWebForms/logout.aspx +++ b/samples/OpenIdRelyingPartyWebForms/logout.aspx @@ -7,6 +7,7 @@ State.FriendlyLoginName = null; State.ProfileFields = null; System.Web.Security.FormsAuthentication.SignOut(); + DotNetOpenAuth.OpenId.RelyingParty.OpenIdRelyingPartyControlBase.LogOff(); Response.Redirect("~/"); } </script> 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.Test/OpenId/RelyingParty/PositiveAuthenticationResponseTests.cs b/src/DotNetOpenAuth.Test/OpenId/RelyingParty/PositiveAuthenticationResponseTests.cs index 083b988..701bcae 100644 --- a/src/DotNetOpenAuth.Test/OpenId/RelyingParty/PositiveAuthenticationResponseTests.cs +++ b/src/DotNetOpenAuth.Test/OpenId/RelyingParty/PositiveAuthenticationResponseTests.cs @@ -38,7 +38,7 @@ namespace DotNetOpenAuth.Test.OpenId.RelyingParty { Assert.AreEqual(AuthenticationStatus.Authenticated, authResponse.Status); Assert.IsNull(authResponse.Exception); Assert.AreEqual<string>(assertion.ClaimedIdentifier, authResponse.ClaimedIdentifier); - Assert.AreEqual<string>(authResponseAccessor.endpoint.FriendlyIdentifierForDisplay, authResponse.FriendlyIdentifierForDisplay); + Assert.AreEqual<string>(authResponse.Endpoint.FriendlyIdentifierForDisplay, authResponse.FriendlyIdentifierForDisplay); Assert.AreSame(extension, authResponse.GetUntrustedExtension(typeof(ClaimsResponse))); Assert.AreSame(extension, authResponse.GetUntrustedExtension<ClaimsResponse>()); Assert.IsNull(authResponse.GetCallbackArgument("a")); diff --git a/src/DotNetOpenAuth/DotNetOpenAuth.csproj b/src/DotNetOpenAuth/DotNetOpenAuth.csproj index aba573d..a36b592 100644 --- a/src/DotNetOpenAuth/DotNetOpenAuth.csproj +++ b/src/DotNetOpenAuth/DotNetOpenAuth.csproj @@ -153,9 +153,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> @@ -498,7 +504,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" /> @@ -547,6 +553,8 @@ <None Include="Messaging\Bindings\Bindings.cd" /> <None Include="Messaging\Exceptions.cd" /> <None Include="Messaging\Messaging.cd" /> + <None Include="OpenId\RelyingParty\Controls.cd" /> + <None Include="OpenId\RelyingParty\OpenIdRelyingParty.cd" /> </ItemGroup> <ItemGroup> <EmbeddedResource Include="Messaging\MessagingStrings.resx"> diff --git a/src/DotNetOpenAuth/OpenId/Identifier.cs b/src/DotNetOpenAuth/OpenId/Identifier.cs index 1b9570e..435dc00 100644 --- a/src/DotNetOpenAuth/OpenId/Identifier.cs +++ b/src/DotNetOpenAuth/OpenId/Identifier.cs @@ -24,15 +24,20 @@ 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> - protected Identifier(bool isDiscoverySecureEndToEnd) { + /// <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 the original string that was normalized to create this Identifier. + /// </summary> + public string OriginalString { get; private set; } + + /// <summary> /// Gets a value indicating whether this Identifier will ensure SSL is /// used throughout the discovery phase and initial redirect of authentication. /// </summary> diff --git a/src/DotNetOpenAuth/OpenId/IdentifierContract.cs b/src/DotNetOpenAuth/OpenId/IdentifierContract.cs index 1758f06..9ab56c5 100644 --- a/src/DotNetOpenAuth/OpenId/IdentifierContract.cs +++ b/src/DotNetOpenAuth/OpenId/IdentifierContract.cs @@ -20,7 +20,7 @@ namespace DotNetOpenAuth.OpenId { /// Prevents a default instance of the IdentifierContract class from being created. /// </summary> private IdentifierContract() - : base(false) { + : base(null, false) { } /// <summary> diff --git a/src/DotNetOpenAuth/OpenId/NoDiscoveryIdentifier.cs b/src/DotNetOpenAuth/OpenId/NoDiscoveryIdentifier.cs index 2c8e865..72f74c8 100644 --- a/src/DotNetOpenAuth/OpenId/NoDiscoveryIdentifier.cs +++ b/src/DotNetOpenAuth/OpenId/NoDiscoveryIdentifier.cs @@ -28,7 +28,7 @@ namespace DotNetOpenAuth.OpenId { /// <param name="wrappedIdentifier">The ordinary Identifier whose discovery is being masked.</param> /// <param name="claimSsl">Whether this Identifier should claim to be SSL-secure, although no discovery will never generate service endpoints anyway.</param> internal NoDiscoveryIdentifier(Identifier wrappedIdentifier, bool claimSsl) - : base(claimSsl) { + : base(wrappedIdentifier.OriginalString, claimSsl) { Contract.Requires(wrappedIdentifier != null); ErrorUtilities.VerifyArgumentNotNull(wrappedIdentifier, "wrappedIdentifier"); diff --git a/src/DotNetOpenAuth/OpenId/OpenIdXrdsHelper.cs b/src/DotNetOpenAuth/OpenId/OpenIdXrdsHelper.cs index 2433df2..c3506fc 100644 --- a/src/DotNetOpenAuth/OpenId/OpenIdXrdsHelper.cs +++ b/src/DotNetOpenAuth/OpenId/OpenIdXrdsHelper.cs @@ -43,7 +43,7 @@ namespace DotNetOpenAuth.OpenId { /// </returns> internal static IEnumerable<ServiceEndpoint> CreateServiceEndpoints(this XrdsDocument xrds, UriIdentifier claimedIdentifier, UriIdentifier userSuppliedIdentifier) { var endpoints = new List<ServiceEndpoint>(); - endpoints.AddRange(xrds.GenerateOPIdentifierServiceEndpoints(claimedIdentifier)); + endpoints.AddRange(xrds.GenerateOPIdentifierServiceEndpoints(userSuppliedIdentifier)); // If any OP Identifier service elements were found, we must not proceed // to return any Claimed Identifier services. @@ -83,7 +83,7 @@ namespace DotNetOpenAuth.OpenId { /// Generates OpenID Providers that can authenticate using directed identity. /// </summary> /// <param name="xrds">The XrdsDocument instance to use in this process.</param> - /// <param name="opIdentifier">The OP Identifier entered (and resolved) by the user.</param> + /// <param name="opIdentifier">The OP Identifier entered (and resolved) by the user. Essentially the user-supplied identifier.</param> /// <returns>A sequence of the providers that can offer directed identity services.</returns> private static IEnumerable<ServiceEndpoint> GenerateOPIdentifierServiceEndpoints(this XrdsDocument xrds, Identifier opIdentifier) { Contract.Requires(xrds != null); diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/AuthenticationRequest.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/AuthenticationRequest.cs index 060ef21..ff7741a 100644 --- a/src/DotNetOpenAuth/OpenId/RelyingParty/AuthenticationRequest.cs +++ b/src/DotNetOpenAuth/OpenId/RelyingParty/AuthenticationRequest.cs @@ -232,6 +232,28 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { } /// <summary> + /// Makes a key/value pair available when the authentication is completed. + /// </summary> + /// <param name="key">The parameter name.</param> + /// <param name="value">The value of the argument. Must not be null.</param> + /// <remarks> + /// <para>Note that these values are NOT protected against tampering in transit. No + /// security-sensitive data should be stored using this method.</para> + /// <para>The value stored here can be retrieved using + /// <see cref="IAuthenticationResponse.GetCallbackArgument"/>.</para> + /// <para>Since the data set here is sent in the querystring of the request and some + /// servers place limits on the size of a request URL, this data should be kept relatively + /// small to ensure successful authentication. About 1.5KB is about all that should be stored.</para> + /// </remarks> + public void SetCallbackArgument(string key, string value) { + ErrorUtilities.VerifyNonZeroLength(key, "key"); + ErrorUtilities.VerifyArgumentNotNull(value, "value"); + ErrorUtilities.VerifyOperation(this.RelyingParty.CanSignCallbackArguments, OpenIdStrings.CallbackArgumentsRequireSecretStore, typeof(IAssociationStore<Uri>).Name, typeof(OpenIdRelyingParty).Name); + + this.returnToArgs[key] = value; + } + + /// <summary> /// Adds an OpenID extension to the request directed at the OpenID provider. /// </summary> /// <param name="extension">The initialized extension to add to the request.</param> @@ -478,7 +500,7 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { request.AssociationHandle = association != null ? association.Handle : null; request.AddReturnToArguments(this.returnToArgs); if (this.endpoint.UserSuppliedIdentifier != null) { - request.AddReturnToArguments(UserSuppliedIdentifierParameterName, this.endpoint.UserSuppliedIdentifier); + request.AddReturnToArguments(UserSuppliedIdentifierParameterName, this.endpoint.UserSuppliedIdentifier.OriginalString); } foreach (IOpenIdMessageExtension extension in this.extensions) { request.Extensions.Add(extension); diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/Controls.cd b/src/DotNetOpenAuth/OpenId/RelyingParty/Controls.cd new file mode 100644 index 0000000..f96db36 --- /dev/null +++ b/src/DotNetOpenAuth/OpenId/RelyingParty/Controls.cd @@ -0,0 +1,112 @@ +<?xml version="1.0" encoding="utf-8"?> +<ClassDiagram MajorVersion="1" MinorVersion="1"> + <Class Name="DotNetOpenAuth.OpenId.RelyingParty.OpenIdRelyingPartyAjaxControlBase"> + <Position X="0.5" Y="9.75" Width="3" /> + <Compartments> + <Compartment Name="Fields" Collapsed="true" /> + </Compartments> + <TypeIdentifier> + <HashCode>BARAAAAAAAAAAACQAAAAAAAEAAAgAAAAAQAFAAAAAFk=</HashCode> + <FileName>OpenId\RelyingParty\OpenIdRelyingPartyAjaxControlBase.cs</FileName> + </TypeIdentifier> + <Lollipop Position="0.2" /> + </Class> + <Class Name="DotNetOpenAuth.OpenId.RelyingParty.OpenIdMobileTextBox"> + <Position X="8.5" Y="5.25" Width="2.5" /> + <Compartments> + <Compartment Name="Fields" Collapsed="true" /> + </Compartments> + <TypeIdentifier> + <HashCode>AI0JADgFQRQQDAIw4lAYSEIWCAMZhMVlELAASQIAgSI=</HashCode> + <FileName>OpenId\RelyingParty\OpenIdMobileTextBox.cs</FileName> + </TypeIdentifier> + </Class> + <Class Name="DotNetOpenAuth.OpenId.RelyingParty.OpenIdLogin"> + <Position X="6.25" Y="1.25" Width="1.75" /> + <Compartments> + <Compartment Name="Fields" Collapsed="true" /> + </Compartments> + <NestedTypes> + <Class Name="DotNetOpenAuth.OpenId.RelyingParty.OpenIdLogin.InPlaceControl" Collapsed="true"> + <TypeIdentifier> + <NewMemberFileName>OpenId\RelyingParty\OpenIdLogin.cs</NewMemberFileName> + </TypeIdentifier> + </Class> + </NestedTypes> + <TypeIdentifier> + <HashCode>gIMgADAIAQEQIJAYOQBSADiQBgiIECk0jQCggdAp4BQ=</HashCode> + <FileName>OpenId\RelyingParty\OpenIdLogin.cs</FileName> + </TypeIdentifier> + </Class> + <Class Name="DotNetOpenAuth.OpenId.RelyingParty.OpenIdAjaxTextBox"> + <Position X="3.75" Y="10" Width="2.25" /> + <Compartments> + <Compartment Name="Fields" Collapsed="true" /> + </Compartments> + <TypeIdentifier> + <HashCode>ACBEEbABZzOzAKCYJNOEwM3uSIR5AAOkUFANCQ7DsVs=</HashCode> + <FileName>OpenId\RelyingParty\OpenIdAjaxTextBox.cs</FileName> + </TypeIdentifier> + <Lollipop Position="0.2" /> + </Class> + <Class Name="DotNetOpenAuth.OpenId.RelyingParty.OpenIdButton"> + <Position X="8.75" Y="1" Width="1.75" /> + <Compartments> + <Compartment Name="Fields" Collapsed="true" /> + </Compartments> + <InheritanceLine Type="DotNetOpenAuth.OpenId.RelyingParty.OpenIdRelyingPartyControlBase" ManuallyRouted="true" FixedFromPoint="true" FixedToPoint="true"> + <Path> + <Point X="2.875" Y="0.5" /> + <Point X="7.194" Y="0.5" /> + <Point X="7.194" Y="1" /> + <Point X="8.75" Y="1" /> + </Path> + </InheritanceLine> + <TypeIdentifier> + <HashCode>BAAEQAAAAAAAAAACAAAgAAAAAIAAAACQABAECABAAAA=</HashCode> + <FileName>OpenId\RelyingParty\OpenIdButton.cs</FileName> + </TypeIdentifier> + <Lollipop Position="0.2" /> + </Class> + <Class Name="DotNetOpenAuth.OpenId.RelyingParty.OpenIdTextBox"> + <Position X="3.5" Y="1.25" Width="2.25" /> + <Compartments> + <Compartment Name="Fields" Collapsed="true" /> + </Compartments> + <TypeIdentifier> + <HashCode>AIEVQjgBIxYITIARcAAACEc2CIAIlER1CBAQSQoEpCg=</HashCode> + <FileName>OpenId\RelyingParty\OpenIdTextBox.cs</FileName> + </TypeIdentifier> + <Lollipop Position="0.2" /> + </Class> + <Class Name="DotNetOpenAuth.OpenId.RelyingParty.OpenIdRelyingPartyControlBase"> + <Position X="0.5" Y="0.5" Width="2.5" /> + <Compartments> + <Compartment Name="Fields" Collapsed="true" /> + </Compartments> + <NestedTypes> + <Enum Name="DotNetOpenAuth.OpenId.RelyingParty.OpenIdRelyingPartyControlBase.LoginSiteNotification" Collapsed="true"> + <TypeIdentifier> + <NewMemberFileName>OpenId\RelyingParty\OpenIdRelyingPartyControlBase.cs</NewMemberFileName> + </TypeIdentifier> + </Enum> + <Enum Name="DotNetOpenAuth.OpenId.RelyingParty.OpenIdRelyingPartyControlBase.LoginPersistence" Collapsed="true"> + <TypeIdentifier> + <NewMemberFileName>OpenId\RelyingParty\OpenIdRelyingPartyControlBase.cs</NewMemberFileName> + </TypeIdentifier> + </Enum> + <Class Name="DotNetOpenAuth.OpenId.RelyingParty.OpenIdRelyingPartyControlBase.DuplicateRequestedHostsComparer" Collapsed="true"> + <TypeIdentifier> + <NewMemberFileName>OpenId\RelyingParty\OpenIdRelyingPartyControlBase.cs</NewMemberFileName> + </TypeIdentifier> + <Lollipop Position="0.2" /> + </Class> + </NestedTypes> + <TypeIdentifier> + <HashCode>BA0AAsAAQCAwQAJAoFAWwADSAgE5EIEEEbAGSAwAgfI=</HashCode> + <FileName>OpenId\RelyingParty\OpenIdRelyingPartyControlBase.cs</FileName> + </TypeIdentifier> + <Lollipop Position="0.2" /> + </Class> + <Font Name="Segoe UI" Size="9" /> +</ClassDiagram>
\ No newline at end of file diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/IAuthenticationRequest.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/IAuthenticationRequest.cs index 8414031..27daa46 100644 --- a/src/DotNetOpenAuth/OpenId/RelyingParty/IAuthenticationRequest.cs +++ b/src/DotNetOpenAuth/OpenId/RelyingParty/IAuthenticationRequest.cs @@ -125,6 +125,22 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { void AddCallbackArguments(string key, string value); /// <summary> + /// Makes a key/value pair available when the authentication is completed. + /// </summary> + /// <param name="key">The parameter name.</param> + /// <param name="value">The value of the argument. Must not be null.</param> + /// <remarks> + /// <para>Note that these values are NOT protected against tampering in transit. No + /// security-sensitive data should be stored using this method.</para> + /// <para>The value stored here can be retrieved using + /// <see cref="IAuthenticationResponse.GetCallbackArgument"/>.</para> + /// <para>Since the data set here is sent in the querystring of the request and some + /// servers place limits on the size of a request URL, this data should be kept relatively + /// small to ensure successful authentication. About 1.5KB is about all that should be stored.</para> + /// </remarks> + void SetCallbackArgument(string key, string value); + + /// <summary> /// Adds an OpenID extension to the request directed at the OpenID provider. /// </summary> /// <param name="extension">The initialized extension to add to the request.</param> diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdAjaxTextBox.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdAjaxTextBox.cs index 6a4413f..0933e9c 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 { + 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,64 +230,18 @@ 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> private Dictionary<Type, string> clientScriptExtensions = new Dictionary<Type, string>(); - /// <summary> - /// Stores the result of an AJAX discovery request while it is waiting - /// to be picked up by ASP.NET on the way down to the user agent. - /// </summary> - private string discoveryResult; - #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 +257,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 +271,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 +291,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 +348,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 +364,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 +380,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 +396,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 +406,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 +422,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 +432,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 +442,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 +452,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 +462,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 +472,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 +482,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 +516,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; - } - } - - /// <summary> - /// Gets the relying party to use when verification of incoming messages is NOT wanted. - /// </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; - } + #region IPostBackDataHandler Members /// <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. + /// When implemented by a class, processes postback data for an ASP.NET server control. /// </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,180 +563,82 @@ 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); - } - } - - return filteredRequests; - } - - /// <summary> - /// Creates the relying party. - /// </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(); - } - - /// <summary> - /// Releases unmanaged and - optionally - managed resources - /// </summary> - /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param> - 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; + /// <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; } } - } - - /// <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); - } + return false; } /// <summary> - /// Fires the <see cref="LoggedIn"/> event. + /// 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="response">The response.</param> - private void OnLoggedIn(IAuthenticationResponse response) { - var loggedIn = this.LoggedIn; - if (loggedIn != null) { - loggedIn(this, new OpenIdEventArgs(response)); - } + protected virtual void RaisePostDataChangedEvent() { + this.OnTextChanged(); } /// <summary> - /// Invokes a method on a parent frame/window's OpenIdAjaxTextBox, - /// and closes the calling popup window if applicable. + /// Called on a postback when the Text property has changed. /// </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)); - } + protected virtual void OnTextChanged() { + EventHandler textChanged = this.TextChanged; + if (textChanged != null) { + textChanged(this, EventArgs.Empty); } - - // 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> @@ -1186,9 +663,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 +702,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..531748a 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,8 +145,9 @@ 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("OP icon size: " + box.dnoi_internal.op_logo.fileSize); + // The filesize check 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'; @@ -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,65 @@ 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 + var parsedPositiveAssertion = new window.dnoa_internal.PositiveAssertion(discoveryResult.successAuthData); + box.dnoi_internal.claimedIdentifier = parsedPositiveAssertion.claimedIdentifier; + box.dnoi_internal.setVisualCue('authenticated', parsedPositiveAssertion.endpoint, parsedPositiveAssertion.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; }; @@ -677,101 +390,5 @@ function initAjaxOpenId(box, openid_logo_url, dotnetopenid_logo_url, spinner_url box.getClaimedIdentifier = function() { return box.dnoi_internal.claimedIdentifier; }; // 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); - // 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"); - // 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({ - claimedIdentifier: claimedId, - requests: [{ endpoint: endpoint }] - }, box.value); - - box.dnoi_internal.processAuthorizationResult(oldAuthResult.toString()); - } + window.dnoa_internal.deserializePreviousAuthentication(findOrCreateHiddenField().value, box.dnoi_internal.onAuthSuccess); } - -function Uri(url) { - this.originalUri = url; - - this.toString = function() { - return this.originalUri; - }; - - this.getAuthority = function() { - var authority = this.getScheme() + "://" + this.getHost(); - return authority; - } - - 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; - } - - 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/OpenIdLogin.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdLogin.cs index fe1ce67..b0dce61 100644 --- a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdLogin.cs +++ b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdLogin.cs @@ -8,10 +8,13 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { using System; using System.ComponentModel; using System.Diagnostics.CodeAnalysis; + using System.Diagnostics.Contracts; using System.Globalization; + using System.Linq; using System.Web.UI; using System.Web.UI.HtmlControls; using System.Web.UI.WebControls; + using DotNetOpenAuth.Messaging; /// <summary> /// An ASP.NET control providing a complete OpenID login experience. @@ -105,7 +108,7 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { /// <summary> /// The default value for the <see cref="RememberMe"/> property. /// </summary> - private const bool RememberMeDefault = UsePersistentCookieDefault; + private const bool RememberMeDefault = false; /// <summary> /// The default value for the <see cref="UriValidatorEnabled"/> property. @@ -226,14 +229,6 @@ 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. - /// </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.")] - public event EventHandler<OpenIdEventArgs> LoggingIn; - - /// <summary> /// Fired when the Remember Me checkbox is changed by the user. /// </summary> [Description("Fires when the Remember Me checkbox is changed by the user.")] @@ -242,6 +237,20 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { #endregion #region Properties + + /// <summary> + /// Gets a <see cref="T:System.Web.UI.ControlCollection"/> object that represents the child controls for a specified server control in the UI hierarchy. + /// </summary> + /// <returns> + /// The collection of child controls for the specified server control. + /// </returns> + public override ControlCollection Controls { + get { + this.EnsureChildControls(); + return base.Controls; + } + } + /// <summary> /// Gets or sets the caption that appears before the text box. /// </summary> @@ -251,8 +260,15 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { [Localizable(true)] [Description("The caption that appears before the text box.")] public string LabelText { - get { return this.label.InnerText; } - set { this.label.InnerText = value; } + get { + EnsureChildControls(); + return this.label.InnerText; + } + + set { + EnsureChildControls(); + this.label.InnerText = value; + } } /// <summary> @@ -264,8 +280,15 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { [Localizable(true)] [Description("The text that introduces the example OpenID url.")] public string ExamplePrefix { - get { return this.examplePrefixLabel.Text; } - set { this.examplePrefixLabel.Text = value; } + get { + EnsureChildControls(); + return this.examplePrefixLabel.Text; + } + + set { + EnsureChildControls(); + this.examplePrefixLabel.Text = value; + } } /// <summary> @@ -278,8 +301,15 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { [Localizable(true)] [Description("The example OpenID Identifier to display to the user.")] public string ExampleUrl { - get { return this.exampleUrlLabel.Text; } - set { this.exampleUrlLabel.Text = value; } + get { + EnsureChildControls(); + return this.exampleUrlLabel.Text; + } + + set { + EnsureChildControls(); + this.exampleUrlLabel.Text = value; + } } /// <summary> @@ -292,8 +322,15 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { [Localizable(true)] [Description("The text to display if the user attempts to login without providing an Identifier.")] public string RequiredText { - get { return this.requiredValidator.Text.Substring(0, this.requiredValidator.Text.Length - RequiredTextSuffix.Length); } - set { this.requiredValidator.ErrorMessage = this.requiredValidator.Text = value + RequiredTextSuffix; } + get { + EnsureChildControls(); + return this.requiredValidator.Text.Substring(0, this.requiredValidator.Text.Length - RequiredTextSuffix.Length); + } + + set { + EnsureChildControls(); + this.requiredValidator.ErrorMessage = this.requiredValidator.Text = value + RequiredTextSuffix; + } } /// <summary> @@ -306,8 +343,15 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { [Localizable(true)] [Description("The text to display if the user provides an invalid form for an Identifier.")] public string UriFormatText { - get { return this.identifierFormatValidator.Text.Substring(0, this.identifierFormatValidator.Text.Length - RequiredTextSuffix.Length); } - set { this.identifierFormatValidator.ErrorMessage = this.identifierFormatValidator.Text = value + RequiredTextSuffix; } + get { + EnsureChildControls(); + return this.identifierFormatValidator.Text.Substring(0, this.identifierFormatValidator.Text.Length - RequiredTextSuffix.Length); + } + + set { + EnsureChildControls(); + this.identifierFormatValidator.ErrorMessage = this.identifierFormatValidator.Text = value + RequiredTextSuffix; + } } /// <summary> @@ -319,8 +363,15 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { [DefaultValue(UriValidatorEnabledDefault)] [Description("Whether to perform Identifier format validation prior to an authentication attempt.")] public bool UriValidatorEnabled { - get { return this.identifierFormatValidator.Enabled; } - set { this.identifierFormatValidator.Enabled = value; } + get { + EnsureChildControls(); + return this.identifierFormatValidator.Enabled; + } + + set { + EnsureChildControls(); + this.identifierFormatValidator.Enabled = value; + } } /// <summary> @@ -332,8 +383,15 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { [Localizable(true)] [Description("The text of the link users can click on to obtain an OpenID.")] public string RegisterText { - get { return this.registerLink.Text; } - set { this.registerLink.Text = value; } + get { + EnsureChildControls(); + return this.registerLink.Text; + } + + set { + EnsureChildControls(); + this.registerLink.Text = value; + } } /// <summary> @@ -346,8 +404,15 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { [Localizable(true)] [Description("The URL to link users to who click the link to obtain a new OpenID.")] public string RegisterUrl { - get { return this.registerLink.NavigateUrl; } - set { this.registerLink.NavigateUrl = value; } + get { + EnsureChildControls(); + return this.registerLink.NavigateUrl; + } + + set { + EnsureChildControls(); + this.registerLink.NavigateUrl = value; + } } /// <summary> @@ -360,8 +425,15 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { [Localizable(true)] [Description("The text of the tooltip to display when the user hovers over the link to obtain a new OpenID.")] public string RegisterToolTip { - get { return this.registerLink.ToolTip; } - set { this.registerLink.ToolTip = value; } + get { + EnsureChildControls(); + return this.registerLink.ToolTip; + } + + set { + EnsureChildControls(); + this.registerLink.ToolTip = value; + } } /// <summary> @@ -373,8 +445,15 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { [DefaultValue(RegisterVisibleDefault)] [Description("Whether to display a link to allow users to easily obtain a new OpenID.")] public bool RegisterVisible { - get { return this.registerLink.Visible; } - set { this.registerLink.Visible = value; } + get { + EnsureChildControls(); + return this.registerLink.Visible; + } + + set { + EnsureChildControls(); + this.registerLink.Visible = value; + } } /// <summary> @@ -386,8 +465,15 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { [Localizable(true)] [Description("The text that appears on the button that initiates login.")] public string ButtonText { - get { return this.loginButton.Text; } - set { this.loginButton.Text = value; } + get { + EnsureChildControls(); + return this.loginButton.Text; + } + + set { + EnsureChildControls(); + this.loginButton.Text = value; + } } /// <summary> @@ -399,8 +485,15 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { [Localizable(true)] [Description("The text of the \"Remember Me\" checkbox.")] public string RememberMeText { - get { return this.rememberMeCheckBox.Text; } - set { this.rememberMeCheckBox.Text = value; } + get { + EnsureChildControls(); + return this.rememberMeCheckBox.Text; + } + + set { + EnsureChildControls(); + this.rememberMeCheckBox.Text = value; + } } /// <summary> @@ -438,8 +531,15 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { [DefaultValue(RememberMeVisibleDefault)] [Description("Whether the \"Remember Me\" checkbox should be displayed.")] public bool RememberMeVisible { - get { return this.rememberMeCheckBox.Visible; } - set { this.rememberMeCheckBox.Visible = value; } + get { + EnsureChildControls(); + return this.rememberMeCheckBox.Visible; + } + + set { + EnsureChildControls(); + this.rememberMeCheckBox.Visible = value; + } } /// <summary> @@ -448,11 +548,11 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { /// </summary> [Bindable(true)] [Category("Appearance")] - [DefaultValue(UsePersistentCookieDefault)] + [DefaultValue(RememberMeDefault)] [Description("Whether a successful authentication should result in a persistent cookie being saved to the browser.")] public bool RememberMe { - get { return this.UsePersistentCookie; } - set { this.UsePersistentCookie = value; } + get { return this.UsePersistentCookie != LoginPersistence.Session; } + set { this.UsePersistentCookie = value ? LoginPersistence.PersistentAuthentication : LoginPersistence.Session; } } /// <summary> @@ -468,7 +568,7 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { set { unchecked { - this.WrappedTextBox.TabIndex = (short)(value + TextBoxTabIndexOffset); + base.TabIndex = (short)(value + TextBoxTabIndexOffset); this.loginButton.TabIndex = (short)(value + LoginButtonTabIndexOffset); this.rememberMeCheckBox.TabIndex = (short)(value + RememberMeTabIndexOffset); this.registerLink.TabIndex = (short)(value + RegisterTabIndexOffset); @@ -485,8 +585,15 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { [Localizable(true)] [Description("The tooltip to display when the user hovers over the login button.")] public string ButtonToolTip { - get { return this.loginButton.ToolTip; } - set { this.loginButton.ToolTip = value; } + get { + EnsureChildControls(); + return this.loginButton.ToolTip; + } + + set { + EnsureChildControls(); + this.loginButton.ToolTip = value; + } } /// <summary> @@ -497,10 +604,12 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { [Description("The validation group that the login button and text box validator belong to.")] public string ValidationGroup { get { + EnsureChildControls(); return this.requiredValidator.ValidationGroup; } set { + EnsureChildControls(); this.requiredValidator.ValidationGroup = value; this.loginButton.ValidationGroup = value; } @@ -525,7 +634,7 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { /// cookie should persist across user sessions. /// </summary> [Browsable(false), Bindable(false)] - public override bool UsePersistentCookie { + public override LoginPersistence UsePersistentCookie { get { return base.UsePersistentCookie; } @@ -535,8 +644,9 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { // use conditional here to prevent infinite recursion // with CheckedChanged event. - if (this.rememberMeCheckBox.Checked != value) { - this.rememberMeCheckBox.Checked = value; + bool rememberMe = value != LoginPersistence.Session; + if (this.rememberMeCheckBox.Checked != rememberMe) { + this.rememberMeCheckBox.Checked = rememberMe; } } } @@ -544,27 +654,27 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { #endregion /// <summary> + /// Outputs server control content to a provided <see cref="T:System.Web.UI.HtmlTextWriter"/> object and stores tracing information about the control if tracing is enabled. + /// </summary> + /// <param name="writer">The <see cref="T:System.Web.UI.HTmlTextWriter"/> object that receives the control content.</param> + public override void RenderControl(HtmlTextWriter writer) { + this.RenderChildren(writer); + } + + /// <summary> /// Creates the child controls. /// </summary> protected override void CreateChildControls() { - // Don't call base.CreateChildControls(). This would add the WrappedTextBox - // to the Controls collection, which would implicitly remove it from the table - // we have already added it to. + this.InitializeControls(); // Just add the panel we've assembled earlier. - this.Controls.Add(this.panel); - - if (ShouldBeFocused) { - WrappedTextBox.Focus(); - } + base.Controls.Add(this.panel); } /// <summary> /// Initializes the child controls. /// </summary> - protected override void InitializeControls() { - base.InitializeControls(); - + protected void InitializeControls() { this.panel = new Panel(); Table table = new Table(); @@ -583,7 +693,7 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { // top row, middle cell cell = new TableCell(); - cell.Controls.Add(this.WrappedTextBox); + cell.Controls.Add(new InPlaceControl(this)); row1.Cells.Add(cell); // top row, right cell @@ -611,7 +721,7 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { this.requiredValidator.ErrorMessage = RequiredTextDefault + RequiredTextSuffix; this.requiredValidator.Text = RequiredTextDefault + RequiredTextSuffix; this.requiredValidator.Display = ValidatorDisplay.Dynamic; - this.requiredValidator.ControlToValidate = WrappedTextBox.ID; + this.requiredValidator.ControlToValidate = this.ID; this.requiredValidator.ValidationGroup = ValidationGroupDefault; cell.Controls.Add(this.requiredValidator); this.identifierFormatValidator = new CustomValidator(); @@ -620,7 +730,7 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { this.identifierFormatValidator.ServerValidate += this.IdentifierFormatValidator_ServerValidate; this.identifierFormatValidator.Enabled = UriValidatorEnabledDefault; this.identifierFormatValidator.Display = ValidatorDisplay.Dynamic; - this.identifierFormatValidator.ControlToValidate = WrappedTextBox.ID; + this.identifierFormatValidator.ControlToValidate = this.ID; this.identifierFormatValidator.ValidationGroup = ValidationGroupDefault; cell.Controls.Add(this.identifierFormatValidator); this.errorLabel = new Label(); @@ -680,26 +790,17 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { } /// <summary> - /// Customizes HTML rendering of the control. - /// </summary> - /// <param name="writer">An <see cref="T:System.Web.UI.HtmlTextWriter"/> that represents the output stream to render HTML content on the client.</param> - protected override void Render(HtmlTextWriter writer) { - // avoid writing begin and end SPAN tags for XHTML validity. - RenderContents(writer); - } - - /// <summary> /// Renders the child controls. /// </summary> /// <param name="writer">The <see cref="T:System.Web.UI.HtmlTextWriter"/> object that receives the rendered content.</param> protected override void RenderChildren(HtmlTextWriter writer) { if (!this.DesignMode) { - this.label.Attributes["for"] = this.WrappedTextBox.ClientID; + this.label.Attributes["for"] = this.ClientID; if (!string.IsNullOrEmpty(this.IdSelectorIdentifier)) { this.idselectorJavascript.Visible = true; this.idselectorJavascript.Text = @"<script type='text/javascript'><!-- -idselector_input_id = '" + WrappedTextBox.ClientID + @"'; +idselector_input_id = '" + this.ClientID + @"'; // --></script> <script type='text/javascript' id='__openidselector' src='https://www.idselector.com/selector/" + this.IdSelectorIdentifier + @"' charset='utf-8'></script>"; } else { @@ -737,30 +838,6 @@ idselector_input_id = '" + WrappedTextBox.ClientID + @"'; } /// <summary> - /// Fires the <see cref="LoggingIn"/> event. - /// </summary> - /// <returns> - /// Returns whether the login should proceed. False if some event handler canceled the request. - /// </returns> - protected virtual bool OnLoggingIn() { - EventHandler<OpenIdEventArgs> loggingIn = this.LoggingIn; - if (this.Request == null) { - this.CreateRequest(); - } - - if (this.Request != null) { - OpenIdEventArgs args = new OpenIdEventArgs(this.Request); - if (loggingIn != null) { - loggingIn(this, args); - } - - return !args.Cancel; - } else { - return false; - } - } - - /// <summary> /// Fires the <see cref="RememberMeChanged"/> event. /// </summary> protected virtual void OnRememberMeChanged() { @@ -799,8 +876,49 @@ idselector_input_id = '" + WrappedTextBox.ClientID + @"'; return; } - if (this.OnLoggingIn()) { - this.LogOn(); + IAuthenticationRequest request = this.CreateRequests().FirstOrDefault(); + if (request != null) { + this.LogOn(request); + } else { + if (!string.IsNullOrEmpty(this.FailedMessageText)) { + this.errorLabel.Text = string.Format(CultureInfo.CurrentCulture, this.FailedMessageText, OpenIdStrings.OpenIdEndpointNotFound); + this.errorLabel.Visible = true; + } + } + } + + /// <summary> + /// Renders the control inner. + /// </summary> + /// <param name="writer">The writer.</param> + private void RenderControlInner(HtmlTextWriter writer) { + base.RenderControl(writer); + } + + /// <summary> + /// A control that acts as a placeholder to indicate where + /// the OpenIdLogin control should render its OpenIdTextBox parent. + /// </summary> + private class InPlaceControl : PlaceHolder { + /// <summary> + /// The owning control to render. + /// </summary> + private OpenIdLogin renderControl; + + /// <summary> + /// Initializes a new instance of the <see cref="InPlaceControl"/> class. + /// </summary> + /// <param name="renderControl">The render control.</param> + internal InPlaceControl(OpenIdLogin renderControl) { + this.renderControl = renderControl; + } + + /// <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) { + this.renderControl.RenderControlInner(writer); } } } 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/OpenIdRelyingParty.cd b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingParty.cd new file mode 100644 index 0000000..0519ecb --- /dev/null +++ b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingParty.cd @@ -0,0 +1 @@ +
\ No newline at end of file diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingPartyAjaxControlBase.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingPartyAjaxControlBase.cs index da2a9ae..480b08c 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,7 +40,47 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { protected const string CallbackJsFunctionAsync = "window.dnoa_internal.callbackAsync"; /// <summary> - /// Stores the result of a AJAX callback discovery. + /// 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> + /// Backing field for the <see cref="RelyingPartyNonVerifying"/> property. + /// </summary> + private static OpenIdRelyingParty relyingPartyNonVerifying; + + /// <summary> + /// The authentication response that just came in. + /// </summary> + private IAuthenticationResponse authenticationResponse; + + /// <summary> + /// Stores the result of an AJAX discovery request while it is waiting + /// to be picked up by ASP.NET on the way down to the user agent. /// </summary> private string discoveryResult; @@ -61,10 +95,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="OpenIdRelyingPartyControlBase.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 +124,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 +252,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 +272,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 +280,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.AuthenticationResponse != null && !this.AuthenticationProcessedAlready) { + // Only process messages targeted at this control. + // Note that Stateless mode causes no receiver to be indicated. + string receiver = this.AuthenticationResponse.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 @@ -176,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> @@ -189,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 @@ -247,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> @@ -294,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) { @@ -306,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..c6f6a75 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,139 +8,170 @@ 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 -/// <summary>Instantiates an object that represents an OpenID Identifier.</summary> -window.OpenId = 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) { - /// <summary>Instantiates an object that stores discovery results of some identifier.</summary> - function DiscoveryResult(identifier, discoveryInfo) { - /// <summary> - /// Instantiates an object that describes an OpenID service endpoint and facilitates - /// initiating and tracking an authentication request. - /// </summary> - function ServiceEndpoint(requestInfo, userSuppliedIdentifier) { - 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.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 width = 800; - var height = 600; - if (self.setup.getQueryArgValue("openid.return_to").indexOf("dotnetopenid.popupUISupported") >= 0) { - width = 450; - height = 500; - } +// The possible authentication results +window.dnoa_internal.authSuccess = new Object(); +window.dnoa_internal.authRefused = new Object(); +window.dnoa_internal.timedOut = new Object(); - 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) { - // 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; - - // 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(); - } - window.dnoa_internal.processAuthorizationResult = null; - } - } - }, 250); - }; - }; - - 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.length = discoveryInfo.requests.length; - for (var i = 0; i < discoveryInfo.requests.length; i++) { - this[i] = new ServiceEndpoint(discoveryInfo.requests[i], identifier); - } - } else { - this.length = 0; - } - }; +/// <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.OpenIdIdentifier = function(identifier) { + /// <summary>Performs discovery on the identifier.</summary> + /// <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>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. discoveryResult = eval('(' + discoveryResult + ')'); // Add behavior for later use. - discoveryResult = new DiscoveryResult(identifier, discoveryResult); + discoveryResult = new window.dnoa_internal.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 +179,314 @@ 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 parsedPositiveAssertion = new window.dnoa_internal.PositiveAssertion(resultUri); + if (parsedPositiveAssertion.claimedIdentifier && parsedPositiveAssertion.claimedIdentifier != discoveryResult.claimedIdentifier) { + discoveryResult.claimedIdentifier = parsedPositiveAssertion.claimedIdentifier; + trace('Authenticated as ' + parsedPositiveAssertion.claimedIdentifier); + } + } 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"); +}; + +/// <summary>Instantiates an object that stores discovery results of some identifier.</summary> +window.dnoa_internal.DiscoveryResult = function(identifier, discoveryInfo) { + var thisDiscoveryResult = this; + + /// <summary> + /// Instantiates an object that describes an OpenID service endpoint and facilitates + /// initiating and tracking an authentication request. + /// </summary> + function ServiceEndpoint(requestInfo, userSuppliedIdentifier) { + 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 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 (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; + 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 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(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 (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.claimedIdentifier = authUri////////////////////////////////// + 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; + + 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); + } + } 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> +/// Called in a page had an AJAX control that had already obtained a positive assertion +/// when a postback occurred, and now that control wants to restore its 'authenticated' state. +/// </summary> +/// <param name="positiveAssertion">The string form of the URI that contains the positive assertion.</param> +/// <param name="onAuthSuccess">Fired if the positive assertion is successfully processed, as if it had just come in.</param> +window.dnoa_internal.deserializePreviousAuthentication = function(positiveAssertion, onAuthSuccess) { + if (!positiveAssertion || positiveAssertion.length === 0) { + return; + } + + trace('Revitalizing an old positive assertion from a prior postback.'); + var oldAuthResult = new window.dnoa_internal.Uri(positiveAssertion); + + // 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 parsedPositiveAssertion = new window.dnoa_internal.PositiveAssertion(positiveAssertion); + + // We weren't given a full discovery history, but we can spoof this much from the + // authentication assertion. + trace('Deserialized claimed_id: ' + parsedPositiveAssertion.claimedIdentifier + ' and endpoint: ' + parsedPositiveAssertion.endpoint); + var discoveryInfo = { + claimedIdentifier: parsedPositiveAssertion.claimedIdentifier, + requests: [{ endpoint: parsedPositiveAssertion.endpoint }] + }; + + window.dnoa_internal.discoveryResults[box.value] = discoveryResult = new window.dnoa_internal.DiscoveryResult(parsedPositiveAssertion.userSuppliedIdentifier, discoveryInfo); + discoveryResult[0].result = window.dnoa_internal.authSuccess; + discoveryResult.successAuthData = positiveAssertion; + + // restore old state from before postback + if (onAuthSuccess) { + onAuthSuccess(discoveryResult, discoveryResult[0]); + } +}; + +window.dnoa_internal.PositiveAssertion = function(uri) { + uri = new window.dnoa_internal.Uri(uri.toString()); + this.endpoint = new window.dnoa_internal.Uri(uri.getQueryArgValue("dnoa.op_endpoint")); + this.userSuppliedIdentifier = uri.getQueryArgValue('dnoa.userSuppliedIdentifier'); + this.claimedIdentifier = uri.getQueryArgValue('openid.claimed_id'); + if (!this.claimedIdentifier) { + this.claimedIdentifier = uri.getQueryArgValue('dnoa.claimed_id'); + } + this.toString = function() { return uri.toString(); }; +}; diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingPartyControlBase.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingPartyControlBase.cs index c584580..ed305e8 100644 --- a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingPartyControlBase.cs +++ b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingPartyControlBase.cs @@ -30,12 +30,22 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { /// A common base class for OpenID Relying Party controls. /// </summary> [DefaultProperty("Identifier"), ValidationProperty("Identifier")] - public abstract class OpenIdRelyingPartyControlBase : Control { + public abstract class OpenIdRelyingPartyControlBase : Control, IDisposable { /// <summary> /// The manifest resource name of the javascript file to include on the hosting page. /// </summary> internal const string EmbeddedJavascriptResource = Util.DefaultNamespace + ".OpenId.RelyingParty.OpenIdRelyingPartyControlBase.js"; + /// <summary> + /// The cookie used to persist the Identifier the user logged in with. + /// </summary> + internal const string PersistentIdentifierCookieName = OpenIdUtilities.CustomParameterPrefix + "OpenIDIdentifier"; + + /// <summary> + /// The callback parameter name to use to store which control initiated the auth request. + /// </summary> + internal const string ReturnToReceivingControlId = OpenIdUtilities.CustomParameterPrefix + "receiver"; + #region Property category constants /// <summary> @@ -55,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> @@ -70,7 +105,7 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { /// <summary> /// Default value of <see cref="UsePersistentCookie"/>. /// </summary> - private const bool UsePersistentCookieDefault = false; + private const LoginPersistence UsePersistentCookieDefault = LoginPersistence.Session; /// <summary> /// Default value of <see cref="LoginMode"/>. @@ -138,40 +173,21 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { #endregion - #region Callback parameter names - /// <summary> - /// The callback parameter for use with persisting the <see cref="UsePersistentCookie"/> property. + /// The lifetime of the cookie used to persist the Identifier the user logged in with. /// </summary> - private const string UsePersistentCookieCallbackKey = OpenIdUtilities.CustomParameterPrefix + "UsePersistentCookie"; + private static readonly TimeSpan PersistentIdentifierTimeToLiveDefault = TimeSpan.FromDays(14); /// <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 callback parameter name to use to store which control initiated the auth request. - /// </summary> - private const string ReturnToReceivingControlId = OpenIdUtilities.CustomParameterPrefix + "receiver"; - - /// <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. + /// Backing field for the <see cref="RelyingParty"/> property. /// </summary> - private const string PopupUISupportedJsHint = "dotnetopenid.popupUISupported"; - - #endregion + private OpenIdRelyingParty relyingParty; /// <summary> - /// Backing field for the <see cref="RelyingParty"/> property. + /// A value indicating whether the <see cref="relyingParty"/> field contains + /// an instance that we own and should Dispose. /// </summary> - private OpenIdRelyingParty relyingParty; + private bool relyingPartyOwned; /// <summary> /// Initializes a new instance of the <see cref="OpenIdRelyingPartyControlBase"/> class. @@ -182,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> @@ -229,6 +244,30 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { } /// <summary> + /// How an OpenID user session should be persisted across visits. + /// </summary> + public enum LoginPersistence { + /// <summary> + /// The user should only be logged in as long as the browser window remains open. + /// Nothing is persisted to help the user on a return visit. Public kiosk mode. + /// </summary> + Session, + + /// <summary> + /// The user should only be logged in as long as the browser window remains open. + /// The OpenID Identifier is persisted to help expedite re-authentication when + /// the user visits the next time. + /// </summary> + SessionAndPersistentIdentifier, + + /// <summary> + /// The user is issued a persistent authentication ticket so that no login is + /// necessary on their return visit. + /// </summary> + PersistentAuthentication, + } + + /// <summary> /// Gets or sets the <see cref="OpenIdRelyingParty"/> instance to use. /// </summary> /// <value>The default value is an <see cref="OpenIdRelyingParty"/> instance initialized according to the web.config file.</value> @@ -244,12 +283,18 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { get { if (this.relyingParty == null) { this.relyingParty = this.CreateRelyingParty(); + this.relyingPartyOwned = true; } return this.relyingParty; } set { + if (this.relyingPartyOwned && this.relyingParty != null) { + this.relyingParty.Dispose(); + } + this.relyingParty = value; + this.relyingPartyOwned = false; } } @@ -335,8 +380,8 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { [Bindable(true), DefaultValue(UsePersistentCookieDefault), Category(BehaviorCategory)] [Description("Whether to send a persistent cookie upon successful " + "login so the user does not have to log in upon returning to this site.")] - public virtual bool UsePersistentCookie { - get { return (bool)(this.ViewState[UsePersistentCookieViewStateKey] ?? UsePersistentCookieDefault); } + public virtual LoginPersistence UsePersistentCookie { + get { return (LoginPersistence)(this.ViewState[UsePersistentCookieViewStateKey] ?? UsePersistentCookieDefault); } set { this.ViewState[UsePersistentCookieViewStateKey] = value; } } @@ -345,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; } } @@ -390,11 +435,31 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { internal AssociationPreference AssociationPreference { get; set; } /// <summary> + /// Clears any cookie set by this control to help the user on a returning visit next time. + /// </summary> + public static void LogOff() { + HttpContext.Current.Response.SetCookie(CreateIdentifierPersistingCookie(null)); + } + + /// <summary> /// Immediately redirects to the OpenID Provider to verify the Identifier /// provided in the text box. /// </summary> public void LogOn() { IAuthenticationRequest request = this.CreateRequests().FirstOrDefault(); + ErrorUtilities.VerifyProtocol(request != null, OpenIdStrings.OpenIdEndpointNotFound); + this.LogOn(request); + } + + /// <summary> + /// Immediately redirects to the OpenID Provider to verify the Identifier + /// provided in the text box. + /// </summary> + /// <param name="request">The request.</param> + public void LogOn(IAuthenticationRequest request) { + ErrorUtilities.VerifyArgumentNotNull(request, "request"); + Contract.Requires<ArgumentNullException>(request != null); + if (this.IsPopupAppropriate(request)) { this.ScriptPopupWindow(request); } else { @@ -403,6 +468,29 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { } /// <summary> + /// Enables a server control to perform final clean up before it is released from memory. + /// </summary> + [SuppressMessage("Microsoft.Design", "CA1063:ImplementIDisposableCorrectly", Justification = "Base class doesn't implement virtual Dispose(bool), so we must call its Dispose() method.")] + public sealed override void Dispose() { + this.Dispose(true); + base.Dispose(); + GC.SuppressFinalize(this); + } + + /// <summary> + /// Releases unmanaged and - optionally - managed resources + /// </summary> + /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param> + protected virtual void Dispose(bool disposing) { + if (disposing) { + if (this.relyingPartyOwned && this.relyingParty != null) { + this.relyingParty.Dispose(); + this.relyingParty = null; + } + } + } + + /// <summary> /// Creates the authentication requests for a given user-supplied Identifier. /// </summary> /// <returns>A sequence of authentication requests, any one of which may be @@ -443,31 +531,31 @@ 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(CultureInfo.InvariantCulture)); - req.AddCallbackArguments(ReturnToReceivingControlId, this.ClientID); + req.SetCallbackArgument(UsePersistentCookieCallbackKey, this.UsePersistentCookie.ToString()); + req.SetCallbackArgument(ReturnToReceivingControlId, this.ClientID); } ((AuthenticationRequest)req).AssociationPreference = this.AssociationPreference; - this.OnLoggingIn(req); - - yield return req; + if (this.OnLoggingIn(req)) { + yield return req; + } } } @@ -483,12 +571,16 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { return; } + if (this.Identifier == null) { + this.TryPresetIdentifierWithCookie(); + } + // Take an unreliable sneek peek to see if we're in a popup and an OpenID // assertion is coming in. We shouldn't process assertions in a popup window. if (this.Page.Request.QueryString[UIPopupCallbackKey] == "1" && this.Page.Request.QueryString[UIPopupCallbackParentKey] == null) { // We're in a popup window. We need to close it and pass the // message back to the parent window for processing. - this.ScriptClosingPopup(); + this.ScriptClosingPopupOrIFrame(); return; // don't do any more processing on it now } @@ -497,33 +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); - bool persistentBool; - if (persistentString != null && bool.TryParse(persistentString, out persistentBool)) { - this.UsePersistentCookie = persistentBool; - } + 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); } } @@ -554,9 +654,13 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { } if (!args.Cancel) { + if (this.UsePersistentCookie == LoginPersistence.SessionAndPersistentIdentifier) { + Page.Response.SetCookie(CreateIdentifierPersistingCookie(response)); + } + switch (this.LoginMode) { case LoginSiteNotification.FormsAuthentication: - FormsAuthentication.RedirectFromLoginPage(response.ClaimedIdentifier, this.UsePersistentCookie); + FormsAuthentication.RedirectFromLoginPage(response.ClaimedIdentifier, this.UsePersistentCookie == LoginPersistence.PersistentAuthentication); break; case LoginSiteNotification.None: default: @@ -623,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. @@ -685,6 +801,57 @@ 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> + /// <returns>An persistent cookie.</returns> + private static HttpCookie CreateIdentifierPersistingCookie(IAuthenticationResponse response) { + HttpCookie cookie = new HttpCookie(PersistentIdentifierCookieName); + bool clearingCookie = false; + + // We'll try to store whatever it was the user originally typed in, but fallback + // to the final claimed_id. + if (response != null && response.Status == AuthenticationStatus.Authenticated) { + var positiveResponse = (PositiveAuthenticationResponse)response; + + // We must escape the value because XRIs start with =, and any leading '=' gets dropped (by ASP.NET?) + cookie.Value = Uri.EscapeDataString(positiveResponse.Endpoint.UserSuppliedIdentifier ?? response.ClaimedIdentifier); + } else { + clearingCookie = true; + cookie.Value = string.Empty; + if (HttpContext.Current.Request.Browser["supportsEmptyStringInCookieValue"] == "false") { + cookie.Value = "NoCookie"; + } + } + + if (clearingCookie) { + // mark the cookie has having already expired to cause the user agent to delete + // the old persisted cookie. + cookie.Expires = DateTime.Now.Subtract(TimeSpan.FromDays(1)); + } else { + // Make the cookie persistent by setting an expiration date + cookie.Expires = DateTime.Now + PersistentIdentifierTimeToLiveDefault; + } + + return cookie; + } + + /// <summary> /// Gets the javascript to executee to redirect or POST an OpenID message to a remote party. /// </summary> /// <param name="request">The authentication request to send.</param> @@ -712,27 +879,30 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { startupScript.AppendLine("window.dnoa_internal = new Object();"); startupScript.AppendLine("window.dnoa_internal.processAuthorizationResult = function(uri) { window.location = uri; };"); startupScript.AppendLine("window.dnoa_internal.popupWindow = function() {"); - startupScript.AppendFormat( - @"\tvar openidPopup = {0}", - UIUtilities.GetWindowPopupScript(this.RelyingParty, request, "openidPopup")); + startupScript.AppendFormat( + @"\tvar openidPopup = {0}", + UIUtilities.GetWindowPopupScript(this.RelyingParty, request, "openidPopup")); startupScript.AppendLine("};"); this.Page.ClientScript.RegisterClientScriptBlock(this.GetType(), "loginPopup", startupScript.ToString(), true); } /// <summary> - /// Wires the popup window to close itself and pass the authentication result to the parent window. + /// Tries to preset the <see cref="Identifier"/> property based on a persistent + /// cookie on the browser. /// </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); + /// <returns> + /// A value indicating whether the <see cref="Identifier"/> property was + /// successfully preset to some non-empty value. + /// </returns> + private bool TryPresetIdentifierWithCookie() { + HttpCookie cookie = this.Page.Request.Cookies[PersistentIdentifierCookieName]; + if (cookie != null) { + this.Identifier = Uri.UnescapeDataString(cookie.Value); + return true; + } - // 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. + return false; } /// <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 b7c879e..b10fdb1 100644 --- a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdTextBox.cs +++ b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdTextBox.cs @@ -40,7 +40,7 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { /// </remarks> [DefaultProperty("Text"), ValidationProperty("Text")] [ToolboxData("<{0}:OpenIdTextBox runat=\"server\" />")] - public class OpenIdTextBox : CompositeControl, IEditableTextControl, ITextControl { + public class OpenIdTextBox : OpenIdRelyingPartyControlBase, IEditableTextControl, ITextControl, IPostBackDataHandler { /// <summary> /// The name of the manifest stream containing the /// OpenID logo that is placed inside the text box. @@ -52,38 +52,18 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { /// </summary> protected const short TabIndexDefault = 0; - /// <summary> - /// Default value of <see cref="UsePersistentCookie"/>. - /// </summary> - protected const bool UsePersistentCookieDefault = false; - #region Property category constants /// <summary> - /// The "Appearance" category for properties. - /// </summary> - private const string AppearanceCategory = "Appearance"; - - /// <summary> /// The "Simple Registration" category for properties. /// </summary> private const string ProfileCategory = "Simple Registration"; - /// <summary> - /// The "Behavior" category for properties. - /// </summary> - private const string BehaviorCategory = "Behavior"; - #endregion #region Property viewstate keys /// <summary> - /// The viewstate key to use for the <see cref="Popup"/> property. - /// </summary> - private const string PopupViewStateKey = "Popup"; - - /// <summary> /// The viewstate key to use for the <see cref="RequestEmail"/> property. /// </summary> private const string RequestEmailViewStateKey = "RequestEmail"; @@ -104,11 +84,6 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { private const string RequestCountryViewStateKey = "RequestCountry"; /// <summary> - /// The viewstate key to use for the <see cref="RequireSsl"/> property. - /// </summary> - private const string RequireSslViewStateKey = "RequireSsl"; - - /// <summary> /// The viewstate key to use for the <see cref="RequestLanguage"/> property. /// </summary> private const string RequestLanguageViewStateKey = "RequestLanguage"; @@ -144,43 +119,48 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { private const string ShowLogoViewStateKey = "ShowLogo"; /// <summary> - /// The viewstate key to use for the <see cref="UsePersistentCookie"/> property. - /// </summary> - private const string UsePersistentCookieViewStateKey = "UsePersistentCookie"; - - /// <summary> /// The viewstate key to use for the <see cref="RequestGender"/> property. /// </summary> private const string RequestGenderViewStateKey = "RequestGender"; /// <summary> - /// The viewstate key to use for the <see cref="ReturnToUrl"/> property. + /// The viewstate key to use for the <see cref="RequestBirthDate"/> property. /// </summary> - private const string ReturnToUrlViewStateKey = "ReturnToUrl"; + private const string RequestBirthDateViewStateKey = "RequestBirthDate"; /// <summary> - /// The viewstate key to use for the <see cref="Stateless"/> property. + /// The viewstate key to use for the <see cref="CssClass"/> property. /// </summary> - private const string StatelessViewStateKey = "Stateless"; + private const string CssClassViewStateKey = "CssClass"; /// <summary> - /// The viewstate key to use for the <see cref="RequestBirthDate"/> property. + /// The viewstate key to use for the <see cref="MaxLength"/> property. /// </summary> - private const string RequestBirthDateViewStateKey = "RequestBirthDate"; + private const string MaxLengthViewStateKey = "MaxLength"; /// <summary> - /// The viewstate key to use for the <see cref="RealmUrl"/> property. + /// The viewstate key to use for the <see cref="Columns"/> property. /// </summary> - private const string RealmUrlViewStateKey = "RealmUrl"; + private const string ColumnsViewStateKey = "Columns"; - #endregion + /// <summary> + /// The viewstate key to use for the <see cref="TabIndex"/> property. + /// </summary> + private const string TabIndexViewStateKey = "TabIndex"; - #region Property defaults + /// <summary> + /// The viewstate key to use for the <see cref="Enabled"/> property. + /// </summary> + private const string EnabledViewStateKey = "Enabled"; /// <summary> - /// The default value for the <see cref="Popup"/> property. + /// The viewstate key to use for the <see cref="Name"/> property. /// </summary> - private const PopupBehavior PopupDefault = PopupBehavior.Never; + private const string NameViewStateKey = "Name"; + + #endregion + + #region Property defaults /// <summary> /// The default value for the <see cref="Columns"/> property. @@ -193,19 +173,14 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { private const int MaxLengthDefault = 40; /// <summary> - /// The default value for the <see cref="EnableRequestProfile"/> property. + /// The default value for the <see cref="Name"/> property. /// </summary> - private const bool EnableRequestProfileDefault = true; + private const string NameDefault = "openid_identifier"; /// <summary> - /// The default value for the <see cref="RequireSsl"/> property. - /// </summary> - private const bool RequireSslDefault = false; - - /// <summary> - /// The default value for the <see cref="Stateless"/> property. + /// The default value for the <see cref="EnableRequestProfile"/> property. /// </summary> - private const bool StatelessDefault = false; + private const bool EnableRequestProfileDefault = true; /// <summary> /// The default value for the <see cref="ShowLogo"/> property. @@ -228,21 +203,11 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { private const string CssClassDefault = "openid"; /// <summary> - /// The default value for the <see cref="ReturnToUrl"/> property. - /// </summary> - private const string ReturnToUrlDefault = ""; - - /// <summary> /// The default value for the <see cref="Text"/> property. /// </summary> private const string TextDefault = ""; /// <summary> - /// The default value for the <see cref="RealmUrl"/> property. - /// </summary> - private const string RealmUrlDefault = "~/"; - - /// <summary> /// The default value for the <see cref="RequestEmail"/> property. /// </summary> private const DemandLevel RequestEmailDefault = DemandLevel.NoRequest; @@ -290,79 +255,17 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { #endregion /// <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 callback parameter for use with persisting the <see cref="UsePersistentCookie"/> property. - /// </summary> - private const string UsePersistentCookieCallbackKey = "OpenIdTextBox_UsePersistentCookie"; - - /// <summary> - /// The text in the text box before the text box is instantiated. - /// </summary> - private string text = TextDefault; - - /// <summary> - /// The text box itself. - /// </summary> - private TextBox wrappedTextBox; - - /// <summary> - /// Backing field for the <see cref="RelyingParty"/> property. - /// </summary> - private OpenIdRelyingParty relyingParty; - - /// <summary> /// Initializes a new instance of the <see cref="OpenIdTextBox"/> class. /// </summary> public OpenIdTextBox() { - this.InitializeControls(); } - #region Events - - /// <summary> - /// Fired upon completion of a successful login. - /// </summary> - [Description("Fired upon completion of a successful login.")] - public event EventHandler<OpenIdEventArgs> LoggedIn; - - /// <summary> - /// Fired when a login attempt fails. - /// </summary> - [Description("Fired when a login attempt fails.")] - public event EventHandler<OpenIdEventArgs> Failed; - - /// <summary> - /// Fired when an authentication attempt is canceled at the OpenID Provider. - /// </summary> - [Description("Fired when an authentication attempt is canceled at the OpenID Provider.")] - public event EventHandler<OpenIdEventArgs> Canceled; - - /// <summary> - /// Fired when an Immediate authentication attempt fails, and the Provider suggests using non-Immediate mode. - /// </summary> - [Description("Fired when an Immediate authentication attempt fails, and the Provider suggests using non-Immediate mode.")] - public event EventHandler<OpenIdEventArgs> SetupRequired; - - #endregion - #region IEditableTextControl Members /// <summary> /// Occurs when the content of the text changes between posts to the server. /// </summary> - public event EventHandler TextChanged { - add { this.WrappedTextBox.TextChanged += value; } - remove { this.WrappedTextBox.TextChanged -= value; } - } + public event EventHandler TextChanged; #endregion @@ -374,102 +277,18 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { [Bindable(true), DefaultValue(""), Category(AppearanceCategory)] [Description("The content of the text box.")] public string Text { - get { - return this.WrappedTextBox != null ? this.WrappedTextBox.Text : this.text; - } - - set { - this.text = value; - if (this.WrappedTextBox != null) { - this.WrappedTextBox.Text = value; - } - } - } - - /// <summary> - /// Gets or sets the OpenID <see cref="Realm"/> of the relying party web site. - /// </summary> - [SuppressMessage("Microsoft.Usage", "CA1806:DoNotIgnoreMethodResults", MessageId = "System.Uri", Justification = "Using Uri.ctor for validation.")] - [SuppressMessage("Microsoft.Usage", "CA1806:DoNotIgnoreMethodResults", MessageId = "DotNetOpenAuth.OpenId.Realm", Justification = "Using ctor for validation.")] - [SuppressMessage("Microsoft.Design", "CA1056:UriPropertiesShouldNotBeStrings", Justification = "Bindable property must be simple type")] - [Bindable(true), DefaultValue(RealmUrlDefault), Category(BehaviorCategory)] - [Description("The OpenID Realm of the relying party web site.")] - [UrlProperty, Editor("System.Web.UI.Design.UrlEditor, System.Design, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a", typeof(UITypeEditor))] - public string RealmUrl { - get { - return (string)(ViewState[RealmUrlViewStateKey] ?? RealmUrlDefault); - } - - set { - if (Page != null && !DesignMode) { - // Validate new value by trying to construct a Realm object based on it. - new Realm(OpenIdUtilities.GetResolvedRealm(this.Page, value, this.RelyingParty.Channel.GetRequestFromContext())); // throws an exception on failure. - } else { - // We can't fully test it, but it should start with either ~/ or a protocol. - if (Regex.IsMatch(value, @"^https?://")) { - new Uri(value.Replace("*.", string.Empty)); // make sure it's fully-qualified, but ignore wildcards - } else if (value.StartsWith("~/", StringComparison.Ordinal)) { - // this is valid too - } else { - throw new UriFormatException(); - } - } - ViewState[RealmUrlViewStateKey] = value; - } - } - - /// <summary> - /// Gets or sets the OpenID ReturnTo of the relying party web site. - /// </summary> - [SuppressMessage("Microsoft.Usage", "CA2234:PassSystemUriObjectsInsteadOfStrings", Justification = "Bindable property must be simple type")] - [SuppressMessage("Microsoft.Usage", "CA1806:DoNotIgnoreMethodResults", MessageId = "System.Uri", Justification = "Using Uri.ctor for validation.")] - [SuppressMessage("Microsoft.Design", "CA1056:UriPropertiesShouldNotBeStrings", Justification = "Bindable property must be simple type")] - [Bindable(true), DefaultValue(ReturnToUrlDefault), Category(BehaviorCategory)] - [Description("The OpenID ReturnTo of the relying party web site.")] - [UrlProperty, Editor("System.Web.UI.Design.UrlEditor, System.Design, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a", typeof(UITypeEditor))] - public string ReturnToUrl { - get { - return (string)(this.ViewState[ReturnToUrlViewStateKey] ?? ReturnToUrlDefault); - } - - set { - if (this.Page != null && !this.DesignMode) { - // Validate new value by trying to construct a Uri based on it. - new Uri(this.RelyingParty.Channel.GetRequestFromContext().UrlBeforeRewriting, this.Page.ResolveUrl(value)); // throws an exception on failure. - } else { - // We can't fully test it, but it should start with either ~/ or a protocol. - if (Regex.IsMatch(value, @"^https?://")) { - new Uri(value); // make sure it's fully-qualified, but ignore wildcards - } else if (value.StartsWith("~/", StringComparison.Ordinal)) { - // this is valid too - } else { - throw new UriFormatException(); - } - } - - this.ViewState[ReturnToUrlViewStateKey] = value; - } - } - - /// <summary> - /// Gets or sets a value indicating when to use a popup window to complete the login experience. - /// </summary> - /// <value>The default value is <see cref="PopupBehavior.Never"/>.</value> - [Bindable(true), DefaultValue(PopupDefault), Category(BehaviorCategory)] - [Description("When to use a popup window to complete the login experience.")] - public PopupBehavior Popup { - get { return (PopupBehavior)(ViewState[PopupViewStateKey] ?? PopupDefault); } - set { ViewState[PopupViewStateKey] = value; } + get { return this.Identifier != null ? this.Identifier.OriginalString : string.Empty; } + set { this.Identifier = value; } } /// <summary> - /// Gets or sets a value indicating whether stateless mode is used. + /// Gets or sets the form name to use for this input field. /// </summary> - [Bindable(true), DefaultValue(StatelessDefault), Category(BehaviorCategory)] - [Description("Controls whether stateless mode is used.")] - public bool Stateless { - get { return (bool)(ViewState[StatelessViewStateKey] ?? StatelessDefault); } - set { ViewState[StatelessViewStateKey] = value; } + [Bindable(true), DefaultValue(NameDefault), Category(BehaviorCategory)] + [Description("The form name of this input field.")] + public string Name { + get { return (string)(this.ViewState[NameViewStateKey] ?? NameDefault); } + set { this.ViewState[NameViewStateKey] = value; } } /// <summary> @@ -477,9 +296,9 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { /// </summary> [Bindable(true), DefaultValue(CssClassDefault), Category(AppearanceCategory)] [Description("The CSS class assigned to the text box.")] - public override string CssClass { - get { return this.WrappedTextBox.CssClass; } - set { this.WrappedTextBox.CssClass = value; } + public string CssClass { + get { return (string)this.ViewState[CssClassViewStateKey]; } + set { this.ViewState[CssClassViewStateKey] = value; } } /// <summary> @@ -503,25 +322,13 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { } /// <summary> - /// Gets or sets a value indicating whether to send a persistent cookie upon successful - /// login so the user does not have to log in upon returning to this site. - /// </summary> - [Bindable(true), DefaultValue(UsePersistentCookieDefault), Category(BehaviorCategory)] - [Description("Whether to send a persistent cookie upon successful " + - "login so the user does not have to log in upon returning to this site.")] - public virtual bool UsePersistentCookie { - get { return (bool)(this.ViewState[UsePersistentCookieViewStateKey] ?? UsePersistentCookieDefault); } - set { this.ViewState[UsePersistentCookieViewStateKey] = value; } - } - - /// <summary> /// Gets or sets the width of the text box in characters. /// </summary> [Bindable(true), DefaultValue(ColumnsDefault), Category(AppearanceCategory)] [Description("The width of the text box in characters.")] public int Columns { - get { return this.WrappedTextBox.Columns; } - set { this.WrappedTextBox.Columns = value; } + get { return (int)(this.ViewState[ColumnsViewStateKey] ?? ColumnsDefault); } + set { this.ViewState[ColumnsViewStateKey] = value; } } /// <summary> @@ -530,8 +337,8 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { [Bindable(true), DefaultValue(MaxLengthDefault), Category(AppearanceCategory)] [Description("The maximum number of characters the browser should allow.")] public int MaxLength { - get { return this.WrappedTextBox.MaxLength; } - set { this.WrappedTextBox.MaxLength = value; } + get { return (int)(this.ViewState[MaxLengthViewStateKey] ?? MaxLengthDefault); } + set { this.ViewState[MaxLengthViewStateKey] = value; } } /// <summary> @@ -546,9 +353,21 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { /// </exception> [Bindable(true), DefaultValue(TabIndexDefault), Category(BehaviorCategory)] [Description("The tab index of the text box control.")] - public override short TabIndex { - get { return this.WrappedTextBox.TabIndex; } - set { this.WrappedTextBox.TabIndex = value; } + 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> @@ -673,501 +492,120 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { set { ViewState[EnableRequestProfileViewStateKey] = value; } } - /// <summary> - /// Gets or sets a value indicating whether to enforce on high security mode, - /// which requires the full authentication pipeline to be protected by SSL. - /// </summary> - [Bindable(true), DefaultValue(RequireSslDefault), Category(BehaviorCategory)] - [Description("Turns on high security mode, requiring the full authentication pipeline to be protected by SSL.")] - public bool RequireSsl { - get { return (bool)(ViewState[RequireSslViewStateKey] ?? RequireSslDefault); } - set { ViewState[RequireSslViewStateKey] = value; } - } - - /// <summary> - /// Gets or sets the type of the custom application store to use, or <c>null</c> to use the default. - /// </summary> - /// <remarks> - /// If set, this property must be set in each Page Load event - /// as it is not persisted across postbacks. - /// </remarks> - public IRelyingPartyApplicationStore CustomApplicationStore { get; set; } - #endregion - #region Properties to hide + #region IPostBackDataHandler Members /// <summary> - /// Gets or sets the foreground color (typically the color of the text) of the Web server control. + /// When implemented by a class, processes postback data for an ASP.NET server control. /// </summary> + /// <param name="postDataKey">The key identifier for the control.</param> + /// <param name="postCollection">The collection of all incoming name values.</param> /// <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"/>. + /// true if the server control's state changes as a result of the postback; otherwise, false. /// </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(); } + bool IPostBackDataHandler.LoadPostData(string postDataKey, NameValueCollection postCollection) { + return this.LoadPostData(postDataKey, postCollection); } /// <summary> - /// Gets or sets the background color of the Web server control. + /// 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> - /// 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. - /// </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(); } + void IPostBackDataHandler.RaisePostDataChangedEvent() { + this.RaisePostDataChangedEvent(); } #endregion /// <summary> - /// Gets or sets the <see cref="OpenIdRelyingParty"/> instance to use. - /// </summary> - /// <value>The default value is an <see cref="OpenIdRelyingParty"/> instance initialized according to the web.config file.</value> - /// <remarks> - /// A performance optimization would be to store off the - /// instance as a static member in your web site and set it - /// to this property in your <see cref="Control.Load">Page.Load</see> - /// event since instantiating these instances can be expensive on - /// heavily trafficked web pages. - /// </remarks> - public OpenIdRelyingParty RelyingParty { - get { - if (this.relyingParty == null) { - this.relyingParty = this.CreateRelyingParty(); - } - return this.relyingParty; - } - - set { - this.relyingParty = value; - } - } - - /// <summary> - /// Gets the <see cref="TextBox"/> control that this control wraps. - /// </summary> - protected TextBox WrappedTextBox { - get { return this.wrappedTextBox; } - } - - /// <summary> - /// Gets or sets a value indicating whether the text box should - /// receive input focus when the web page appears. - /// </summary> - protected bool ShouldBeFocused { get; set; } - - /// <summary> - /// Gets or sets the OpenID authentication request that is about to be sent. - /// </summary> - protected IAuthenticationRequest Request { get; set; } - - /// <summary> - /// Sets the input focus to start on the text box when the page appears - /// in the user's browser. - /// </summary> - public override void Focus() { - if (Controls.Count == 0) { - this.ShouldBeFocused = true; - } else { - this.WrappedTextBox.Focus(); - } - } - - /// <summary> - /// Constructs the authentication request and returns it. - /// </summary> - /// <returns>The instantiated authentication request.</returns> - /// <remarks> - /// <para>This method need not be called before calling the <see cref="LogOn"/> method, - /// but is offered in the event that adding extensions to the request is desired.</para> - /// <para>The Simple Registration extension arguments are added to the request - /// before returning if <see cref="EnableRequestProfile"/> is set to true.</para> - /// </remarks> - [SuppressMessage("Microsoft.Usage", "CA2234:PassSystemUriObjectsInsteadOfStrings", Justification = "Uri(Uri, string) accepts second arguments that Uri(Uri, new Uri(string)) does not that we must support.")] - public IAuthenticationRequest CreateRequest() { - ErrorUtilities.VerifyOperation(this.Request == null, OpenIdStrings.CreateRequestAlreadyCalled); - ErrorUtilities.VerifyOperation(!string.IsNullOrEmpty(this.Text), OpenIdStrings.OpenIdTextBoxEmpty); - - try { - // Approximate the returnTo (either based on the customize property or the page URL) - // so we can use it to help with Realm resolution. - var requestContext = this.RelyingParty.Channel.GetRequestFromContext(); - Uri returnToApproximation = this.ReturnToUrl != null ? new Uri(requestContext.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. - Identifier userSuppliedIdentifier; - if (Identifier.TryParse(this.Text, out userSuppliedIdentifier)) { - Realm typedRealm = new Realm(realm); - if (string.IsNullOrEmpty(this.ReturnToUrl)) { - this.Request = this.RelyingParty.CreateRequest(userSuppliedIdentifier, typedRealm); - } else { - // Since the user actually gave us a return_to value, - // the "approximation" is exactly what we want. - this.Request = this.RelyingParty.CreateRequest(userSuppliedIdentifier, typedRealm, returnToApproximation); - } - - if (this.EnableRequestProfile) { - this.AddProfileArgs(this.Request); - } - - if (this.IsPopupAppropriate()) { - // Inform the OP that it will appear in a popup window. - this.Request.AddExtension(new UIRequest()); - } - - // Add state that needs to survive across the redirect. - if (!this.Stateless) { - this.Request.AddCallbackArguments(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; - } - } catch (ProtocolException ex) { - this.OnFailed(new FailedAuthenticationResponse(ex)); - } - - return this.Request; - } - - /// <summary> - /// Immediately redirects to the OpenID Provider to verify the Identifier - /// provided in the text box. - /// </summary> - public void LogOn() { - if (this.Request == null) { - this.CreateRequest(); // sets this.Request - } - - if (this.Request != null) { - if (this.IsPopupAppropriate()) { - this.ScriptPopupWindow(); - } else { - this.Request.RedirectToProvider(); - } - } - } - - /// <summary> - /// Enables a server control to perform final clean up before it is released from memory. - /// </summary> - [SuppressMessage("Microsoft.Design", "CA1063:ImplementIDisposableCorrectly", Justification = "Base class doesn't implement virtual Dispose(bool), so we must call its Dispose() method.")] - public sealed override void Dispose() { - this.Dispose(true); - base.Dispose(); - GC.SuppressFinalize(this); - } - - /// <summary> - /// Creates the text box control. - /// </summary> - protected override void CreateChildControls() { - base.CreateChildControls(); - - this.Controls.Add(this.wrappedTextBox); - if (this.ShouldBeFocused) { - this.WrappedTextBox.Focus(); - } - } - - /// <summary> - /// Initializes the text box control. - /// </summary> - protected virtual void InitializeControls() { - this.wrappedTextBox = new TextBox(); - this.wrappedTextBox.ID = "wrappedTextBox"; - this.wrappedTextBox.CssClass = CssClassDefault; - this.wrappedTextBox.Columns = ColumnsDefault; - this.wrappedTextBox.Text = this.text; - this.wrappedTextBox.TabIndex = TabIndexDefault; - } - - /// <summary> /// Checks for incoming OpenID authentication responses and fires appropriate events. /// </summary> /// <param name="e">The <see cref="T:System.EventArgs"/> object that contains the event data.</param> protected override void OnLoad(EventArgs e) { - base.OnLoad(e); - - if (!Enabled || Page.IsPostBack) { + if (!this.Enabled) { return; } - // Take an unreliable sneek peek to see if we're in a popup and an OpenID - // assertion is coming in. We shouldn't process assertions in a popup window. - if (this.Page.Request.QueryString[UIPopupCallbackKey] == "1" && this.Page.Request.QueryString[UIPopupCallbackParentKey] == null) { - // We're in a popup window. We need to close it and pass the - // message back to the parent window for processing. - this.ScriptClosingPopup(); - return; // don't do any more processing on it now - } - - var response = this.RelyingParty.GetResponse(); - if (response != null) { - string persistentString = response.GetCallbackArgument(UsePersistentCookieCallbackKey); - bool persistentBool; - if (persistentString != null && bool.TryParse(persistentString, out persistentBool)) { - this.UsePersistentCookie = persistentBool; - } - - switch (response.Status) { - case AuthenticationStatus.Canceled: - this.OnCanceled(response); - break; - case AuthenticationStatus.Authenticated: - this.OnLoggedIn(response); - break; - case AuthenticationStatus.SetupRequired: - this.OnSetupRequired(response); - break; - case AuthenticationStatus.Failed: - this.OnFailed(response); - break; - 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); - } - } + this.Page.RegisterRequiresPostBack(this); + base.OnLoad(e); } /// <summary> - /// Prepares the text box to be rendered. + /// 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="e">An <see cref="T:System.EventArgs"/> object that contains the event data.</param> - protected override void OnPreRender(EventArgs e) { - base.OnPreRender(e); - + /// <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) { if (this.ShowLogo) { string logoUrl = Page.ClientScript.GetWebResourceUrl( typeof(OpenIdTextBox), EmbeddedLogoResourceName); - this.WrappedTextBox.Style[HtmlTextWriterStyle.BackgroundImage] = string.Format( - CultureInfo.InvariantCulture, "url({0})", HttpUtility.HtmlEncode(logoUrl)); - this.WrappedTextBox.Style["background-repeat"] = "no-repeat"; - this.WrappedTextBox.Style["background-position"] = "0 50%"; - this.WrappedTextBox.Style[HtmlTextWriterStyle.PaddingLeft] = "18px"; + writer.AddStyleAttribute( + HtmlTextWriterStyle.BackgroundImage, + string.Format(CultureInfo.InvariantCulture, "url({0})", HttpUtility.HtmlEncode(logoUrl))); + writer.AddStyleAttribute("background-repeat", "no-repeat"); + writer.AddStyleAttribute("background-position", "0 50%"); + writer.AddStyleAttribute(HtmlTextWriterStyle.PaddingLeft, "18px"); } if (this.PresetBorder) { - this.WrappedTextBox.Style[HtmlTextWriterStyle.BorderStyle] = "solid"; - this.WrappedTextBox.Style[HtmlTextWriterStyle.BorderWidth] = "1px"; - this.WrappedTextBox.Style[HtmlTextWriterStyle.BorderColor] = "lightgray"; + writer.AddStyleAttribute(HtmlTextWriterStyle.BorderStyle, "solid"); + writer.AddStyleAttribute(HtmlTextWriterStyle.BorderWidth, "1px"); + writer.AddStyleAttribute(HtmlTextWriterStyle.BorderColor, "lightgray"); } - } - /// <summary> - /// Releases unmanaged and - optionally - managed resources - /// </summary> - /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param> - protected virtual void Dispose(bool disposing) { - if (disposing) { - if (this.relyingParty != null) { - this.relyingParty.Dispose(); - this.relyingParty = null; - } + if (!string.IsNullOrEmpty(this.CssClass)) { + writer.AddAttribute(HtmlTextWriterAttribute.Class, this.CssClass); } - } - - #region Events - - /// <summary> - /// Fires the <see cref="LoggedIn"/> event. - /// </summary> - /// <param name="response">The response.</param> - protected virtual void OnLoggedIn(IAuthenticationResponse response) { - ErrorUtilities.VerifyArgumentNotNull(response, "response"); - ErrorUtilities.VerifyInternal(response.Status == AuthenticationStatus.Authenticated, "Firing OnLoggedIn event without an authenticated response."); - var loggedIn = this.LoggedIn; - OpenIdEventArgs args = new OpenIdEventArgs(response); - if (loggedIn != null) { - loggedIn(this, args); - } + writer.AddAttribute(HtmlTextWriterAttribute.Id, this.ClientID); + writer.AddAttribute(HtmlTextWriterAttribute.Name, HttpUtility.HtmlEncode(this.Name)); + writer.AddAttribute(HtmlTextWriterAttribute.Type, "text"); + writer.AddAttribute(HtmlTextWriterAttribute.Size, this.Columns.ToString(CultureInfo.InvariantCulture)); + writer.AddAttribute(HtmlTextWriterAttribute.Value, HttpUtility.HtmlEncode(this.Text)); + writer.AddAttribute(HtmlTextWriterAttribute.Tabindex, this.TabIndex.ToString(CultureInfo.CurrentCulture)); - if (!args.Cancel) { - FormsAuthentication.RedirectFromLoginPage(response.ClaimedIdentifier, this.UsePersistentCookie); - } + writer.RenderBeginTag(HtmlTextWriterTag.Input); + writer.RenderEndTag(); } /// <summary> - /// Fires the <see cref="Failed"/> event. + /// When implemented by a class, processes postback data for an ASP.NET server control. /// </summary> - /// <param name="response">The response.</param> - protected virtual void OnFailed(IAuthenticationResponse response) { - ErrorUtilities.VerifyArgumentNotNull(response, "response"); - ErrorUtilities.VerifyInternal(response.Status == AuthenticationStatus.Failed, "Firing Failed event for the wrong response type."); - - var failed = this.Failed; - if (failed != null) { - failed(this, new OpenIdEventArgs(response)); + /// <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 false; } /// <summary> - /// Fires the <see cref="Canceled"/> event. + /// 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="response">The response.</param> - protected virtual void OnCanceled(IAuthenticationResponse response) { - ErrorUtilities.VerifyArgumentNotNull(response, "response"); - ErrorUtilities.VerifyInternal(response.Status == AuthenticationStatus.Canceled, "Firing Canceled event for the wrong response type."); - - var canceled = this.Canceled; - if (canceled != null) { - canceled(this, new OpenIdEventArgs(response)); - } + protected virtual void RaisePostDataChangedEvent() { + this.OnTextChanged(); } /// <summary> - /// Fires the <see cref="SetupRequired"/> event. + /// Called on a postback when the Text property has changed. /// </summary> - /// <param name="response">The response.</param> - protected virtual void OnSetupRequired(IAuthenticationResponse response) { - ErrorUtilities.VerifyArgumentNotNull(response, "response"); - ErrorUtilities.VerifyInternal(response.Status == AuthenticationStatus.SetupRequired, "Firing SetupRequired event for the wrong response type."); - - // Why are we firing Failed when we're OnSetupRequired? Backward compatibility. - var setupRequired = this.SetupRequired; - if (setupRequired != null) { - setupRequired(this, new OpenIdEventArgs(response)); + protected virtual void OnTextChanged() { + EventHandler textChanged = this.TextChanged; + if (textChanged != null) { + textChanged(this, EventArgs.Empty); } } - #endregion - /// <summary> /// Adds extensions to a given authentication request to ask the Provider /// for user profile data. @@ -1190,73 +628,5 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { null : new Uri(this.RelyingParty.Channel.GetRequestFromContext().UrlBeforeRewriting, this.Page.ResolveUrl(this.PolicyUrl)), }); } - - /// <summary> - /// Creates the relying party instance used to generate authentication requests. - /// </summary> - /// <returns>The instantiated relying party.</returns> - private OpenIdRelyingParty CreateRelyingParty() { - // If we're in stateful mode, first use the explicitly given one on this control if there - // is one. Then try the configuration file specified one. Finally, use the default - // in-memory one that's built into OpenIdRelyingParty. - IRelyingPartyApplicationStore store = this.Stateless ? null : - (this.CustomApplicationStore ?? DotNetOpenAuthSection.Configuration.OpenId.RelyingParty.ApplicationStore.CreateInstance(OpenIdRelyingParty.HttpApplicationStore)); - var rp = new OpenIdRelyingParty(store); - - // Only set RequireSsl to true, as we don't want to override - // a .config setting of true with false. - if (this.RequireSsl) { - rp.SecuritySettings.RequireSsl = true; - } - return rp; - } - - /// <summary> - /// Detects whether a popup window should be used to show the Provider's UI - /// and applies the UI extension to the request when appropriate. - /// </summary> - /// <returns><c>true</c> if a popup should be used; <c>false</c> otherwise.</returns> - private bool IsPopupAppropriate() { - Contract.Requires(this.Request != null); - - return this.Popup == PopupBehavior.Always || this.Request.Provider.IsExtensionSupported<UIRequest>(); - } - - /// <summary> - /// Wires the return page to immediately display a popup window with the Provider in it. - /// </summary> - private void ScriptPopupWindow() { - Contract.Requires(this.Request != null); - Contract.Requires(this.RelyingParty != null); - - this.Request.AddCallbackArguments(UIPopupCallbackKey, "1"); - - StringBuilder startupScript = new StringBuilder(); - - // Add a callback function that the popup window can call on this, the - // parent window, to pass back the authentication result. - startupScript.AppendLine("window.dnoa_internal = new Object();"); - startupScript.AppendLine("window.dnoa_internal.processAuthorizationResult = function(uri) { window.location = uri; };"); - - // Open the popup window. - startupScript.AppendFormat( - @"var openidPopup = {0}", - UIUtilities.GetWindowPopupScript(this.RelyingParty, this.Request, "openidPopup")); - - this.Page.ClientScript.RegisterStartupScript(this.GetType(), "loginPopup", startupScript.ToString(), true); - } - - /// <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();"); - - // We're referencing the OpenIdRelyingPartyControlBase type here to avoid double-registering this script - // if the other control exists on the page. - this.Page.ClientScript.RegisterStartupScript(typeof(OpenIdRelyingPartyControlBase), "loginPopupClose", startupScript.ToString(), true); - } } } diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/PositiveAuthenticationResponse.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/PositiveAuthenticationResponse.cs index 69a6eaa..c6ab095 100644 --- a/src/DotNetOpenAuth/OpenId/RelyingParty/PositiveAuthenticationResponse.cs +++ b/src/DotNetOpenAuth/OpenId/RelyingParty/PositiveAuthenticationResponse.cs @@ -19,17 +19,6 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { [DebuggerDisplay("Status: {Status}, ClaimedIdentifier: {ClaimedIdentifier}")] internal class PositiveAuthenticationResponse : PositiveAnonymousResponse { /// <summary> - /// The OpenID service endpoint reconstructed from the assertion message. - /// </summary> - /// <remarks> - /// This information is straight from the Provider, and therefore must not - /// be trusted until verified as matching the discovery information for - /// the claimed identifier to avoid a Provider asserting an Identifier - /// for which it has no authority. - /// </remarks> - private readonly ServiceEndpoint endpoint; - - /// <summary> /// Initializes a new instance of the <see cref="PositiveAuthenticationResponse"/> class. /// </summary> /// <param name="response">The positive assertion response that was just received by the Relying Party.</param> @@ -39,7 +28,7 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { Contract.Requires(relyingParty != null); ErrorUtilities.VerifyArgumentNotNull(relyingParty, "relyingParty"); - this.endpoint = ServiceEndpoint.CreateForClaimedIdentifier( + this.Endpoint = ServiceEndpoint.CreateForClaimedIdentifier( this.Response.ClaimedIdentifier, this.Response.GetReturnToArgument(AuthenticationRequest.UserSuppliedIdentifierParameterName), this.Response.LocalIdentifier, @@ -69,7 +58,7 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { /// </para> /// </remarks> public override Identifier ClaimedIdentifier { - get { return this.endpoint.ClaimedIdentifier; } + get { return this.Endpoint.ClaimedIdentifier; } } /// <summary> @@ -102,7 +91,7 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { /// </para> /// </remarks> public override string FriendlyIdentifierForDisplay { - get { return this.endpoint.FriendlyIdentifierForDisplay; } + get { return this.Endpoint.FriendlyIdentifierForDisplay; } } /// <summary> @@ -115,6 +104,17 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { #endregion /// <summary> + /// Gets the OpenID service endpoint reconstructed from the assertion message. + /// </summary> + /// <remarks> + /// This information is straight from the Provider, and therefore must not + /// be trusted until verified as matching the discovery information for + /// the claimed identifier to avoid a Provider asserting an Identifier + /// for which it has no authority. + /// </remarks> + internal ServiceEndpoint Endpoint { get; private set; } + + /// <summary> /// Gets the positive assertion response message. /// </summary> protected internal new PositiveAssertionResponse Response { @@ -155,9 +155,9 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { // is merely a hint that must be verified by performing discovery again here. var discoveryResults = claimedId.Discover(relyingParty.WebRequestHandler); ErrorUtilities.VerifyProtocol( - discoveryResults.Contains(this.endpoint), + discoveryResults.Contains(this.Endpoint), OpenIdStrings.IssuedAssertionFailsIdentifierDiscovery, - this.endpoint, + this.Endpoint, discoveryResults.ToStringDeferred(true)); } } 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; diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/ServiceEndpoint.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/ServiceEndpoint.cs index 88264f5..fc9a24a 100644 --- a/src/DotNetOpenAuth/OpenId/RelyingParty/ServiceEndpoint.cs +++ b/src/DotNetOpenAuth/OpenId/RelyingParty/ServiceEndpoint.cs @@ -427,7 +427,7 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { /// <summary> /// Creates a <see cref="ServiceEndpoint"/> instance to represent some OP Identifier. /// </summary> - /// <param name="providerIdentifier">The provider identifier.</param> + /// <param name="providerIdentifier">The provider identifier (actually the user-supplied identifier).</param> /// <param name="providerEndpoint">The provider endpoint.</param> /// <param name="servicePriority">The service priority.</param> /// <param name="uriPriority">The URI priority.</param> diff --git a/src/DotNetOpenAuth/OpenId/UriIdentifier.cs b/src/DotNetOpenAuth/OpenId/UriIdentifier.cs index 3cd95b0..4384367 100644 --- a/src/DotNetOpenAuth/OpenId/UriIdentifier.cs +++ b/src/DotNetOpenAuth/OpenId/UriIdentifier.cs @@ -42,7 +42,7 @@ namespace DotNetOpenAuth.OpenId { /// <param name="uri">The value this identifier will represent.</param> /// <param name="requireSslDiscovery">if set to <c>true</c> [require SSL discovery].</param> internal UriIdentifier(string uri, bool requireSslDiscovery) - : base(requireSslDiscovery) { + : base(uri, requireSslDiscovery) { ErrorUtilities.VerifyNonZeroLength(uri, "uri"); Uri canonicalUri; bool schemePrepended; @@ -69,7 +69,7 @@ namespace DotNetOpenAuth.OpenId { /// <param name="uri">The value this identifier will represent.</param> /// <param name="requireSslDiscovery">if set to <c>true</c> [require SSL discovery].</param> internal UriIdentifier(Uri uri, bool requireSslDiscovery) - : base(requireSslDiscovery) { + : base(uri != null ? uri.OriginalString : null, requireSslDiscovery) { ErrorUtilities.VerifyArgumentNotNull(uri, "uri"); if (!TryCanonicalize(new UriBuilder(uri), out uri)) { throw new UriFormatException(); diff --git a/src/DotNetOpenAuth/OpenId/XriIdentifier.cs b/src/DotNetOpenAuth/OpenId/XriIdentifier.cs index a85c33c..28008a8 100644 --- a/src/DotNetOpenAuth/OpenId/XriIdentifier.cs +++ b/src/DotNetOpenAuth/OpenId/XriIdentifier.cs @@ -74,7 +74,7 @@ namespace DotNetOpenAuth.OpenId { /// only succeed if it can be done entirely using SSL. /// </param> internal XriIdentifier(string xri, bool requireSsl) - : base(requireSsl) { + : base(xri, requireSsl) { Contract.Requires((xri != null && xri.Length > 0) || !string.IsNullOrEmpty(xri)); ErrorUtilities.VerifyFormat(IsValidXri(xri), OpenIdStrings.InvalidXri, xri); Contract.Assume(xri != null); // Proven by IsValidXri |