summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAndrew Arnott <andrewarnott@gmail.com>2009-01-24 17:50:09 -0800
committerAndrew <andrewarnott@gmail.com>2009-01-24 17:50:09 -0800
commit7af1c64b1ad3b841d872346918e65ec0b6d55082 (patch)
tree467ac4cb5c00f58cada09065354e96ccd832d4a7
parent618faa5fb7be445a0d5f0ceac4926cd2a113fae4 (diff)
downloadDotNetOpenAuth-7af1c64b1ad3b841d872346918e65ec0b6d55082.zip
DotNetOpenAuth-7af1c64b1ad3b841d872346918e65ec0b6d55082.tar.gz
DotNetOpenAuth-7af1c64b1ad3b841d872346918e65ec0b6d55082.tar.bz2
Added the OpenIdAjaxTextBox control.
-rw-r--r--src/DotNetOpenAuth.Test/Messaging/HttpRequestInfoTests.cs6
-rw-r--r--src/DotNetOpenAuth/DotNetOpenAuth.csproj9
-rw-r--r--src/DotNetOpenAuth/Messaging/HttpRequestInfo.cs1
-rw-r--r--src/DotNetOpenAuth/Messaging/MessagingUtilities.cs16
-rw-r--r--src/DotNetOpenAuth/OpenId/Extensions/IClientScriptExtensionResponse.cs33
-rw-r--r--src/DotNetOpenAuth/OpenId/OpenIdStrings.Designer.cs18
-rw-r--r--src/DotNetOpenAuth/OpenId/OpenIdStrings.resx6
-rw-r--r--src/DotNetOpenAuth/OpenId/RelyingParty/AuthenticationRequest.cs13
-rw-r--r--src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdAjaxTextBox.cs1248
-rw-r--r--src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdAjaxTextBox.js737
-rw-r--r--src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingParty.cs949
-rw-r--r--src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdTextBox.cs2
-rw-r--r--src/DotNetOpenAuth/OpenId/RelyingParty/PositiveAuthenticationResponse.cs14
-rw-r--r--src/DotNetOpenAuth/OpenId/RelyingParty/login_failure.pngbin0 -> 714 bytes
-rw-r--r--src/DotNetOpenAuth/OpenId/RelyingParty/login_success (lock).pngbin0 -> 571 bytes
-rw-r--r--src/DotNetOpenAuth/OpenId/RelyingParty/login_success.pngbin0 -> 464 bytes
-rw-r--r--src/DotNetOpenAuth/OpenId/RelyingParty/spinner.gifbin0 -> 725 bytes
17 files changed, 2583 insertions, 469 deletions
diff --git a/src/DotNetOpenAuth.Test/Messaging/HttpRequestInfoTests.cs b/src/DotNetOpenAuth.Test/Messaging/HttpRequestInfoTests.cs
index 5333f97..971495f 100644
--- a/src/DotNetOpenAuth.Test/Messaging/HttpRequestInfoTests.cs
+++ b/src/DotNetOpenAuth.Test/Messaging/HttpRequestInfoTests.cs
@@ -12,6 +12,12 @@ namespace DotNetOpenAuth.Test.Messaging {
[TestClass]
public class HttpRequestInfoTests : TestBase {
[TestMethod]
+ public void CtorDefault() {
+ HttpRequestInfo info = new HttpRequestInfo();
+ Assert.AreEqual("GET", info.HttpMethod);
+ }
+
+ [TestMethod]
public void CtorRequest() {
HttpRequest request = new HttpRequest("file", "http://someserver?a=b", "a=b");
////request.Headers["headername"] = "headervalue"; // PlatformNotSupportedException prevents us mocking this up
diff --git a/src/DotNetOpenAuth/DotNetOpenAuth.csproj b/src/DotNetOpenAuth/DotNetOpenAuth.csproj
index 71bc702..0a4d79d 100644
--- a/src/DotNetOpenAuth/DotNetOpenAuth.csproj
+++ b/src/DotNetOpenAuth/DotNetOpenAuth.csproj
@@ -196,6 +196,7 @@
<Compile Include="OpenId\Extensions\AttributeExchange\WellKnownAttributes.cs" />
<Compile Include="OpenId\Extensions\ExtensionBase.cs" />
<Compile Include="OpenId\Extensions\ExtensionArgumentsManager.cs" />
+ <Compile Include="OpenId\Extensions\IClientScriptExtensionResponse.cs" />
<Compile Include="OpenId\Extensions\OpenIdExtensionFactory.cs" />
<Compile Include="OpenId\Extensions\ProviderAuthenticationPolicy\AuthenticationPolicies.cs" />
<Compile Include="OpenId\Extensions\ProviderAuthenticationPolicy\Constants.cs" />
@@ -264,6 +265,7 @@
<Compile Include="OpenId\RelyingParty\AuthenticationRequest.cs" />
<Compile Include="OpenId\RelyingParty\AuthenticationRequestMode.cs" />
<Compile Include="OpenId\RelyingParty\NegativeAuthenticationResponse.cs" />
+ <Compile Include="OpenId\RelyingParty\OpenIdAjaxTextBox.cs" />
<Compile Include="OpenId\RelyingParty\OpenIdEventArgs.cs" />
<Compile Include="OpenId\RelyingParty\OpenIdLogin.cs" />
<Compile Include="OpenId\RelyingParty\OpenIdTextBox.cs" />
@@ -357,6 +359,13 @@
<ItemGroup>
<EmbeddedResource Include="OpenId\RelyingParty\openid_login.gif" />
</ItemGroup>
+ <ItemGroup>
+ <EmbeddedResource Include="OpenId\RelyingParty\login_failure.png" />
+ <EmbeddedResource Include="OpenId\RelyingParty\login_success %28lock%29.png" />
+ <EmbeddedResource Include="OpenId\RelyingParty\login_success.png" />
+ <EmbeddedResource Include="OpenId\RelyingParty\OpenIdAjaxTextBox.js" />
+ <EmbeddedResource Include="OpenId\RelyingParty\spinner.gif" />
+ </ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<Import Project="..\..\tools\DotNetOpenAuth.Versioning.targets" />
</Project> \ No newline at end of file
diff --git a/src/DotNetOpenAuth/Messaging/HttpRequestInfo.cs b/src/DotNetOpenAuth/Messaging/HttpRequestInfo.cs
index e498c6f..cb446e7 100644
--- a/src/DotNetOpenAuth/Messaging/HttpRequestInfo.cs
+++ b/src/DotNetOpenAuth/Messaging/HttpRequestInfo.cs
@@ -36,6 +36,7 @@ namespace DotNetOpenAuth.Messaging {
/// Initializes a new instance of the <see cref="HttpRequestInfo"/> class.
/// </summary>
internal HttpRequestInfo() {
+ this.HttpMethod = "GET";
}
/// <summary>
diff --git a/src/DotNetOpenAuth/Messaging/MessagingUtilities.cs b/src/DotNetOpenAuth/Messaging/MessagingUtilities.cs
index 18b06e1..6c40fba 100644
--- a/src/DotNetOpenAuth/Messaging/MessagingUtilities.cs
+++ b/src/DotNetOpenAuth/Messaging/MessagingUtilities.cs
@@ -70,6 +70,22 @@ namespace DotNetOpenAuth.Messaging {
}
/// <summary>
+ /// Gets the query or form data from the original request (before any URL rewriting has occurred.)
+ /// </summary>
+ /// <returns>A set of name=value pairs.</returns>
+ public static NameValueCollection GetQueryOrFormFromContext() {
+ ErrorUtilities.VerifyHttpContext();
+ HttpRequest request = HttpContext.Current.Request;
+ NameValueCollection query;
+ if (request.RequestType == "GET") {
+ query = GetQueryFromContextNVC();
+ } else {
+ query = request.Form;
+ }
+ return query;
+ }
+
+ /// <summary>
/// Strips any and all URI query parameters that start with some prefix.
/// </summary>
/// <param name="uri">The URI that may have a query with parameters to remove.</param>
diff --git a/src/DotNetOpenAuth/OpenId/Extensions/IClientScriptExtensionResponse.cs b/src/DotNetOpenAuth/OpenId/Extensions/IClientScriptExtensionResponse.cs
new file mode 100644
index 0000000..c84f507
--- /dev/null
+++ b/src/DotNetOpenAuth/OpenId/Extensions/IClientScriptExtensionResponse.cs
@@ -0,0 +1,33 @@
+//-----------------------------------------------------------------------
+// <copyright file="IClientScriptExtensionResponse.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OpenId.Extensions {
+ using System.Collections.Generic;
+ using DotNetOpenAuth.Messaging;
+ using DotNetOpenAuth.OpenId.Messages;
+ using DotNetOpenAuth.OpenId.RelyingParty;
+
+ /// <summary>
+ /// An interface that OpenID extensions can implement to allow authentication response
+ /// messages with included extensions to be processed by Javascript on the user agent.
+ /// </summary>
+ public interface IClientScriptExtensionResponse : IExtensionMessage {
+ /// <summary>
+ /// Reads the extension information on an authentication response from the provider.
+ /// </summary>
+ /// <param name="response">The incoming OpenID response carrying the extension.</param>
+ /// <returns>
+ /// A Javascript snippet that when executed on the user agent returns an object with
+ /// the information deserialized from the extension response.
+ /// </returns>
+ /// <remarks>
+ /// This method is called <b>before</b> the signature on the assertion response has been
+ /// verified. Therefore all information in these fields should be assumed unreliable
+ /// and potentially falsified.
+ /// </remarks>
+ string InitializeJavaScriptData(IProtocolMessageWithExtensions response);
+ }
+}
diff --git a/src/DotNetOpenAuth/OpenId/OpenIdStrings.Designer.cs b/src/DotNetOpenAuth/OpenId/OpenIdStrings.Designer.cs
index 11af056..bd9559c 100644
--- a/src/DotNetOpenAuth/OpenId/OpenIdStrings.Designer.cs
+++ b/src/DotNetOpenAuth/OpenId/OpenIdStrings.Designer.cs
@@ -169,6 +169,24 @@ namespace DotNetOpenAuth.OpenId {
}
/// <summary>
+ /// Looks up a localized string similar to An extension with this property name (&apos;{0}&apos;) has already been registered..
+ /// </summary>
+ internal static string ClientScriptExtensionPropertyNameCollision {
+ get {
+ return ResourceManager.GetString("ClientScriptExtensionPropertyNameCollision", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The extension &apos;{0}&apos; has already been registered..
+ /// </summary>
+ internal static string ClientScriptExtensionTypeCollision {
+ get {
+ return ResourceManager.GetString("ClientScriptExtensionTypeCollision", resourceCulture);
+ }
+ }
+
+ /// <summary>
/// Looks up a localized string similar to An authentication request has already been created using CreateRequest()..
/// </summary>
internal static string CreateRequestAlreadyCalled {
diff --git a/src/DotNetOpenAuth/OpenId/OpenIdStrings.resx b/src/DotNetOpenAuth/OpenId/OpenIdStrings.resx
index 1791e5d..dab82bb 100644
--- a/src/DotNetOpenAuth/OpenId/OpenIdStrings.resx
+++ b/src/DotNetOpenAuth/OpenId/OpenIdStrings.resx
@@ -271,4 +271,10 @@ Discovered endpoint info:
<data name="OpenIdTextBoxEmpty" xml:space="preserve">
<value>No OpenId url is provided.</value>
</data>
+ <data name="ClientScriptExtensionPropertyNameCollision" xml:space="preserve">
+ <value>An extension with this property name ('{0}') has already been registered.</value>
+ </data>
+ <data name="ClientScriptExtensionTypeCollision" xml:space="preserve">
+ <value>The extension '{0}' has already been registered.</value>
+ </data>
</root> \ No newline at end of file
diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/AuthenticationRequest.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/AuthenticationRequest.cs
index ce627dc..f76a5a4 100644
--- a/src/DotNetOpenAuth/OpenId/RelyingParty/AuthenticationRequest.cs
+++ b/src/DotNetOpenAuth/OpenId/RelyingParty/AuthenticationRequest.cs
@@ -156,6 +156,19 @@ namespace DotNetOpenAuth.OpenId.RelyingParty {
get { return this.endpoint.Protocol.Version; }
}
+ #endregion
+
+ /// <summary>
+ /// Gets or sets how an association may or should be created or used
+ /// in the formulation of the authentication request.
+ /// </summary>
+ internal AssociationPreference AssociationPreference {
+ get { return this.associationPreference; }
+ set { this.associationPreference = value; }
+ }
+
+ #region IAuthenticationRequest methods
+
/// <summary>
/// Makes a dictionary of key/value pairs available when the authentication is completed.
/// </summary>
diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdAjaxTextBox.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdAjaxTextBox.cs
new file mode 100644
index 0000000..9bdf2f8
--- /dev/null
+++ b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdAjaxTextBox.cs
@@ -0,0 +1,1248 @@
+//-----------------------------------------------------------------------
+// <copyright file="OpenIdAjaxTextBox.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+[assembly: System.Web.UI.WebResource(DotNetOpenAuth.OpenId.RelyingParty.OpenIdAjaxTextBox.EmbeddedScriptResourceName, "text/javascript")]
+[assembly: System.Web.UI.WebResource(DotNetOpenAuth.OpenId.RelyingParty.OpenIdAjaxTextBox.EmbeddedDotNetOpenIdLogoResourceName, "image/gif")]
+[assembly: System.Web.UI.WebResource(DotNetOpenAuth.OpenId.RelyingParty.OpenIdAjaxTextBox.EmbeddedSpinnerResourceName, "image/gif")]
+[assembly: System.Web.UI.WebResource(DotNetOpenAuth.OpenId.RelyingParty.OpenIdAjaxTextBox.EmbeddedLoginSuccessResourceName, "image/png")]
+[assembly: System.Web.UI.WebResource(DotNetOpenAuth.OpenId.RelyingParty.OpenIdAjaxTextBox.EmbeddedLoginFailureResourceName, "image/png")]
+
+#pragma warning disable 0809 // marking inherited, unsupported properties as obsolete to discourage their use
+
+namespace DotNetOpenAuth.OpenId.RelyingParty {
+ using System;
+ using System.Collections.Generic;
+ using System.Collections.Specialized;
+ using System.ComponentModel;
+ using System.Diagnostics;
+ using System.Diagnostics.CodeAnalysis;
+ 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;
+
+ /// <summary>
+ /// An ASP.NET control that provides a minimal text box that is OpenID-aware and uses AJAX for
+ /// a premium login experience.
+ /// </summary>
+ [DefaultProperty("Text"), ValidationProperty("Text")]
+ [ToolboxData("<{0}:OpenIdAjaxTextBox runat=\"server\" />")]
+ public class OpenIdAjaxTextBox : WebControl, ICallbackEventHandler {
+ /// <summary>
+ /// The name of the manifest stream containing the OpenIdAjaxTextBox.js file.
+ /// </summary>
+ internal const string EmbeddedScriptResourceName = Util.DefaultNamespace + ".OpenId.RelyingParty.OpenIdAjaxTextBox.js";
+
+ /// <summary>
+ /// The name of the manifest stream containing the dotnetopenid_16x16.gif file.
+ /// </summary>
+ internal const string EmbeddedDotNetOpenIdLogoResourceName = Util.DefaultNamespace + ".OpenId.RelyingParty.dotnetopenid_16x16.gif";
+
+ /// <summary>
+ /// The name of the manifest stream containing the spinner.gif file.
+ /// </summary>
+ internal const string EmbeddedSpinnerResourceName = Util.DefaultNamespace + ".OpenId.RelyingParty.spinner.gif";
+
+ /// <summary>
+ /// The name of the manifest stream containing the login_success.png file.
+ /// </summary>
+ internal const string EmbeddedLoginSuccessResourceName = Util.DefaultNamespace + ".OpenId.RelyingParty.login_success.png";
+
+ /// <summary>
+ /// The name of the manifest stream containing the login_failure.png file.
+ /// </summary>
+ internal const string EmbeddedLoginFailureResourceName = Util.DefaultNamespace + ".OpenId.RelyingParty.login_failure.png";
+
+ #region Property viewstate keys
+
+ /// <summary>
+ /// The viewstate key to use for storing the value of the <see cref="Columns"/> property.
+ /// </summary>
+ 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.
+ /// </summary>
+ private const string AuthenticationResponseViewStateKey = "AuthenticationResponse";
+
+ /// <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="AuthenticatedAsToolTip"/> property.
+ /// </summary>
+ private const string AuthenticatedAsToolTipViewStateKey = "AuthenticatedAsToolTip";
+
+ /// <summary>
+ /// The viewstate key to use for storing the value of the <see cref="AuthenticationSucceededToolTip"/> property.
+ /// </summary>
+ 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";
+
+ /// <summary>
+ /// The viewstate key to use for storing the value of the <see cref="AuthenticationFailedToolTip"/> property.
+ /// </summary>
+ private const string AuthenticationFailedToolTipViewStateKey = "AuthenticationFailedToolTip";
+
+ /// <summary>
+ /// The viewstate key to use for storing the value of the <see cref="IdentifierRequiredMessage"/> property.
+ /// </summary>
+ private const string IdentifierRequiredMessageViewStateKey = "BusyToolTip";
+
+ /// <summary>
+ /// The viewstate key to use for storing the value of the <see cref="BusyToolTip"/> property.
+ /// </summary>
+ private const string BusyToolTipViewStateKey = "BusyToolTip";
+
+ /// <summary>
+ /// The viewstate key to use for storing the value of the <see cref="LogOnText"/> property.
+ /// </summary>
+ private const string LogOnTextViewStateKey = "LoginText";
+
+ /// <summary>
+ /// The viewstate key to use for storing the value of the <see cref="Throttle"/> property.
+ /// </summary>
+ private const string ThrottleViewStateKey = "Throttle";
+
+ /// <summary>
+ /// The viewstate key to use for storing the value of the <see cref="LogOnToolTip"/> property.
+ /// </summary>
+ private const string LogOnToolTipViewStateKey = "LoginToolTip";
+
+ /// <summary>
+ /// The viewstate key to use for storing the value of the <see cref="Name"/> property.
+ /// </summary>
+ private const string NameViewStateKey = "Name";
+
+ /// <summary>
+ /// The viewstate key to use for storing the value of the <see cref="Timeout"/> property.
+ /// </summary>
+ private const string TimeoutViewStateKey = "Timeout";
+
+ /// <summary>
+ /// The viewstate key to use for storing the value of the <see cref="Text"/> property.
+ /// </summary>
+ private const string TextViewStateKey = "Text";
+
+ /// <summary>
+ /// The viewstate key to use for storing the value of the <see cref="TabIndex"/> property.
+ /// </summary>
+ private const string TabIndexViewStateKey = "TabIndex";
+
+ /// <summary>
+ /// The viewstate key to use for storing the value of the <see cref="RetryToolTip"/> property.
+ /// </summary>
+ private const string RetryToolTipViewStateKey = "RetryToolTip";
+
+ /// <summary>
+ /// The viewstate key to use for storing the value of the <see cref="RetryText"/> property.
+ /// </summary>
+ private const string RetryTextViewStateKey = "RetryText";
+
+ #endregion
+
+ #region Property defaults
+
+ /// <summary>
+ /// The default value for the <see cref="Columns"/> property.
+ /// </summary>
+ 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.
+ /// </summary>
+ private const string RealmUrlDefault = "~/";
+
+ /// <summary>
+ /// The default value for the <see cref="LogOnInProgressMessage"/> property.
+ /// </summary>
+ private const string LogOnInProgressMessageDefault = "Please wait for login to complete.";
+
+ /// <summary>
+ /// The default value for the <see cref="AuthenticationSucceededToolTip"/> property.
+ /// </summary>
+ private const string AuthenticationSucceededToolTipDefault = "Authenticated by {0}.";
+
+ /// <summary>
+ /// The default value for the <see cref="AuthenticatedAsToolTip"/> property.
+ /// </summary>
+ private const string AuthenticatedAsToolTipDefault = "Authenticated as {0}.";
+
+ /// <summary>
+ /// The default value for the <see cref="AuthenticationFailedToolTip"/> property.
+ /// </summary>
+ private const string AuthenticationFailedToolTipDefault = "Authentication failed.";
+
+ /// <summary>
+ /// The default value for the <see cref="Throttle"/> property.
+ /// </summary>
+ private const int ThrottleDefault = 3;
+
+ /// <summary>
+ /// The default value for the <see cref="LogOnText"/> property.
+ /// </summary>
+ private const string LogOnTextDefault = "LOG IN";
+
+ /// <summary>
+ /// The default value for the <see cref="BusyToolTip"/> property.
+ /// </summary>
+ private const string BusyToolTipDefault = "Discovering/authenticating";
+
+ /// <summary>
+ /// The default value for the <see cref="IdentifierRequiredMessage"/> property.
+ /// </summary>
+ private const string IdentifierRequiredMessageDefault = "Please correct errors in OpenID identifier and allow login to complete before submitting.";
+
+ /// <summary>
+ /// The default value for the <see cref="Name"/> property.
+ /// </summary>
+ private const string NameDefault = "openid_identifier";
+
+ /// <summary>
+ /// Default value for <see cref="TabIndex"/> property.
+ /// </summary>
+ private const short TabIndexDefault = 0;
+
+ /// <summary>
+ /// The default value for the <see cref="RetryToolTip"/> property.
+ /// </summary>
+ private const string RetryToolTipDefault = "Retry a failed identifier discovery.";
+
+ /// <summary>
+ /// The default value for the <see cref="LogOnToolTip"/> property.
+ /// </summary>
+ private const string LogOnToolTipDefault = "Click here to log in using a pop-up window.";
+
+ /// <summary>
+ /// The default value for the <see cref="RetryText"/> property.
+ /// </summary>
+ private const string RetryTextDefault = "RETRY";
+
+ #endregion
+
+ /// <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.
+ /// </summary>
+ [Description("Fired when authentication has completed successfully.")]
+ public event EventHandler<OpenIdEventArgs> LoggedIn;
+
+ /// <summary>
+ /// Gets or sets the client-side script that executes when an authentication
+ /// assertion is received (but before it is verified).
+ /// </summary>
+ /// <remarks>
+ /// <para>In the context of the executing javascript set in this property, the
+ /// local variable <i>sender</i> is set to the openid_identifier input box
+ /// that is executing this code.
+ /// This variable has a getClaimedIdentifier() method that may be used to
+ /// identify the user who is being authenticated.</para>
+ /// <para>It is <b>very</b> important to note that when this code executes,
+ /// 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>
+ /// </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")]
+ public string OnClientAssertionReceived {
+ get { return this.ViewState[OnClientAssertionReceivedViewStateKey] as string; }
+ set { this.ViewState[OnClientAssertionReceivedViewStateKey] = value; }
+ }
+
+ #endregion
+
+ #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 {
+ Url = authUri,
+ };
+ var rp = CreateRelyingParty(true);
+ this.authenticationResponse = rp.GetResponse(clientResponseInfo);
+
+ // Save out the authentication response to viewstate so we can find it on
+ // a subsequent postback.
+ this.ViewState[AuthenticationResponseViewStateKey] = 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.")]
+ public string Text {
+ get { return (string)(this.ViewState[TextViewStateKey] ?? string.Empty); }
+ set { this.ViewState[TextViewStateKey] = value ?? string.Empty; }
+ }
+
+ /// <summary>
+ /// Gets or sets the width of the text box in characters.
+ /// </summary>
+ [Bindable(true), Category("Appearance"), DefaultValue(ColumnsDefault)]
+ [Description("The width of the text box in characters.")]
+ public int Columns {
+ get {
+ return (int)(this.ViewState[ColumnsViewStateKey] ?? ColumnsDefault);
+ }
+
+ set {
+ ErrorUtilities.VerifyArgumentInRange(value >= 0, "value");
+ this.ViewState[ColumnsViewStateKey] = 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)]
+ [Description("The tab index of the text box control. Use 0 to omit an explicit tabindex.")]
+ public override short TabIndex {
+ get { return (short)(this.ViewState[TabIndexViewStateKey] ?? TabIndexDefault); }
+ set { this.ViewState[TabIndexViewStateKey] = value; }
+ }
+
+ /// <summary>
+ /// Gets or sets the HTML name to assign to the text field.
+ /// </summary>
+ [Bindable(true), DefaultValue(NameDefault), Category("Misc")]
+ [Description("The HTML name to assign to the text field.")]
+ public string Name {
+ get {
+ return (string)(this.ViewState[NameViewStateKey] ?? NameDefault);
+ }
+
+ set {
+ ErrorUtilities.VerifyNonZeroLength(value, "value");
+ this.ViewState[NameViewStateKey] = value ?? string.Empty;
+ }
+ }
+
+ /// <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")]
+ [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 {
+ return (TimeSpan)(this.ViewState[TimeoutViewStateKey] ?? TimeoutDefault);
+ }
+
+ set {
+ ErrorUtilities.VerifyArgumentInRange(value.TotalMilliseconds > 0, "value");
+ this.ViewState[TimeoutViewStateKey] = value;
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets the maximum number of OpenID Providers to simultaneously try to authenticate with.
+ /// </summary>
+ [Browsable(true), DefaultValue(ThrottleDefault), Category("Behavior")]
+ [Description("The maximum number of OpenID Providers to simultaneously try to authenticate with.")]
+ public int Throttle {
+ get {
+ return (int)(this.ViewState[ThrottleViewStateKey] ?? ThrottleDefault);
+ }
+
+ set {
+ ErrorUtilities.VerifyArgumentInRange(value > 0, "value");
+ this.ViewState[ThrottleViewStateKey] = value;
+ }
+ }
+
+ /// <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")]
+ [Description("The text that appears on the LOG IN button in cases where immediate (invisible) authentication fails.")]
+ public string LogOnText {
+ get {
+ return (string)(this.ViewState[LogOnTextViewStateKey] ?? LogOnTextDefault);
+ }
+
+ set {
+ ErrorUtilities.VerifyNonZeroLength(value, "value");
+ this.ViewState[LogOnTextViewStateKey] = value ?? string.Empty;
+ }
+ }
+
+ /// <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")]
+ [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); }
+ set { this.ViewState[LogOnToolTipViewStateKey] = value ?? string.Empty; }
+ }
+
+ /// <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")]
+ [Description("The text that appears on the RETRY button in cases where authentication times out.")]
+ public string RetryText {
+ get {
+ return (string)(this.ViewState[RetryTextViewStateKey] ?? RetryTextDefault);
+ }
+
+ set {
+ ErrorUtilities.VerifyNonZeroLength(value, "value");
+ this.ViewState[RetryTextViewStateKey] = value ?? string.Empty;
+ }
+ }
+
+ /// <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")]
+ [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); }
+ set { this.ViewState[RetryToolTipViewStateKey] = value ?? string.Empty; }
+ }
+
+ /// <summary>
+ /// Gets or sets the tool tip text that appears when authentication succeeds.
+ /// </summary>
+ [Bindable(true), DefaultValue(AuthenticationSucceededToolTipDefault), Localizable(true), Category("Appearance")]
+ [Description("The tool tip text that appears when authentication succeeds.")]
+ public string AuthenticationSucceededToolTip {
+ get { return (string)(this.ViewState[AuthenticationSucceededToolTipViewStateKey] ?? AuthenticationSucceededToolTipDefault); }
+ set { this.ViewState[AuthenticationSucceededToolTipViewStateKey] = value ?? string.Empty; }
+ }
+
+ /// <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")]
+ [Description("The tool tip text that appears on the green checkmark when authentication succeeds.")]
+ public string AuthenticatedAsToolTip {
+ get { return (string)(this.ViewState[AuthenticatedAsToolTipViewStateKey] ?? AuthenticatedAsToolTipDefault); }
+ set { this.ViewState[AuthenticatedAsToolTipViewStateKey] = value ?? string.Empty; }
+ }
+
+ /// <summary>
+ /// Gets or sets the tool tip text that appears when authentication fails.
+ /// </summary>
+ [Bindable(true), DefaultValue(AuthenticationFailedToolTipDefault), Localizable(true), Category("Appearance")]
+ [Description("The tool tip text that appears when authentication fails.")]
+ public string AuthenticationFailedToolTip {
+ get { return (string)(this.ViewState[AuthenticationFailedToolTipViewStateKey] ?? AuthenticationFailedToolTipDefault); }
+ set { this.ViewState[AuthenticationFailedToolTipViewStateKey] = value ?? string.Empty; }
+ }
+
+ /// <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")]
+ [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); }
+ set { this.ViewState[BusyToolTipViewStateKey] = value ?? string.Empty; }
+ }
+
+ /// <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")]
+ [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); }
+ set { this.ViewState[IdentifierRequiredMessageViewStateKey] = value ?? string.Empty; }
+ }
+
+ /// <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")]
+ [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 = "DotNetOpenId.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.")]
+ 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(Page, 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.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.")]
+ 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(MessagingUtilities.GetRequestUrlFromContext(), 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, Browsable(false), Bindable(false)]
+ public override bool EnableTheming {
+ get { return false; }
+ set { throw new NotSupportedException(); }
+ }
+
+ #endregion
+
+ /// <summary>
+ /// Gets the default value for the <see cref="Timeout"/> property.
+ /// </summary>
+ /// <value>8 seconds; or eternity if the debugger is attached.</value>
+ private static TimeSpan TimeoutDefault {
+ get {
+ if (Debugger.IsAttached) {
+ Logger.Warn("Debugger is attached. Inflating default OpenIdAjaxTextbox.Timeout value to infinity.");
+ return TimeSpan.MaxValue;
+ } else {
+ return TimeSpan.FromSeconds(8);
+ }
+ }
+ }
+
+ /// <summary>
+ /// Gets the name of the open id auth data form key.
+ /// </summary>
+ /// <value>A concatenation of <see cref="Name"/> and <c>"_openidAuthData"</c>.</value>
+ private string OpenIdAuthDataFormKey {
+ get { return this.Name + "_openidAuthData"; }
+ }
+
+ /// <summary>
+ /// Places focus on the text box when the page is rendered on the browser.
+ /// </summary>
+ public override void Focus() {
+ // we don't emit the code to focus the control immediately, in case the control
+ // is never rendered to the page because its Visible property is false or that
+ // of any of its parent containers.
+ this.focusCalled = true;
+ }
+
+ /// <summary>
+ /// Allows an OpenID extension to read data out of an unverified positive authentication assertion
+ /// and send it down to the client browser so that Javascript running on the page can perform
+ /// some preprocessing on the extension data.
+ /// </summary>
+ /// <typeparam name="T">The extension <i>response</i> type that will read data from the assertion.</typeparam>
+ /// <param name="propertyName">The property name on the openid_identifier input box object that will be used to store the extension data. For example: sreg</param>
+ /// <remarks>
+ /// This method should be called from the <see cref="UnconfirmedPositiveAssertion"/> event handler.
+ /// </remarks>
+ [SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter", Justification = "By design")]
+ public void RegisterClientScriptExtension<T>(string propertyName) where T : IClientScriptExtensionResponse {
+ ErrorUtilities.VerifyNonZeroLength(propertyName, "propertyName");
+ ErrorUtilities.VerifyArgumentNamed(!this.clientScriptExtensions.ContainsValue(propertyName), "propertyName", OpenIdStrings.ClientScriptExtensionPropertyNameCollision, propertyName);
+ foreach (var ext in this.clientScriptExtensions.Keys) {
+ ErrorUtilities.VerifyArgument(ext != typeof(T), OpenIdStrings.ClientScriptExtensionTypeCollision, typeof(T).FullName);
+ }
+ this.clientScriptExtensions.Add(typeof(T), propertyName);
+ }
+
+ #region ICallbackEventHandler Members
+
+ /// <summary>
+ /// Returns the result of discovery on some Identifier passed to <see cref="ICallbackEventHandler.RaiseCallbackEvent"/>.
+ /// </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.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);
+ if (requests.Count > 0) {
+ discoveryResultBuilder.AppendFormat("claimedIdentifier: {0},", Util.GetSafeJavascriptValue(requests[0].ClaimedIdentifier));
+ discoveryResultBuilder.Append("requests: [");
+ foreach (IAuthenticationRequest request in requests) {
+ this.OnLoggingIn(request);
+ discoveryResultBuilder.Append("{");
+ discoveryResultBuilder.AppendFormat("endpoint: {0},", Util.GetSafeJavascriptValue(request.Provider.Uri.AbsoluteUri));
+ request.Mode = AuthenticationRequestMode.Immediate;
+ UserAgentResponse response = request.RedirectingResponse;
+ discoveryResultBuilder.AppendFormat("immediate: {0},", Util.GetSafeJavascriptValue(response.DirectUriRequest.AbsoluteUri));
+ request.Mode = AuthenticationRequestMode.Setup;
+ response = request.RedirectingResponse;
+ discoveryResultBuilder.AppendFormat("setup: {0}", Util.GetSafeJavascriptValue(response.DirectUriRequest.AbsoluteUri));
+ discoveryResultBuilder.Append("},");
+ }
+ discoveryResultBuilder.Length -= 1; // trim off last comma
+ discoveryResultBuilder.Append("]");
+ } else {
+ discoveryResultBuilder.Append("requests: new Array(),");
+ discoveryResultBuilder.AppendFormat("error: {0}", Util.GetSafeJavascriptValue(OpenIdStrings.OpenIdEndpointNotFound));
+ }
+ } catch (ProtocolException ex) {
+ discoveryResultBuilder.Append("requests: new Array(),");
+ discoveryResultBuilder.AppendFormat("error: {0}", Util.GetSafeJavascriptValue(ex.Message));
+ }
+ discoveryResultBuilder.Append("}");
+ this.discoveryResult = discoveryResultBuilder.ToString();
+ }
+
+ #endregion
+
+ /// <summary>
+ /// Fires the <see cref="LoggingIn"/> event.
+ /// </summary>
+ /// <param name="request">The request.</param>
+ protected virtual void OnLoggingIn(IAuthenticationRequest request) {
+ var loggingIn = this.LoggingIn;
+ if (loggingIn != null) {
+ loggingIn(this, new OpenIdEventArgs(request));
+ }
+ }
+
+ /// <summary>
+ /// Fires the <see cref="UnconfirmedPositiveAssertion"/> event.
+ /// </summary>
+ protected virtual void OnUnconfirmedPositiveAssertion() {
+ var unconfirmedPositiveAssertion = this.UnconfirmedPositiveAssertion;
+ if (unconfirmedPositiveAssertion != null) {
+ unconfirmedPositiveAssertion(this, null);
+ }
+ }
+
+ /// <summary>
+ /// Fires the <see cref="LoggedIn"/> event.
+ /// </summary>
+ /// <param name="response">The response.</param>
+ protected virtual void OnLoggedIn(IAuthenticationResponse response) {
+ var loggedIn = this.LoggedIn;
+ if (loggedIn != null) {
+ loggedIn(this, new OpenIdEventArgs(response));
+ }
+ }
+
+ /// <summary>
+ /// Prepares the control for loading.
+ /// </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 (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 = MessagingUtilities.GetQueryOrFormFromContext();
+ string userSuppliedIdentifier = query["dotnetopenid.userSuppliedIdentifier"];
+ if (!string.IsNullOrEmpty(userSuppliedIdentifier) && query["dotnetopenid.phase"] == "2") {
+ this.ReportAuthenticationResult();
+ }
+ }
+ }
+
+ /// <summary>
+ /// Prepares to render the control.
+ /// </summary>
+ /// <param name="e">An <see cref="T:System.EventArgs"/> object that contains the event data.</param>
+ protected override void OnPreRender(EventArgs e) {
+ base.OnPreRender(e);
+
+ this.PrepareClientJavascript();
+ }
+
+ /// <summary>
+ /// 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) {
+ // 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("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 (this.TabIndex > 0) {
+ writer.WriteAttribute("tabindex", this.TabIndex.ToString(CultureInfo.InvariantCulture));
+ }
+ if (!this.Enabled) {
+ writer.WriteAttribute("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(" />");
+ }
+ }
+
+ /// <summary>
+ /// Filters a sequence of OP endpoints so that an OP hostname only appears once in the list.
+ /// </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>
+ /// Invokes a method on a parent frame/window's OpenIdAjaxTextBox,
+ /// and closes the calling popup window if applicable.
+ /// </summary>
+ /// <param name="methodCall">The method to call on the OpenIdAjaxTextBox, including
+ /// parameters. (i.e. "callback('arg1', 2)"). No escaping is done by this method.</param>
+ private void CallbackUserAgentMethod(string methodCall) {
+ this.CallbackUserAgentMethod(methodCall, null);
+ }
+
+ /// <summary>
+ /// Invokes a method on a parent frame/window's OpenIdAjaxTextBox,
+ /// and closes the calling popup window if applicable.
+ /// </summary>
+ /// <param name="methodCall">The method to call on the OpenIdAjaxTextBox, including
+ /// parameters. (i.e. "callback('arg1', 2)"). No escaping is done by this method.</param>
+ /// <param name="preAssignments">An optional list of assignments to make to the input box object before placing the method call.</param>
+ private void CallbackUserAgentMethod(string methodCall, string[] preAssignments) {
+ Logger.InfoFormat("Sending Javascript callback: {0}", methodCall);
+ Page.Response.Write(@"<html><body><script language='javascript'>
+ var inPopup = !window.frameElement;
+ var objSrc = inPopup ? window.opener.waiting_openidBox : window.frameElement.openidBox;
+");
+ if (preAssignments != null) {
+ foreach (string assignment in preAssignments) {
+ Page.Response.Write(string.Format(CultureInfo.InvariantCulture, " objSrc.{0};\n", assignment));
+ }
+ }
+
+ // Something about calling objSrc.{0} can somehow cause FireFox to forget about the inPopup variable,
+ // so we have to actually put the test for it ABOVE the call to objSrc.{0} so that it already
+ // whether to call window.self.close() after the call.
+ string htmlFormat = @" if (inPopup) {{
+ objSrc.{0};
+ window.self.close();
+}} else {{
+ objSrc.{0};
+}}
+</script></body></html>";
+ Page.Response.Write(string.Format(CultureInfo.InvariantCulture, htmlFormat, methodCall));
+ Page.Response.End();
+ }
+
+ /// <summary>
+ /// Assembles the javascript to send to the client and registers it with ASP.NET for transmission.
+ /// </summary>
+ private void PrepareClientJavascript() {
+ string identifierParameterName = "identifier";
+ string discoveryCallbackResultParameterName = "resultFunction";
+ string discoveryErrorCallbackParameterName = "errorCallback";
+ string discoveryCallback = Page.ClientScript.GetCallbackEventReference(
+ this,
+ identifierParameterName,
+ discoveryCallbackResultParameterName,
+ identifierParameterName,
+ discoveryErrorCallbackParameterName,
+ true);
+
+ // Import the .js file where most of the code is.
+ this.Page.ClientScript.RegisterClientScriptResource(typeof(OpenIdAjaxTextBox), EmbeddedScriptResourceName);
+
+ // Call into the .js file with initialization information.
+ 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}",
+ Util.GetSafeJavascriptValue(this.Page.ClientScript.GetWebResourceUrl(this.GetType(), OpenIdTextBox.EmbeddedLogoResourceName)),
+ Util.GetSafeJavascriptValue(this.Page.ClientScript.GetWebResourceUrl(this.GetType(), EmbeddedDotNetOpenIdLogoResourceName)),
+ Util.GetSafeJavascriptValue(this.Page.ClientScript.GetWebResourceUrl(this.GetType(), EmbeddedSpinnerResourceName)),
+ Util.GetSafeJavascriptValue(this.Page.ClientScript.GetWebResourceUrl(this.GetType(), EmbeddedLoginSuccessResourceName)),
+ Util.GetSafeJavascriptValue(this.Page.ClientScript.GetWebResourceUrl(this.GetType(), EmbeddedLoginFailureResourceName)),
+ this.Throttle,
+ this.Timeout.TotalMilliseconds,
+ string.IsNullOrEmpty(this.OnClientAssertionReceived) ? "null" : "'" + this.OnClientAssertionReceived.Replace(@"\", @"\\").Replace("'", @"\'") + "'",
+ Util.GetSafeJavascriptValue(this.LogOnText),
+ Util.GetSafeJavascriptValue(this.LogOnToolTip),
+ Util.GetSafeJavascriptValue(this.RetryText),
+ Util.GetSafeJavascriptValue(this.RetryToolTip),
+ Util.GetSafeJavascriptValue(this.BusyToolTip),
+ Util.GetSafeJavascriptValue(this.IdentifierRequiredMessage),
+ Util.GetSafeJavascriptValue(this.LogOnInProgressMessage),
+ Util.GetSafeJavascriptValue(this.AuthenticationSucceededToolTip),
+ Util.GetSafeJavascriptValue(this.AuthenticatedAsToolTip),
+ Util.GetSafeJavascriptValue(this.AuthenticationFailedToolTip),
+ identifierParameterName,
+ discoveryCallbackResultParameterName,
+ discoveryErrorCallbackParameterName,
+ discoveryCallback,
+ Environment.NewLine);
+
+ startupScript.AppendLine("</script>");
+
+ Page.ClientScript.RegisterStartupScript(this.GetType(), "ajaxstartup", startupScript.ToString());
+ string htmlFormat = @"
+var openidbox = document.getElementsByName('{0}')[0];
+if (!openidbox.dnoi_internal.onSubmit()) {{ return false; }}
+";
+ Page.ClientScript.RegisterOnSubmitStatement(
+ this.GetType(),
+ "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 List<IAuthenticationRequest> CreateRequests(string userSuppliedIdentifier, bool immediate) {
+ var requests = new List<IAuthenticationRequest>();
+
+ OpenIdRelyingParty rp = CreateRelyingParty(true);
+
+ // 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);
+ realm.Scheme = Page.Request.Url.Scheme;
+ realm.Port = Page.Request.Url.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(rp.CreateRequests(userSuppliedIdentifier, typedRealm));
+ } else {
+ Uri returnTo = new Uri(MessagingUtilities.GetRequestUrlFromContext(), this.ReturnToUrl);
+ requests.AddRange(rp.CreateRequests(userSuppliedIdentifier, typedRealm, returnTo));
+ }
+
+ // 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 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", req.ClaimedIdentifier);
+ 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.InfoFormat("AJAX (iframe) callback from OP: {0}", this.Page.Request.Url);
+ List<string> assignments = new List<string>();
+
+ OpenIdRelyingParty rp = CreateRelyingParty(false);
+ var f = HttpUtility.ParseQueryString(this.Page.Request.Url.Query).ToDictionary();
+ var authResponse = rp.GetResponse();
+ if (authResponse.Status == AuthenticationStatus.Authenticated) {
+ this.OnUnconfirmedPositiveAssertion();
+ foreach (var pair in this.clientScriptExtensions) {
+ IClientScriptExtensionResponse extension = (IClientScriptExtensionResponse)authResponse.GetExtension(pair.Key);
+ 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
new file mode 100644
index 0000000..53f6d43
--- /dev/null
+++ b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdAjaxTextBox.js
@@ -0,0 +1,737 @@
+// 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 alert boxes
+
+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) {
+ box.dnoi_internal = new Object();
+ if (assertionReceivedCode) {
+ box.dnoi_internal.onauthenticated = function(sender, e) { eval(assertionReceivedCode); }
+ }
+
+ 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.constructButton = function(text, tooltip, onclick) {
+ var button = document.createElement('button');
+ button.textContent = text; // Mozilla
+ button.value = text; // IE
+ button.title = tooltip != null ? tooltip : '';
+ button.onclick = onclick;
+ button.style.visibility = 'hidden';
+ button.style.position = 'absolute';
+ button.style.padding = "0px";
+ button.style.fontSize = '8px';
+ button.style.top = "1px";
+ button.style.bottom = "1px";
+ button.style.right = "2px";
+ box.parentNode.appendChild(button);
+ return button;
+ }
+
+ box.dnoi_internal.constructIcon = function(imageUrl, tooltip, rightSide, visible, height) {
+ var icon = document.createElement('img');
+ icon.src = imageUrl;
+ icon.title = tooltip != null ? tooltip : '';
+ icon.originalTitle = icon.title;
+ if (!visible) {
+ icon.style.visibility = 'hidden';
+ }
+ icon.style.position = 'absolute';
+ icon.style.top = "2px";
+ icon.style.bottom = "2px"; // for FireFox (and IE7, I think)
+ if (height) {
+ icon.style.height = height; // for Chrome and IE8
+ }
+ if (rightSide) {
+ icon.style.right = "2px";
+ } else {
+ icon.style.left = "2px";
+ }
+ box.parentNode.appendChild(icon);
+ return icon;
+ }
+
+ box.dnoi_internal.prefetchImage = function(imageUrl) {
+ var img = document.createElement('img');
+ img.src = imageUrl;
+ img.style.display = 'none';
+ box.parentNode.appendChild(img);
+ return img;
+ }
+
+ function findParentForm(element) {
+ if (element == null || element.nodeName == "FORM") {
+ return element;
+ }
+
+ return findParentForm(element.parentNode);
+ };
+
+ box.parentForm = findParentForm(box);
+
+ function findOrCreateHiddenField() {
+ var name = box.name + '_openidAuthData';
+ var existing = window.document.getElementsByName(name);
+ if (existing && existing.length > 0) {
+ return existing[0];
+ }
+
+ var hiddenField = document.createElement('input');
+ hiddenField.setAttribute("name", name);
+ hiddenField.setAttribute("type", "hidden");
+ box.parentForm.appendChild(hiddenField);
+ return hiddenField;
+ };
+
+ box.dnoi_internal.loginButton = box.dnoi_internal.constructButton(loginButtonText, loginButtonToolTip, function() {
+ var discoveryInfo = box.dnoi_internal.authenticationRequests[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();
+ return false;
+ });
+ box.dnoi_internal.retryButton = box.dnoi_internal.constructButton(retryButtonText, retryButtonToolTip, function() {
+ box.timeout += 5000; // give the retry attempt 5s longer than the last attempt
+ box.dnoi_internal.performDiscovery(box.value);
+ return false;
+ });
+ box.dnoi_internal.openid_logo = box.dnoi_internal.constructIcon(openid_logo_url, null, false, true);
+ box.dnoi_internal.op_logo = box.dnoi_internal.constructIcon('', authenticatedByToolTip, false, false, "16px");
+ box.dnoi_internal.spinner = box.dnoi_internal.constructIcon(spinner_url, busyToolTip, true);
+ box.dnoi_internal.success_icon = box.dnoi_internal.constructIcon(success_icon_url, authenticatedAsToolTip, true);
+ //box.dnoi_internal.failure_icon = box.dnoi_internal.constructIcon(failure_icon_url, authenticationFailedToolTip, true);
+
+ // Disable the display of the DotNetOpenId logo
+ //box.dnoi_internal.dnoi_logo = box.dnoi_internal.constructIcon(dotnetopenid_logo_url);
+ box.dnoi_internal.dnoi_logo = box.dnoi_internal.openid_logo;
+
+ box.dnoi_internal.setVisualCue = function(state, authenticatedBy, authenticatedAs) {
+ box.dnoi_internal.openid_logo.style.visibility = 'hidden';
+ box.dnoi_internal.dnoi_logo.style.visibility = 'hidden';
+ box.dnoi_internal.op_logo.style.visibility = 'hidden';
+ box.dnoi_internal.openid_logo.title = box.dnoi_internal.openid_logo.originalTitle;
+ box.dnoi_internal.spinner.style.visibility = 'hidden';
+ box.dnoi_internal.success_icon.style.visibility = 'hidden';
+ // box.dnoi_internal.failure_icon.style.visibility = 'hidden';
+ box.dnoi_internal.loginButton.style.visibility = 'hidden';
+ box.dnoi_internal.retryButton.style.visibility = 'hidden';
+ box.title = '';
+ box.dnoi_internal.state = state;
+ if (state == "discovering") {
+ box.dnoi_internal.dnoi_logo.style.visibility = 'visible';
+ box.dnoi_internal.spinner.style.visibility = 'visible';
+ box.dnoi_internal.claimedIdentifier = null;
+ box.title = '';
+ window.status = "Discovering OpenID Identifier '" + box.value + "'...";
+ } else if (state == "authenticated") {
+ var opLogo = box.dnoi_internal.deriveOPFavIcon();
+ if (opLogo) {
+ box.dnoi_internal.op_logo.src = opLogo;
+ box.dnoi_internal.op_logo.style.visibility = 'visible';
+ box.dnoi_internal.op_logo.title = box.dnoi_internal.op_logo.originalTitle.replace('{0}', authenticatedBy.getHost());
+ } else {
+ box.dnoi_internal.openid_logo.style.visibility = 'visible';
+ box.dnoi_internal.openid_logo.title = box.dnoi_internal.op_logo.originalTitle.replace('{0}', authenticatedBy.getHost());
+ }
+ box.dnoi_internal.success_icon.style.visibility = 'visible';
+ box.dnoi_internal.success_icon.title = box.dnoi_internal.success_icon.originalTitle.replace('{0}', authenticatedAs);
+ box.title = box.dnoi_internal.claimedIdentifier;
+ window.status = "Authenticated as " + box.value;
+ } else if (state == "setup") {
+ var opLogo = box.dnoi_internal.deriveOPFavIcon();
+ if (opLogo) {
+ box.dnoi_internal.op_logo.src = opLogo;
+ box.dnoi_internal.op_logo.style.visibility = 'visible';
+ } else {
+ box.dnoi_internal.openid_logo.style.visibility = 'visible';
+ }
+ box.dnoi_internal.loginButton.style.visibility = 'visible';
+ box.dnoi_internal.claimedIdentifier = null;
+ window.status = "Authentication requires setup.";
+ } else if (state == "failed") {
+ box.dnoi_internal.openid_logo.style.visibility = 'visible';
+ //box.dnoi_internal.failure_icon.style.visibility = 'visible';
+ box.dnoi_internal.retryButton.style.visibility = 'visible';
+ box.dnoi_internal.claimedIdentifier = null;
+ window.status = authenticationFailedToolTip;
+ box.title = authenticationFailedToolTip;
+ } else if (state = '' || state == null) {
+ box.dnoi_internal.openid_logo.style.visibility = 'visible';
+ box.title = '';
+ box.dnoi_internal.claimedIdentifier = null;
+ window.status = null;
+ } else {
+ box.dnoi_internal.claimedIdentifier = null;
+ trace('unrecognized state ' + state);
+ }
+ }
+
+ box.dnoi_internal.isBusy = function() {
+ return box.dnoi_internal.state == 'discovering' ||
+ box.dnoi_internal.authenticationRequests[box.lastDiscoveredIdentifier].busy();
+ };
+
+ box.dnoi_internal.canAttemptLogin = function() {
+ if (box.value.length == 0) return false;
+ if (box.dnoi_internal.authenticationRequests[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];
+ }
+
+ box.dnoi_internal.isAuthenticated = function() {
+ return box.dnoi_internal.getUserSuppliedIdentifierResults().findSuccessfulRequest() != null;
+ }
+
+ box.dnoi_internal.onSubmit = function() {
+ 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);
+ } else {
+ hiddenField.setAttribute("value", '');
+ if (box.dnoi_internal.isBusy()) {
+ alert(loginInProgressMessage);
+ } else {
+ if (box.value.length > 0) {
+ // submitPending will be true if we've already tried deferring submit for a login,
+ // in which case we just want to display a box to the user.
+ if (box.dnoi_internal.submitPending || !box.dnoi_internal.canAttemptLogin()) {
+ alert(identifierRequiredMessage);
+ } else {
+ // The user hasn't clicked "Login" yet. We'll click login for him,
+ // after leaving a note for ourselves to automatically click submit
+ // when login is complete.
+ box.dnoi_internal.submitPending = box.dnoi_internal.submitButtonJustClicked;
+ if (box.dnoi_internal.submitPending == null) {
+ box.dnoi_internal.submitPending = true;
+ }
+ box.dnoi_internal.loginButton.onclick();
+ return false; // abort submit for now
+ }
+ } else {
+ return true;
+ }
+ }
+ return false;
+ }
+ return true;
+ };
+
+ /// <summary>
+ /// Records which submit button caused this openid box to question whether it
+ /// was ready to submit the user's identifier so that that button can be re-invoked
+ /// automatically after authentication completes.
+ /// </summary>
+ box.dnoi_internal.setLastSubmitButtonClicked = function(evt) {
+ var button;
+ if (evt.target) {
+ button = evt.target;
+ } else {
+ button = evt.srcElement;
+ }
+
+ box.dnoi_internal.submitButtonJustClicked = button;
+ };
+
+ // Find all submit buttons and hook their click events so that we can validate
+ // whether we are ready for the user to postback.
+ var inputs = document.getElementsByTagName('input');
+ for (var i = 0; i < inputs.length; i++) {
+ var el = inputs[i];
+ if (el.type == 'submit') {
+ if (el.attachEvent) {
+ el.attachEvent("onclick", box.dnoi_internal.setLastSubmitButtonClicked);
+ } else {
+ el.addEventListener("click", box.dnoi_internal.setLastSubmitButtonClicked, true);
+ }
+ }
+ }
+
+ /// <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];
+ }
+ }
+ };
+ 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];
+ }
+
+ 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;
+ };
+ this.trySetup = function() {
+ self.abort(); // ensure no concurrent attempts
+ window.waiting_openidBox = box;
+ self.popup = window.open(self.setup, 'opLogin', 'status=0,toolbar=0,location=1,resizable=1,scrollbars=1,width=800,height=600');
+ };
+ };
+
+ /*****************************************
+ * Flow
+ *****************************************/
+
+ /// <summary>Called to initiate discovery on some identifier.</summary>
+ box.dnoi_internal.performDiscovery = function(identifier) {
+ 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);
+ };
+
+ /// <summary>Callback that is invoked when discovery fails.</summary>
+ box.dnoi_internal.discoveryFailed = function(message, identifier) {
+ box.dnoi_internal.setVisualCue('failed');
+ if (message) { box.title = message; }
+ }
+
+ /// <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);
+
+ // 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();
+ }
+ }
+
+ /// <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);
+
+ // 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;
+ }
+ 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();
+ }
+ }
+ } 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");
+ }
+ };
+
+ function isOpenID2Response(resultUri) {
+ return resultUri.containsQueryArg("openid.ns");
+ };
+
+ 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();
+ }
+ } else {
+ if ((priorSuccess = discoveryInfo.findSuccessfulRequest())) {
+ box.dnoi_internal.setVisualCue('authenticated', priorSuccess.endpoint, discoveryInfo.claimedIdentifier);
+ } else {
+ discoveryInfo.tryImmediate();
+ }
+ }
+ return true;
+ };
+ box.onkeyup = function(event) {
+ box.dnoi_internal.setVisualCue();
+ return true;
+ };
+
+ 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());
+ }
+}
+
+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 pair = queryStringPairs[i].split('=');
+ this.Pairs.push(new KeyValuePair(unescape(pair[0]), unescape(pair[1])))
+ }
+ };
+
+ 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/OpenIdRelyingParty.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingParty.cs
index 4678137..1c57605 100644
--- a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingParty.cs
+++ b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingParty.cs
@@ -1,467 +1,482 @@
-//-----------------------------------------------------------------------
-// <copyright file="OpenIdRelyingParty.cs" company="Andrew Arnott">
-// Copyright (c) Andrew Arnott. All rights reserved.
-// </copyright>
-//-----------------------------------------------------------------------
-
-namespace DotNetOpenAuth.OpenId.RelyingParty {
- using System;
- using System.Collections.Generic;
- using System.Collections.Specialized;
- using System.ComponentModel;
- using System.Linq;
- using System.Web;
- using DotNetOpenAuth.Configuration;
- using DotNetOpenAuth.Messaging;
- using DotNetOpenAuth.Messaging.Bindings;
- using DotNetOpenAuth.OpenId.ChannelElements;
- using DotNetOpenAuth.OpenId.Messages;
-
- /// <summary>
- /// A delegate that decides whether a given OpenID Provider endpoint may be
- /// considered for authenticating a user.
- /// </summary>
- /// <param name="endpoint">The endpoint for consideration.</param>
- /// <returns>
- /// <c>True</c> if the endpoint should be considered.
- /// <c>False</c> to remove it from the pool of acceptable providers.
- /// </returns>
- public delegate bool EndpointSelector(IXrdsProviderEndpoint endpoint);
-
- /// <summary>
- /// Provides the programmatic facilities to act as an OpenId consumer.
- /// </summary>
- public sealed class OpenIdRelyingParty {
- /// <summary>
- /// The name of the key to use in the HttpApplication cache to store the
- /// instance of <see cref="StandardRelyingPartyApplicationStore"/> to use.
- /// </summary>
- private const string ApplicationStoreKey = "DotNetOpenAuth.OpenId.RelyingParty.OpenIdRelyingParty.ApplicationStore";
-
- /// <summary>
- /// Backing field for the <see cref="SecuritySettings"/> property.
- /// </summary>
- private RelyingPartySecuritySettings securitySettings;
-
- /// <summary>
- /// Backing store for the <see cref="EndpointOrder"/> property.
- /// </summary>
- private Comparison<IXrdsProviderEndpoint> endpointOrder = DefaultEndpointOrder;
-
- /// <summary>
- /// Backing field for the <see cref="Channel"/> property.
- /// </summary>
- private Channel channel;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="OpenIdRelyingParty"/> class.
- /// </summary>
- public OpenIdRelyingParty()
- : this(DotNetOpenAuthSection.Configuration.OpenId.RelyingParty.ApplicationStore.CreateInstance(HttpApplicationStore)) {
- }
-
- /// <summary>
- /// Initializes a new instance of the <see cref="OpenIdRelyingParty"/> class.
- /// </summary>
- /// <param name="applicationStore">The application store. If null, the relying party will always operate in "dumb mode".</param>
- public OpenIdRelyingParty(IRelyingPartyApplicationStore applicationStore)
- : this(applicationStore, applicationStore, applicationStore) {
- }
-
- /// <summary>
- /// Initializes a new instance of the <see cref="OpenIdRelyingParty"/> class.
- /// </summary>
- /// <param name="associationStore">The association store. If null, the relying party will always operate in "dumb mode".</param>
- /// <param name="nonceStore">The nonce store to use. If null, the relying party will always operate in "dumb mode".</param>
- /// <param name="secretStore">The secret store to use. If null, the relying party will always operate in "dumb mode".</param>
- private OpenIdRelyingParty(IAssociationStore<Uri> associationStore, INonceStore nonceStore, IPrivateSecretStore secretStore) {
- // If we are a smart-mode RP (supporting associations), then we MUST also be
- // capable of storing nonces to prevent replay attacks.
- // If we're a dumb-mode RP, then 2.0 OPs are responsible for preventing replays.
- ErrorUtilities.VerifyArgument(associationStore == null || nonceStore != null, OpenIdStrings.AssociationStoreRequiresNonceStore);
-
- this.securitySettings = DotNetOpenAuthSection.Configuration.OpenId.RelyingParty.SecuritySettings.CreateSecuritySettings();
-
- // Without a nonce store, we must rely on the Provider to protect against
- // replay attacks. But only 2.0+ Providers can be expected to provide
- // replay protection.
- if (nonceStore == null) {
- this.SecuritySettings.MinimumRequiredOpenIdVersion = ProtocolVersion.V20;
- }
-
- this.channel = new OpenIdChannel(associationStore, nonceStore, secretStore, this.SecuritySettings);
- this.AssociationManager = new AssociationManager(this.Channel, associationStore, this.SecuritySettings);
- }
-
- /// <summary>
- /// Gets an XRDS sorting routine that uses the XRDS Service/@Priority
- /// attribute to determine order.
- /// </summary>
- /// <remarks>
- /// Endpoints lacking any priority value are sorted to the end of the list.
- /// </remarks>
- [EditorBrowsable(EditorBrowsableState.Advanced)]
- public static Comparison<IXrdsProviderEndpoint> DefaultEndpointOrder {
- get { return ServiceEndpoint.EndpointOrder; }
- }
-
- /// <summary>
- /// Gets the standard state storage mechanism that uses ASP.NET's
- /// HttpApplication state dictionary to store associations and nonces.
- /// </summary>
- [EditorBrowsable(EditorBrowsableState.Advanced)]
- public static IRelyingPartyApplicationStore HttpApplicationStore {
- get {
- HttpContext context = HttpContext.Current;
- ErrorUtilities.VerifyOperation(context != null, OpenIdStrings.StoreRequiredWhenNoHttpContextAvailable, typeof(IRelyingPartyApplicationStore).Name);
- var store = (IRelyingPartyApplicationStore)context.Application[ApplicationStoreKey];
- if (store == null) {
- context.Application.Lock();
- try {
- if ((store = (IRelyingPartyApplicationStore)context.Application[ApplicationStoreKey]) == null) {
- context.Application[ApplicationStoreKey] = store = new StandardRelyingPartyApplicationStore();
- }
- } finally {
- context.Application.UnLock();
- }
- }
-
- return store;
- }
- }
-
- /// <summary>
- /// Gets or sets the channel to use for sending/receiving messages.
- /// </summary>
- public Channel Channel {
- get {
- return this.channel;
- }
-
- set {
- ErrorUtilities.VerifyArgumentNotNull(value, "value");
- this.channel = value;
- this.AssociationManager.Channel = value;
- }
- }
-
- /// <summary>
- /// Gets the security settings used by this Relying Party.
- /// </summary>
- public RelyingPartySecuritySettings SecuritySettings {
- get {
- return this.securitySettings;
- }
-
- internal set {
- ErrorUtilities.VerifyArgumentNotNull(value, "value");
- this.securitySettings = value;
- this.AssociationManager.SecuritySettings = value;
- }
- }
-
- /// <summary>
- /// Gets or sets the optional Provider Endpoint filter to use.
- /// </summary>
- /// <remarks>
- /// Provides a way to optionally filter the providers that may be used in authenticating a user.
- /// If provided, the delegate should return true to accept an endpoint, and false to reject it.
- /// If null, all identity providers will be accepted. This is the default.
- /// </remarks>
- [EditorBrowsable(EditorBrowsableState.Advanced)]
- public EndpointSelector EndpointFilter { get; set; }
-
- /// <summary>
- /// Gets or sets the ordering routine that will determine which XRDS
- /// Service element to try first
- /// </summary>
- /// <value>Default is <see cref="DefaultEndpointOrder"/>.</value>
- /// <remarks>
- /// This may never be null. To reset to default behavior this property
- /// can be set to the value of <see cref="DefaultEndpointOrder"/>.
- /// </remarks>
- [EditorBrowsable(EditorBrowsableState.Advanced)]
- public Comparison<IXrdsProviderEndpoint> EndpointOrder {
- get {
- return this.endpointOrder;
- }
-
- set {
- ErrorUtilities.VerifyArgumentNotNull(value, "value");
- this.endpointOrder = value;
- }
- }
-
- /// <summary>
- /// Gets a value indicating whether this Relying Party can sign its return_to
- /// parameter in outgoing authentication requests.
- /// </summary>
- internal bool CanSignCallbackArguments {
- get { return this.Channel.BindingElements.OfType<ReturnToSignatureBindingElement>().Any(); }
- }
-
- /// <summary>
- /// Gets the web request handler to use for discovery and the part of
- /// authentication where direct messages are sent to an untrusted remote party.
- /// </summary>
- internal IDirectWebRequestHandler WebRequestHandler {
- get { return this.Channel.WebRequestHandler; }
- }
-
- /// <summary>
- /// Gets the association manager.
- /// </summary>
- internal AssociationManager AssociationManager { get; private set; }
-
- /// <summary>
- /// Creates an authentication request to verify that a user controls
- /// some given Identifier.
- /// </summary>
- /// <param name="userSuppliedIdentifier">
- /// The Identifier supplied by the user. This may be a URL, an XRI or i-name.
- /// </param>
- /// <param name="realm">
- /// The shorest URL that describes this relying party web site's address.
- /// For example, if your login page is found at https://www.example.com/login.aspx,
- /// your realm would typically be https://www.example.com/.
- /// </param>
- /// <param name="returnToUrl">
- /// The URL of the login page, or the page prepared to receive authentication
- /// responses from the OpenID Provider.
- /// </param>
- /// <returns>
- /// An authentication request object that describes the HTTP response to
- /// send to the user agent to initiate the authentication.
- /// </returns>
- /// <exception cref="ProtocolException">Thrown if no OpenID endpoint could be found.</exception>
- public IAuthenticationRequest CreateRequest(Identifier userSuppliedIdentifier, Realm realm, Uri returnToUrl) {
- try {
- return this.CreateRequests(userSuppliedIdentifier, realm, returnToUrl).First();
- } catch (InvalidOperationException ex) {
- throw ErrorUtilities.Wrap(ex, OpenIdStrings.OpenIdEndpointNotFound);
- }
- }
-
- /// <summary>
- /// Creates an authentication request to verify that a user controls
- /// some given Identifier.
- /// </summary>
- /// <param name="userSuppliedIdentifier">
- /// The Identifier supplied by the user. This may be a URL, an XRI or i-name.
- /// </param>
- /// <param name="realm">
- /// The shorest URL that describes this relying party web site's address.
- /// For example, if your login page is found at https://www.example.com/login.aspx,
- /// your realm would typically be https://www.example.com/.
- /// </param>
- /// <returns>
- /// An authentication request object that describes the HTTP response to
- /// send to the user agent to initiate the authentication.
- /// </returns>
- /// <remarks>
- /// <para>Requires an <see cref="HttpContext.Current">HttpContext.Current</see> context.</para>
- /// </remarks>
- /// <exception cref="ProtocolException">Thrown if no OpenID endpoint could be found.</exception>
- /// <exception cref="InvalidOperationException">Thrown if <see cref="HttpContext.Current">HttpContext.Current</see> == <c>null</c>.</exception>
- public IAuthenticationRequest CreateRequest(Identifier userSuppliedIdentifier, Realm realm) {
- try {
- return this.CreateRequests(userSuppliedIdentifier, realm).First();
- } catch (InvalidOperationException ex) {
- throw ErrorUtilities.Wrap(ex, OpenIdStrings.OpenIdEndpointNotFound);
- }
- }
-
- /// <summary>
- /// Creates an authentication request to verify that a user controls
- /// some given Identifier.
- /// </summary>
- /// <param name="userSuppliedIdentifier">
- /// The Identifier supplied by the user. This may be a URL, an XRI or i-name.
- /// </param>
- /// <returns>
- /// An authentication request object that describes the HTTP response to
- /// send to the user agent to initiate the authentication.
- /// </returns>
- /// <remarks>
- /// <para>Requires an <see cref="HttpContext.Current">HttpContext.Current</see> context.</para>
- /// </remarks>
- /// <exception cref="ProtocolException">Thrown if no OpenID endpoint could be found.</exception>
- /// <exception cref="InvalidOperationException">Thrown if <see cref="HttpContext.Current">HttpContext.Current</see> == <c>null</c>.</exception>
- public IAuthenticationRequest CreateRequest(Identifier userSuppliedIdentifier) {
- try {
- return this.CreateRequests(userSuppliedIdentifier).First();
- } catch (InvalidOperationException ex) {
- throw ErrorUtilities.Wrap(ex, OpenIdStrings.OpenIdEndpointNotFound);
- }
- }
-
- /// <summary>
- /// Gets an authentication response from a Provider.
- /// </summary>
- /// <returns>The processed authentication response if there is any; <c>null</c> otherwise.</returns>
- /// <remarks>
- /// <para>Requires an <see cref="HttpContext.Current">HttpContext.Current</see> context.</para>
- /// </remarks>
- public IAuthenticationResponse GetResponse() {
- return this.GetResponse(this.Channel.GetRequestFromContext());
- }
-
- /// <summary>
- /// Gets an authentication response from a Provider.
- /// </summary>
- /// <param name="httpRequestInfo">The HTTP request that may be carrying an authentication response from the Provider.</param>
- /// <returns>The processed authentication response if there is any; <c>null</c> otherwise.</returns>
- public IAuthenticationResponse GetResponse(HttpRequestInfo httpRequestInfo) {
- try {
- var message = this.Channel.ReadFromRequest();
- PositiveAssertionResponse positiveAssertion;
- NegativeAssertionResponse negativeAssertion;
- if ((positiveAssertion = message as PositiveAssertionResponse) != null) {
- return new PositiveAuthenticationResponse(positiveAssertion, this);
- } else if ((negativeAssertion = message as NegativeAssertionResponse) != null) {
- return new NegativeAuthenticationResponse(negativeAssertion);
- } else if (message != null) {
- Logger.WarnFormat("Received unexpected message type {0} when expecting an assertion message.", message.GetType().Name);
- }
-
- return null;
- } catch (ProtocolException ex) {
- return new FailedAuthenticationResponse(ex);
- }
- }
-
- /// <summary>
- /// Determines whether some parameter name belongs to OpenID or this library
- /// as a protocol or internal parameter name.
- /// </summary>
- /// <param name="parameterName">Name of the parameter.</param>
- /// <returns>
- /// <c>true</c> if the named parameter is a library- or protocol-specific parameter; otherwise, <c>false</c>.
- /// </returns>
- internal static bool IsOpenIdSupportingParameter(string parameterName) {
- Protocol protocol = Protocol.Default;
- return parameterName.StartsWith(protocol.openid.Prefix, StringComparison.OrdinalIgnoreCase)
- || parameterName.StartsWith("dnoi.", StringComparison.Ordinal);
- }
-
- /// <summary>
- /// Generates the authentication requests that can satisfy the requirements of some OpenID Identifier.
- /// </summary>
- /// <param name="userSuppliedIdentifier">
- /// The Identifier supplied by the user. This may be a URL, an XRI or i-name.
- /// </param>
- /// <param name="realm">
- /// The shorest URL that describes this relying party web site's address.
- /// For example, if your login page is found at https://www.example.com/login.aspx,
- /// your realm would typically be https://www.example.com/.
- /// </param>
- /// <param name="returnToUrl">
- /// The URL of the login page, or the page prepared to receive authentication
- /// responses from the OpenID Provider.
- /// </param>
- /// <returns>
- /// An authentication request object that describes the HTTP response to
- /// send to the user agent to initiate the authentication.
- /// </returns>
- /// <remarks>
- /// <para>Any individual generated request can satisfy the authentication.
- /// The generated requests are sorted in preferred order.
- /// Each request is generated as it is enumerated to. Associations are created only as
- /// <see cref="IAuthenticationRequest.RedirectingResponse"/> is called.</para>
- /// <para>No exception is thrown if no OpenID endpoints were discovered.
- /// An empty enumerable is returned instead.</para>
- /// </remarks>
- internal IEnumerable<IAuthenticationRequest> CreateRequests(Identifier userSuppliedIdentifier, Realm realm, Uri returnToUrl) {
- ErrorUtilities.VerifyArgumentNotNull(realm, "realm");
- ErrorUtilities.VerifyArgumentNotNull(returnToUrl, "returnToUrl");
-
- return AuthenticationRequest.Create(userSuppliedIdentifier, this, realm, returnToUrl, true).Cast<IAuthenticationRequest>();
- }
-
- /// <summary>
- /// Generates the authentication requests that can satisfy the requirements of some OpenID Identifier.
- /// </summary>
- /// <param name="userSuppliedIdentifier">
- /// The Identifier supplied by the user. This may be a URL, an XRI or i-name.
- /// </param>
- /// <param name="realm">
- /// The shorest URL that describes this relying party web site's address.
- /// For example, if your login page is found at https://www.example.com/login.aspx,
- /// your realm would typically be https://www.example.com/.
- /// </param>
- /// <returns>
- /// An authentication request object that describes the HTTP response to
- /// send to the user agent to initiate the authentication.
- /// </returns>
- /// <remarks>
- /// <para>Any individual generated request can satisfy the authentication.
- /// The generated requests are sorted in preferred order.
- /// Each request is generated as it is enumerated to. Associations are created only as
- /// <see cref="IAuthenticationRequest.RedirectingResponse"/> is called.</para>
- /// <para>No exception is thrown if no OpenID endpoints were discovered.
- /// An empty enumerable is returned instead.</para>
- /// <para>Requires an <see cref="HttpContext.Current">HttpContext.Current</see> context.</para>
- /// </remarks>
- /// <exception cref="InvalidOperationException">Thrown if <see cref="HttpContext.Current">HttpContext.Current</see> == <c>null</c>.</exception>
- internal IEnumerable<IAuthenticationRequest> CreateRequests(Identifier userSuppliedIdentifier, Realm realm) {
- ErrorUtilities.VerifyHttpContext();
-
- // Build the return_to URL
- UriBuilder returnTo = new UriBuilder(MessagingUtilities.GetRequestUrlFromContext());
-
- // Trim off any parameters with an "openid." prefix, and a few known others
- // to avoid carrying state from a prior login attempt.
- returnTo.Query = string.Empty;
- NameValueCollection queryParams = MessagingUtilities.GetQueryFromContextNVC();
- var returnToParams = new Dictionary<string, string>(queryParams.Count);
- foreach (string key in queryParams) {
- if (!IsOpenIdSupportingParameter(key)) {
- returnToParams.Add(key, queryParams[key]);
- }
- }
- returnTo.AppendQueryArgs(returnToParams);
-
- return this.CreateRequests(userSuppliedIdentifier, realm, returnTo.Uri);
- }
-
- /// <summary>
- /// Generates the authentication requests that can satisfy the requirements of some OpenID Identifier.
- /// </summary>
- /// <param name="userSuppliedIdentifier">
- /// The Identifier supplied by the user. This may be a URL, an XRI or i-name.
- /// </param>
- /// <returns>
- /// An authentication request object that describes the HTTP response to
- /// send to the user agent to initiate the authentication.
- /// </returns>
- /// <remarks>
- /// <para>Any individual generated request can satisfy the authentication.
- /// The generated requests are sorted in preferred order.
- /// Each request is generated as it is enumerated to. Associations are created only as
- /// <see cref="IAuthenticationRequest.RedirectingResponse"/> is called.</para>
- /// <para>No exception is thrown if no OpenID endpoints were discovered.
- /// An empty enumerable is returned instead.</para>
- /// <para>Requires an <see cref="HttpContext.Current">HttpContext.Current</see> context.</para>
- /// </remarks>
- /// <exception cref="InvalidOperationException">Thrown if <see cref="HttpContext.Current">HttpContext.Current</see> == <c>null</c>.</exception>
- internal IEnumerable<IAuthenticationRequest> CreateRequests(Identifier userSuppliedIdentifier) {
- ErrorUtilities.VerifyHttpContext();
-
- // Build the realm URL
- UriBuilder realmUrl = new UriBuilder(MessagingUtilities.GetRequestUrlFromContext());
- realmUrl.Path = HttpContext.Current.Request.ApplicationPath;
- realmUrl.Query = null;
- realmUrl.Fragment = null;
-
- // For RP discovery, the realm url MUST NOT redirect. To prevent this for
- // virtual directory hosted apps, we need to make sure that the realm path ends
- // in a slash (since our calculation above guarantees it doesn't end in a specific
- // page like default.aspx).
- if (!realmUrl.Path.EndsWith("/", StringComparison.Ordinal)) {
- realmUrl.Path += "/";
- }
-
- return this.CreateRequests(userSuppliedIdentifier, new Realm(realmUrl.Uri));
- }
- }
-}
+//-----------------------------------------------------------------------
+// <copyright file="OpenIdRelyingParty.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OpenId.RelyingParty {
+ using System;
+ using System.Collections.Generic;
+ using System.Collections.Specialized;
+ using System.ComponentModel;
+ using System.Linq;
+ using System.Web;
+ using DotNetOpenAuth.Configuration;
+ using DotNetOpenAuth.Messaging;
+ using DotNetOpenAuth.Messaging.Bindings;
+ using DotNetOpenAuth.OpenId.ChannelElements;
+ using DotNetOpenAuth.OpenId.Messages;
+
+ /// <summary>
+ /// A delegate that decides whether a given OpenID Provider endpoint may be
+ /// considered for authenticating a user.
+ /// </summary>
+ /// <param name="endpoint">The endpoint for consideration.</param>
+ /// <returns>
+ /// <c>True</c> if the endpoint should be considered.
+ /// <c>False</c> to remove it from the pool of acceptable providers.
+ /// </returns>
+ public delegate bool EndpointSelector(IXrdsProviderEndpoint endpoint);
+
+ /// <summary>
+ /// Provides the programmatic facilities to act as an OpenId consumer.
+ /// </summary>
+ public sealed class OpenIdRelyingParty {
+ /// <summary>
+ /// The name of the key to use in the HttpApplication cache to store the
+ /// instance of <see cref="StandardRelyingPartyApplicationStore"/> to use.
+ /// </summary>
+ private const string ApplicationStoreKey = "DotNetOpenAuth.OpenId.RelyingParty.OpenIdRelyingParty.ApplicationStore";
+
+ /// <summary>
+ /// Backing field for the <see cref="SecuritySettings"/> property.
+ /// </summary>
+ private RelyingPartySecuritySettings securitySettings;
+
+ /// <summary>
+ /// Backing store for the <see cref="EndpointOrder"/> property.
+ /// </summary>
+ private Comparison<IXrdsProviderEndpoint> endpointOrder = DefaultEndpointOrder;
+
+ /// <summary>
+ /// Backing field for the <see cref="Channel"/> property.
+ /// </summary>
+ private Channel channel;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="OpenIdRelyingParty"/> class.
+ /// </summary>
+ public OpenIdRelyingParty()
+ : this(DotNetOpenAuthSection.Configuration.OpenId.RelyingParty.ApplicationStore.CreateInstance(HttpApplicationStore)) {
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="OpenIdRelyingParty"/> class.
+ /// </summary>
+ /// <param name="applicationStore">The application store. If null, the relying party will always operate in "dumb mode".</param>
+ public OpenIdRelyingParty(IRelyingPartyApplicationStore applicationStore)
+ : this(applicationStore, applicationStore, applicationStore) {
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="OpenIdRelyingParty"/> class.
+ /// </summary>
+ /// <param name="associationStore">The association store. If null, the relying party will always operate in "dumb mode".</param>
+ /// <param name="nonceStore">The nonce store to use. If null, the relying party will always operate in "dumb mode".</param>
+ /// <param name="secretStore">The secret store to use. If null, the relying party will always operate in "dumb mode".</param>
+ private OpenIdRelyingParty(IAssociationStore<Uri> associationStore, INonceStore nonceStore, IPrivateSecretStore secretStore) {
+ // If we are a smart-mode RP (supporting associations), then we MUST also be
+ // capable of storing nonces to prevent replay attacks.
+ // If we're a dumb-mode RP, then 2.0 OPs are responsible for preventing replays.
+ ErrorUtilities.VerifyArgument(associationStore == null || nonceStore != null, OpenIdStrings.AssociationStoreRequiresNonceStore);
+
+ this.securitySettings = DotNetOpenAuthSection.Configuration.OpenId.RelyingParty.SecuritySettings.CreateSecuritySettings();
+
+ // Without a nonce store, we must rely on the Provider to protect against
+ // replay attacks. But only 2.0+ Providers can be expected to provide
+ // replay protection.
+ if (nonceStore == null) {
+ this.SecuritySettings.MinimumRequiredOpenIdVersion = ProtocolVersion.V20;
+ }
+
+ this.channel = new OpenIdChannel(associationStore, nonceStore, secretStore, this.SecuritySettings);
+ this.AssociationManager = new AssociationManager(this.Channel, associationStore, this.SecuritySettings);
+ }
+
+ /// <summary>
+ /// Gets an XRDS sorting routine that uses the XRDS Service/@Priority
+ /// attribute to determine order.
+ /// </summary>
+ /// <remarks>
+ /// Endpoints lacking any priority value are sorted to the end of the list.
+ /// </remarks>
+ [EditorBrowsable(EditorBrowsableState.Advanced)]
+ public static Comparison<IXrdsProviderEndpoint> DefaultEndpointOrder {
+ get { return ServiceEndpoint.EndpointOrder; }
+ }
+
+ /// <summary>
+ /// Gets the standard state storage mechanism that uses ASP.NET's
+ /// HttpApplication state dictionary to store associations and nonces.
+ /// </summary>
+ [EditorBrowsable(EditorBrowsableState.Advanced)]
+ public static IRelyingPartyApplicationStore HttpApplicationStore {
+ get {
+ HttpContext context = HttpContext.Current;
+ ErrorUtilities.VerifyOperation(context != null, OpenIdStrings.StoreRequiredWhenNoHttpContextAvailable, typeof(IRelyingPartyApplicationStore).Name);
+ var store = (IRelyingPartyApplicationStore)context.Application[ApplicationStoreKey];
+ if (store == null) {
+ context.Application.Lock();
+ try {
+ if ((store = (IRelyingPartyApplicationStore)context.Application[ApplicationStoreKey]) == null) {
+ context.Application[ApplicationStoreKey] = store = new StandardRelyingPartyApplicationStore();
+ }
+ } finally {
+ context.Application.UnLock();
+ }
+ }
+
+ return store;
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets the channel to use for sending/receiving messages.
+ /// </summary>
+ public Channel Channel {
+ get {
+ return this.channel;
+ }
+
+ set {
+ ErrorUtilities.VerifyArgumentNotNull(value, "value");
+ this.channel = value;
+ this.AssociationManager.Channel = value;
+ }
+ }
+
+ /// <summary>
+ /// Gets the security settings used by this Relying Party.
+ /// </summary>
+ public RelyingPartySecuritySettings SecuritySettings {
+ get {
+ return this.securitySettings;
+ }
+
+ internal set {
+ ErrorUtilities.VerifyArgumentNotNull(value, "value");
+ this.securitySettings = value;
+ this.AssociationManager.SecuritySettings = value;
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets the optional Provider Endpoint filter to use.
+ /// </summary>
+ /// <remarks>
+ /// Provides a way to optionally filter the providers that may be used in authenticating a user.
+ /// If provided, the delegate should return true to accept an endpoint, and false to reject it.
+ /// If null, all identity providers will be accepted. This is the default.
+ /// </remarks>
+ [EditorBrowsable(EditorBrowsableState.Advanced)]
+ public EndpointSelector EndpointFilter { get; set; }
+
+ /// <summary>
+ /// Gets or sets the ordering routine that will determine which XRDS
+ /// Service element to try first
+ /// </summary>
+ /// <value>Default is <see cref="DefaultEndpointOrder"/>.</value>
+ /// <remarks>
+ /// This may never be null. To reset to default behavior this property
+ /// can be set to the value of <see cref="DefaultEndpointOrder"/>.
+ /// </remarks>
+ [EditorBrowsable(EditorBrowsableState.Advanced)]
+ public Comparison<IXrdsProviderEndpoint> EndpointOrder {
+ get {
+ return this.endpointOrder;
+ }
+
+ set {
+ ErrorUtilities.VerifyArgumentNotNull(value, "value");
+ this.endpointOrder = value;
+ }
+ }
+
+ /// <summary>
+ /// Gets a value indicating whether this Relying Party can sign its return_to
+ /// parameter in outgoing authentication requests.
+ /// </summary>
+ internal bool CanSignCallbackArguments {
+ get { return this.Channel.BindingElements.OfType<ReturnToSignatureBindingElement>().Any(); }
+ }
+
+ /// <summary>
+ /// Gets the web request handler to use for discovery and the part of
+ /// authentication where direct messages are sent to an untrusted remote party.
+ /// </summary>
+ internal IDirectWebRequestHandler WebRequestHandler {
+ get { return this.Channel.WebRequestHandler; }
+ }
+
+ /// <summary>
+ /// Gets the association manager.
+ /// </summary>
+ internal AssociationManager AssociationManager { get; private set; }
+
+ /// <summary>
+ /// Creates an authentication request to verify that a user controls
+ /// some given Identifier.
+ /// </summary>
+ /// <param name="userSuppliedIdentifier">
+ /// The Identifier supplied by the user. This may be a URL, an XRI or i-name.
+ /// </param>
+ /// <param name="realm">
+ /// The shorest URL that describes this relying party web site's address.
+ /// For example, if your login page is found at https://www.example.com/login.aspx,
+ /// your realm would typically be https://www.example.com/.
+ /// </param>
+ /// <param name="returnToUrl">
+ /// The URL of the login page, or the page prepared to receive authentication
+ /// responses from the OpenID Provider.
+ /// </param>
+ /// <returns>
+ /// An authentication request object that describes the HTTP response to
+ /// send to the user agent to initiate the authentication.
+ /// </returns>
+ /// <exception cref="ProtocolException">Thrown if no OpenID endpoint could be found.</exception>
+ public IAuthenticationRequest CreateRequest(Identifier userSuppliedIdentifier, Realm realm, Uri returnToUrl) {
+ try {
+ return this.CreateRequests(userSuppliedIdentifier, realm, returnToUrl).First();
+ } catch (InvalidOperationException ex) {
+ throw ErrorUtilities.Wrap(ex, OpenIdStrings.OpenIdEndpointNotFound);
+ }
+ }
+
+ /// <summary>
+ /// Creates an authentication request to verify that a user controls
+ /// some given Identifier.
+ /// </summary>
+ /// <param name="userSuppliedIdentifier">
+ /// The Identifier supplied by the user. This may be a URL, an XRI or i-name.
+ /// </param>
+ /// <param name="realm">
+ /// The shorest URL that describes this relying party web site's address.
+ /// For example, if your login page is found at https://www.example.com/login.aspx,
+ /// your realm would typically be https://www.example.com/.
+ /// </param>
+ /// <returns>
+ /// An authentication request object that describes the HTTP response to
+ /// send to the user agent to initiate the authentication.
+ /// </returns>
+ /// <remarks>
+ /// <para>Requires an <see cref="HttpContext.Current">HttpContext.Current</see> context.</para>
+ /// </remarks>
+ /// <exception cref="ProtocolException">Thrown if no OpenID endpoint could be found.</exception>
+ /// <exception cref="InvalidOperationException">Thrown if <see cref="HttpContext.Current">HttpContext.Current</see> == <c>null</c>.</exception>
+ public IAuthenticationRequest CreateRequest(Identifier userSuppliedIdentifier, Realm realm) {
+ try {
+ return this.CreateRequests(userSuppliedIdentifier, realm).First();
+ } catch (InvalidOperationException ex) {
+ throw ErrorUtilities.Wrap(ex, OpenIdStrings.OpenIdEndpointNotFound);
+ }
+ }
+
+ /// <summary>
+ /// Creates an authentication request to verify that a user controls
+ /// some given Identifier.
+ /// </summary>
+ /// <param name="userSuppliedIdentifier">
+ /// The Identifier supplied by the user. This may be a URL, an XRI or i-name.
+ /// </param>
+ /// <returns>
+ /// An authentication request object that describes the HTTP response to
+ /// send to the user agent to initiate the authentication.
+ /// </returns>
+ /// <remarks>
+ /// <para>Requires an <see cref="HttpContext.Current">HttpContext.Current</see> context.</para>
+ /// </remarks>
+ /// <exception cref="ProtocolException">Thrown if no OpenID endpoint could be found.</exception>
+ /// <exception cref="InvalidOperationException">Thrown if <see cref="HttpContext.Current">HttpContext.Current</see> == <c>null</c>.</exception>
+ public IAuthenticationRequest CreateRequest(Identifier userSuppliedIdentifier) {
+ try {
+ return this.CreateRequests(userSuppliedIdentifier).First();
+ } catch (InvalidOperationException ex) {
+ throw ErrorUtilities.Wrap(ex, OpenIdStrings.OpenIdEndpointNotFound);
+ }
+ }
+
+ /// <summary>
+ /// Gets an authentication response from a Provider.
+ /// </summary>
+ /// <returns>The processed authentication response if there is any; <c>null</c> otherwise.</returns>
+ /// <remarks>
+ /// <para>Requires an <see cref="HttpContext.Current">HttpContext.Current</see> context.</para>
+ /// </remarks>
+ public IAuthenticationResponse GetResponse() {
+ return this.GetResponse(this.Channel.GetRequestFromContext());
+ }
+
+ /// <summary>
+ /// Gets an authentication response from a Provider.
+ /// </summary>
+ /// <param name="httpRequestInfo">The HTTP request that may be carrying an authentication response from the Provider.</param>
+ /// <returns>The processed authentication response if there is any; <c>null</c> otherwise.</returns>
+ public IAuthenticationResponse GetResponse(HttpRequestInfo httpRequestInfo) {
+ try {
+ var message = this.Channel.ReadFromRequest();
+ PositiveAssertionResponse positiveAssertion;
+ NegativeAssertionResponse negativeAssertion;
+ if ((positiveAssertion = message as PositiveAssertionResponse) != null) {
+ return new PositiveAuthenticationResponse(positiveAssertion, this);
+ } else if ((negativeAssertion = message as NegativeAssertionResponse) != null) {
+ return new NegativeAuthenticationResponse(negativeAssertion);
+ } else if (message != null) {
+ Logger.WarnFormat("Received unexpected message type {0} when expecting an assertion message.", message.GetType().Name);
+ }
+
+ return null;
+ } catch (ProtocolException ex) {
+ return new FailedAuthenticationResponse(ex);
+ }
+ }
+
+ /// <summary>
+ /// Determines whether some parameter name belongs to OpenID or this library
+ /// as a protocol or internal parameter name.
+ /// </summary>
+ /// <param name="parameterName">Name of the parameter.</param>
+ /// <returns>
+ /// <c>true</c> if the named parameter is a library- or protocol-specific parameter; otherwise, <c>false</c>.
+ /// </returns>
+ internal static bool IsOpenIdSupportingParameter(string parameterName) {
+ Protocol protocol = Protocol.Default;
+ return parameterName.StartsWith(protocol.openid.Prefix, StringComparison.OrdinalIgnoreCase)
+ || parameterName.StartsWith("dnoi.", StringComparison.Ordinal);
+ }
+
+ /// <summary>
+ /// Creates a relying party that does not verify incoming messages against
+ /// nonce or association stores.
+ /// </summary>
+ /// <returns>The instantiated <see cref="OpenIdRelyingParty"/>.</returns>
+ /// <remarks>
+ /// Useful for previewing messages while
+ /// allowing them to be fully processed and verified later.
+ /// </remarks>
+ internal static OpenIdRelyingParty CreateNonVerifying() {
+ OpenIdRelyingParty rp = new OpenIdRelyingParty();
+ rp.Channel = new OpenIdChannel(null, null, null, rp.SecuritySettings);
+ return rp;
+ }
+
+ /// <summary>
+ /// Generates the authentication requests that can satisfy the requirements of some OpenID Identifier.
+ /// </summary>
+ /// <param name="userSuppliedIdentifier">
+ /// The Identifier supplied by the user. This may be a URL, an XRI or i-name.
+ /// </param>
+ /// <param name="realm">
+ /// The shorest URL that describes this relying party web site's address.
+ /// For example, if your login page is found at https://www.example.com/login.aspx,
+ /// your realm would typically be https://www.example.com/.
+ /// </param>
+ /// <param name="returnToUrl">
+ /// The URL of the login page, or the page prepared to receive authentication
+ /// responses from the OpenID Provider.
+ /// </param>
+ /// <returns>
+ /// An authentication request object that describes the HTTP response to
+ /// send to the user agent to initiate the authentication.
+ /// </returns>
+ /// <remarks>
+ /// <para>Any individual generated request can satisfy the authentication.
+ /// The generated requests are sorted in preferred order.
+ /// Each request is generated as it is enumerated to. Associations are created only as
+ /// <see cref="IAuthenticationRequest.RedirectingResponse"/> is called.</para>
+ /// <para>No exception is thrown if no OpenID endpoints were discovered.
+ /// An empty enumerable is returned instead.</para>
+ /// </remarks>
+ internal IEnumerable<IAuthenticationRequest> CreateRequests(Identifier userSuppliedIdentifier, Realm realm, Uri returnToUrl) {
+ ErrorUtilities.VerifyArgumentNotNull(realm, "realm");
+ ErrorUtilities.VerifyArgumentNotNull(returnToUrl, "returnToUrl");
+
+ return AuthenticationRequest.Create(userSuppliedIdentifier, this, realm, returnToUrl, true).Cast<IAuthenticationRequest>();
+ }
+
+ /// <summary>
+ /// Generates the authentication requests that can satisfy the requirements of some OpenID Identifier.
+ /// </summary>
+ /// <param name="userSuppliedIdentifier">
+ /// The Identifier supplied by the user. This may be a URL, an XRI or i-name.
+ /// </param>
+ /// <param name="realm">
+ /// The shorest URL that describes this relying party web site's address.
+ /// For example, if your login page is found at https://www.example.com/login.aspx,
+ /// your realm would typically be https://www.example.com/.
+ /// </param>
+ /// <returns>
+ /// An authentication request object that describes the HTTP response to
+ /// send to the user agent to initiate the authentication.
+ /// </returns>
+ /// <remarks>
+ /// <para>Any individual generated request can satisfy the authentication.
+ /// The generated requests are sorted in preferred order.
+ /// Each request is generated as it is enumerated to. Associations are created only as
+ /// <see cref="IAuthenticationRequest.RedirectingResponse"/> is called.</para>
+ /// <para>No exception is thrown if no OpenID endpoints were discovered.
+ /// An empty enumerable is returned instead.</para>
+ /// <para>Requires an <see cref="HttpContext.Current">HttpContext.Current</see> context.</para>
+ /// </remarks>
+ /// <exception cref="InvalidOperationException">Thrown if <see cref="HttpContext.Current">HttpContext.Current</see> == <c>null</c>.</exception>
+ internal IEnumerable<IAuthenticationRequest> CreateRequests(Identifier userSuppliedIdentifier, Realm realm) {
+ ErrorUtilities.VerifyHttpContext();
+
+ // Build the return_to URL
+ UriBuilder returnTo = new UriBuilder(MessagingUtilities.GetRequestUrlFromContext());
+
+ // Trim off any parameters with an "openid." prefix, and a few known others
+ // to avoid carrying state from a prior login attempt.
+ returnTo.Query = string.Empty;
+ NameValueCollection queryParams = MessagingUtilities.GetQueryFromContextNVC();
+ var returnToParams = new Dictionary<string, string>(queryParams.Count);
+ foreach (string key in queryParams) {
+ if (!IsOpenIdSupportingParameter(key)) {
+ returnToParams.Add(key, queryParams[key]);
+ }
+ }
+ returnTo.AppendQueryArgs(returnToParams);
+
+ return this.CreateRequests(userSuppliedIdentifier, realm, returnTo.Uri);
+ }
+
+ /// <summary>
+ /// Generates the authentication requests that can satisfy the requirements of some OpenID Identifier.
+ /// </summary>
+ /// <param name="userSuppliedIdentifier">
+ /// The Identifier supplied by the user. This may be a URL, an XRI or i-name.
+ /// </param>
+ /// <returns>
+ /// An authentication request object that describes the HTTP response to
+ /// send to the user agent to initiate the authentication.
+ /// </returns>
+ /// <remarks>
+ /// <para>Any individual generated request can satisfy the authentication.
+ /// The generated requests are sorted in preferred order.
+ /// Each request is generated as it is enumerated to. Associations are created only as
+ /// <see cref="IAuthenticationRequest.RedirectingResponse"/> is called.</para>
+ /// <para>No exception is thrown if no OpenID endpoints were discovered.
+ /// An empty enumerable is returned instead.</para>
+ /// <para>Requires an <see cref="HttpContext.Current">HttpContext.Current</see> context.</para>
+ /// </remarks>
+ /// <exception cref="InvalidOperationException">Thrown if <see cref="HttpContext.Current">HttpContext.Current</see> == <c>null</c>.</exception>
+ internal IEnumerable<IAuthenticationRequest> CreateRequests(Identifier userSuppliedIdentifier) {
+ ErrorUtilities.VerifyHttpContext();
+
+ // Build the realm URL
+ UriBuilder realmUrl = new UriBuilder(MessagingUtilities.GetRequestUrlFromContext());
+ realmUrl.Path = HttpContext.Current.Request.ApplicationPath;
+ realmUrl.Query = null;
+ realmUrl.Fragment = null;
+
+ // For RP discovery, the realm url MUST NOT redirect. To prevent this for
+ // virtual directory hosted apps, we need to make sure that the realm path ends
+ // in a slash (since our calculation above guarantees it doesn't end in a specific
+ // page like default.aspx).
+ if (!realmUrl.Path.EndsWith("/", StringComparison.Ordinal)) {
+ realmUrl.Path += "/";
+ }
+
+ return this.CreateRequests(userSuppliedIdentifier, new Realm(realmUrl.Uri));
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdTextBox.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdTextBox.cs
index 3fb1817..25088ab 100644
--- a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdTextBox.cs
+++ b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdTextBox.cs
@@ -6,7 +6,7 @@
[assembly: System.Web.UI.WebResource(DotNetOpenAuth.OpenId.RelyingParty.OpenIdTextBox.EmbeddedLogoResourceName, "image/gif")]
-#pragma warning disable 0809
+#pragma warning disable 0809 // marking inherited, unsupported properties as obsolete to discourage their use
namespace DotNetOpenAuth.OpenId.RelyingParty {
using System;
diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/PositiveAuthenticationResponse.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/PositiveAuthenticationResponse.cs
index 2deb909..4e76a48 100644
--- a/src/DotNetOpenAuth/OpenId/RelyingParty/PositiveAuthenticationResponse.cs
+++ b/src/DotNetOpenAuth/OpenId/RelyingParty/PositiveAuthenticationResponse.cs
@@ -12,6 +12,7 @@ namespace DotNetOpenAuth.OpenId.RelyingParty {
using System.Text;
using System.Web;
using DotNetOpenAuth.Messaging;
+ using DotNetOpenAuth.OpenId.Extensions;
using DotNetOpenAuth.OpenId.Messages;
/// <summary>
@@ -64,7 +65,7 @@ namespace DotNetOpenAuth.OpenId.RelyingParty {
this.VerifyDiscoveryMatchesAssertion();
}
- #region IAuthenticationResponse Members
+ #region IAuthenticationResponse Properties
/// <summary>
/// Gets the Identifier that the end user claims to own. For use with user database storage and lookup.
@@ -137,6 +138,17 @@ namespace DotNetOpenAuth.OpenId.RelyingParty {
get { return null; }
}
+ #endregion
+
+ /// <summary>
+ /// Gets the positive assertion response message.
+ /// </summary>
+ internal PositiveAssertionResponse Response {
+ get { return this.response; }
+ }
+
+ #region IAuthenticationResponse methods
+
/// <summary>
/// Gets a callback argument's value that was previously added using
/// <see cref="IAuthenticationRequest.AddCallbackArguments(string, string)"/>.
diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/login_failure.png b/src/DotNetOpenAuth/OpenId/RelyingParty/login_failure.png
new file mode 100644
index 0000000..8003700
--- /dev/null
+++ b/src/DotNetOpenAuth/OpenId/RelyingParty/login_failure.png
Binary files differ
diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/login_success (lock).png b/src/DotNetOpenAuth/OpenId/RelyingParty/login_success (lock).png
new file mode 100644
index 0000000..bc0c0c8
--- /dev/null
+++ b/src/DotNetOpenAuth/OpenId/RelyingParty/login_success (lock).png
Binary files differ
diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/login_success.png b/src/DotNetOpenAuth/OpenId/RelyingParty/login_success.png
new file mode 100644
index 0000000..0ae1365
--- /dev/null
+++ b/src/DotNetOpenAuth/OpenId/RelyingParty/login_success.png
Binary files differ
diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/spinner.gif b/src/DotNetOpenAuth/OpenId/RelyingParty/spinner.gif
new file mode 100644
index 0000000..9cb298e
--- /dev/null
+++ b/src/DotNetOpenAuth/OpenId/RelyingParty/spinner.gif
Binary files differ