summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--samples/OpenIdRelyingPartyWebForms/login.aspx4
-rw-r--r--samples/OpenIdRelyingPartyWebForms/login.aspx.cs10
-rw-r--r--samples/OpenIdRelyingPartyWebForms/login.aspx.designer.cs2
-rw-r--r--src/DotNetOpenAuth.Test/DotNetOpenAuth.Test.csproj1
-rw-r--r--src/DotNetOpenAuth.Test/Messaging/EnumerableCacheTests.cs130
-rw-r--r--src/DotNetOpenAuth/ComponentModel/ConverterBase.cs32
-rw-r--r--src/DotNetOpenAuth/ComponentModel/IdentifierConverter.cs69
-rw-r--r--src/DotNetOpenAuth/ComponentModel/SuggestedStringsConverter.cs19
-rw-r--r--src/DotNetOpenAuth/ComponentModel/UriConverter.cs2
-rw-r--r--src/DotNetOpenAuth/DotNetOpenAuth.csproj14
-rw-r--r--src/DotNetOpenAuth/Messaging/EnumerableCache.cs244
-rw-r--r--src/DotNetOpenAuth/Messaging/ErrorUtilities.cs21
-rw-r--r--src/DotNetOpenAuth/Messaging/StandardWebRequestHandler.cs1
-rw-r--r--src/DotNetOpenAuth/OpenId/OpenIdStrings.Designer.cs18
-rw-r--r--src/DotNetOpenAuth/OpenId/OpenIdStrings.resx6
-rw-r--r--src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdAjaxTextBox.js6
-rw-r--r--src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdButton.cs137
-rw-r--r--src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingPartyAjaxControlBase.cs320
-rw-r--r--src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingPartyAjaxControlBase.js150
-rw-r--r--src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingPartyControlBase.cs749
-rw-r--r--src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingPartyControlBase.js174
-rw-r--r--src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdTextBox.cs4
-rw-r--r--src/DotNetOpenAuth/OpenId/RelyingParty/WellKnownProviders.cs33
23 files changed, 2113 insertions, 33 deletions
diff --git a/samples/OpenIdRelyingPartyWebForms/login.aspx b/samples/OpenIdRelyingPartyWebForms/login.aspx
index 20294e7..5d857de 100644
--- a/samples/OpenIdRelyingPartyWebForms/login.aspx
+++ b/samples/OpenIdRelyingPartyWebForms/login.aspx
@@ -23,7 +23,7 @@
<asp:Label ID="setupRequiredLabel" runat="server" EnableViewState="False" Text="You must log into your Provider first to use Immediate mode."
Visible="False" />
<p>
- <asp:ImageButton runat="server" ImageUrl="~/images/yahoo.png" ID="yahooLoginButton"
- OnClick="yahooLoginButton_Click" />
+ <rp:OpenIdButton runat="server" ImageUrl="~/images/yahoo.png" Text="Login with Yahoo!" ID="yahooLoginButton"
+ Identifier="https://me.yahoo.com/" />
</p>
</asp:Content>
diff --git a/samples/OpenIdRelyingPartyWebForms/login.aspx.cs b/samples/OpenIdRelyingPartyWebForms/login.aspx.cs
index fb961dd..1de942a 100644
--- a/samples/OpenIdRelyingPartyWebForms/login.aspx.cs
+++ b/samples/OpenIdRelyingPartyWebForms/login.aspx.cs
@@ -39,16 +39,6 @@ namespace OpenIdRelyingPartyWebForms {
this.setupRequiredLabel.Visible = true;
}
- protected void yahooLoginButton_Click(object sender, ImageClickEventArgs e) {
- OpenIdRelyingParty openid = new OpenIdRelyingParty();
- var req = openid.CreateRequest("yahoo.com");
- this.prepareRequest(req);
- req.RedirectToProvider();
-
- // We don't listen for the response from the provider explicitly
- // because the OpenIdLogin control is already doing that for us.
- }
-
private void prepareRequest(IAuthenticationRequest request) {
// Collect the PAPE policies requested by the user.
List<string> policies = new List<string>();
diff --git a/samples/OpenIdRelyingPartyWebForms/login.aspx.designer.cs b/samples/OpenIdRelyingPartyWebForms/login.aspx.designer.cs
index 60de3ff..944f5ff 100644
--- a/samples/OpenIdRelyingPartyWebForms/login.aspx.designer.cs
+++ b/samples/OpenIdRelyingPartyWebForms/login.aspx.designer.cs
@@ -56,6 +56,6 @@ namespace OpenIdRelyingPartyWebForms {
/// Auto-generated field.
/// To modify move field declaration from designer file to code-behind file.
/// </remarks>
- protected global::System.Web.UI.WebControls.ImageButton yahooLoginButton;
+ protected global::DotNetOpenAuth.OpenId.RelyingParty.OpenIdButton yahooLoginButton;
}
}
diff --git a/src/DotNetOpenAuth.Test/DotNetOpenAuth.Test.csproj b/src/DotNetOpenAuth.Test/DotNetOpenAuth.Test.csproj
index 3957cfa..1a7a406 100644
--- a/src/DotNetOpenAuth.Test/DotNetOpenAuth.Test.csproj
+++ b/src/DotNetOpenAuth.Test/DotNetOpenAuth.Test.csproj
@@ -128,6 +128,7 @@
<Compile Include="Hosting\HttpHost.cs" />
<Compile Include="Hosting\TestingWorkerRequest.cs" />
<Compile Include="Messaging\CollectionAssert.cs" />
+ <Compile Include="Messaging\EnumerableCacheTests.cs" />
<Compile Include="Messaging\ErrorUtilitiesTests.cs" />
<Compile Include="Messaging\MessageSerializerTests.cs" />
<Compile Include="Messaging\Reflection\MessageDescriptionTests.cs" />
diff --git a/src/DotNetOpenAuth.Test/Messaging/EnumerableCacheTests.cs b/src/DotNetOpenAuth.Test/Messaging/EnumerableCacheTests.cs
new file mode 100644
index 0000000..55f4394
--- /dev/null
+++ b/src/DotNetOpenAuth.Test/Messaging/EnumerableCacheTests.cs
@@ -0,0 +1,130 @@
+//-----------------------------------------------------------------------
+// <copyright file="EnumerableCacheTests.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// This code is released under the Microsoft Public License (Ms-PL).
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.Test.Messaging {
+ using System;
+ using System.Collections.Generic;
+ using System.Collections.ObjectModel;
+ using System.Linq;
+ using DotNetOpenAuth.Messaging;
+ using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+ /// <summary>
+ /// Tests for cached enumeration.
+ /// </summary>
+ [TestClass]
+ public class EnumerableCacheTests {
+ /// <summary>
+ /// The number of times the generator method's implementation is started.
+ /// </summary>
+ private int generatorInvocations;
+
+ /// <summary>
+ /// The number of times the end of the generator method's implementation is reached.
+ /// </summary>
+ private int generatorCompleted;
+
+ /// <summary>
+ /// Gets or sets the test context.
+ /// </summary>
+ public TestContext TestContext { get; set; }
+
+ /// <summary>
+ /// Sets up a test.
+ /// </summary>
+ [TestInitialize]
+ public void Setup() {
+ this.generatorInvocations = 0;
+ this.generatorCompleted = 0;
+ }
+
+ [TestMethod]
+ public void EnumerableCache() {
+ // Baseline
+ var generator = this.NumberGenerator();
+ var list1 = generator.ToList();
+ var list2 = generator.ToList();
+ Assert.AreEqual(2, this.generatorInvocations);
+ CollectionAssert.AreEqual(list1, list2);
+
+ // Cache behavior
+ this.generatorInvocations = 0;
+ this.generatorCompleted = 0;
+ generator = this.NumberGenerator().CacheGeneratedResults();
+ var list3 = generator.ToList();
+ var list4 = generator.ToList();
+ Assert.AreEqual(1, this.generatorInvocations);
+ Assert.AreEqual(1, this.generatorCompleted);
+ CollectionAssert.AreEqual(list1, list3);
+ CollectionAssert.AreEqual(list1, list4);
+ }
+
+ [TestMethod]
+ public void GeneratesOnlyRequiredElements() {
+ var generator = this.NumberGenerator().CacheGeneratedResults();
+ Assert.AreEqual(0, this.generatorInvocations);
+ generator.Take(2).ToList();
+ Assert.AreEqual(1, this.generatorInvocations);
+ Assert.AreEqual(0, this.generatorCompleted, "Only taking part of the list should not have completed the generator.");
+ }
+
+ [TestMethod]
+ public void PassThruDoubleCache() {
+ var cache1 = this.NumberGenerator().CacheGeneratedResults();
+ var cache2 = cache1.CacheGeneratedResults();
+ Assert.AreSame(cache1, cache2, "Two caches were set up rather than just sharing the first one.");
+ }
+
+ [TestMethod]
+ public void PassThruList() {
+ var list = this.NumberGenerator().ToList();
+ var cache = list.CacheGeneratedResults();
+ Assert.AreSame(list, cache);
+ }
+
+ [TestMethod]
+ public void PassThruArray() {
+ var array = this.NumberGenerator().ToArray();
+ var cache = array.CacheGeneratedResults();
+ Assert.AreSame(array, cache);
+ }
+
+ [TestMethod]
+ public void PassThruCollection() {
+ var collection = new Collection<int>();
+ var cache = collection.CacheGeneratedResults();
+ Assert.AreSame(collection, cache);
+ }
+
+ /// <summary>
+ /// Tests calling IEnumerator.Current before first call to MoveNext.
+ /// </summary>
+ [TestMethod, ExpectedException(typeof(InvalidOperationException))]
+ public void EnumerableCacheCurrentThrowsBefore() {
+ var foo = this.NumberGenerator().CacheGeneratedResults().GetEnumerator().Current;
+ }
+
+ /// <summary>
+ /// Tests calling IEnumerator.Current after MoveNext returns false.
+ /// </summary>
+ [TestMethod, ExpectedException(typeof(InvalidOperationException))]
+ public void EnumerableCacheCurrentThrowsAfter() {
+ var enumerator = this.NumberGenerator().CacheGeneratedResults().GetEnumerator();
+ while (enumerator.MoveNext()) {
+ }
+ var foo = enumerator.Current;
+ }
+
+ private IEnumerable<int> NumberGenerator() {
+ this.generatorInvocations++;
+ for (int i = 10; i < 15; i++) {
+ yield return i;
+ }
+ this.generatorCompleted++;
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth/ComponentModel/ConverterBase.cs b/src/DotNetOpenAuth/ComponentModel/ConverterBase.cs
index a7d58c7..37f9c78 100644
--- a/src/DotNetOpenAuth/ComponentModel/ConverterBase.cs
+++ b/src/DotNetOpenAuth/ComponentModel/ConverterBase.cs
@@ -12,6 +12,9 @@ namespace DotNetOpenAuth.ComponentModel {
using System.Diagnostics.CodeAnalysis;
using System.Diagnostics.Contracts;
using System.Globalization;
+using System.Reflection;
+ using System.Security;
+ using System.Security.Permissions;
/// <summary>
/// A design-time helper to allow Intellisense to aid typing
@@ -141,6 +144,10 @@ namespace DotNetOpenAuth.ComponentModel {
/// The conversion cannot be performed.
/// </exception>
public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType) {
+ if (destinationType.IsInstanceOfType(value)) {
+ return value;
+ }
+
T typedValue = (T)value;
if (destinationType == typeof(string)) {
return this.ConvertToString(typedValue);
@@ -152,6 +159,20 @@ namespace DotNetOpenAuth.ComponentModel {
}
/// <summary>
+ /// Creates an <see cref="InstanceDescriptor"/> instance, protecting against the LinkDemand.
+ /// </summary>
+ /// <param name="memberInfo">The member info.</param>
+ /// <param name="arguments">The arguments.</param>
+ /// <returns>A <see cref="InstanceDescriptor"/>, or <c>null</c> if sufficient permissions are unavailable.</returns>
+ protected static InstanceDescriptor CreateInstanceDescriptor(MemberInfo memberInfo, ICollection arguments) {
+ try {
+ return CreateInstanceDescriptorPrivate(memberInfo, arguments);
+ } catch (SecurityException) {
+ return null;
+ }
+ }
+
+ /// <summary>
/// Gets the standard values to suggest with Intellisense in the designer.
/// </summary>
/// <returns>A collection of the standard values.</returns>
@@ -185,5 +206,16 @@ namespace DotNetOpenAuth.ComponentModel {
/// <returns>The string representation of the object.</returns>
[Pure]
protected abstract string ConvertToString(T value);
+
+ /// <summary>
+ /// Creates an <see cref="InstanceDescriptor"/> instance, protecting against the LinkDemand.
+ /// </summary>
+ /// <param name="memberInfo">The member info.</param>
+ /// <param name="arguments">The arguments.</param>
+ /// <returns>A <see cref="InstanceDescriptor"/>.</returns>
+ [PermissionSet(SecurityAction.Demand, Name = "FullTrust")]
+ private static InstanceDescriptor CreateInstanceDescriptorPrivate(MemberInfo memberInfo, ICollection arguments) {
+ return new InstanceDescriptor(memberInfo, arguments);
+ }
}
}
diff --git a/src/DotNetOpenAuth/ComponentModel/IdentifierConverter.cs b/src/DotNetOpenAuth/ComponentModel/IdentifierConverter.cs
new file mode 100644
index 0000000..6ba9c4b
--- /dev/null
+++ b/src/DotNetOpenAuth/ComponentModel/IdentifierConverter.cs
@@ -0,0 +1,69 @@
+//-----------------------------------------------------------------------
+// <copyright file="IdentifierConverter.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.ComponentModel {
+ using System;
+ using System.Collections;
+ using System.ComponentModel.Design.Serialization;
+ using System.Reflection;
+ using DotNetOpenAuth.OpenId;
+ using DotNetOpenAuth.OpenId.RelyingParty;
+
+ /// <summary>
+ /// A design-time helper to give an OpenID Identifier property an auto-complete functionality
+ /// listing the OP Identifiers in the <see cref="WellKnownProviders"/> class.
+ /// </summary>
+ public class IdentifierConverter : ConverterBase<Identifier> {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="IdentifierConverter"/> class.
+ /// </summary>
+ [Obsolete("This class is meant for design-time use within an IDE, and not meant to be used directly by runtime code.")]
+ public IdentifierConverter() {
+ }
+
+ /// <summary>
+ /// Converts a value from its string representation to its strongly-typed object.
+ /// </summary>
+ /// <param name="value">The value.</param>
+ /// <returns>The strongly-typed object.</returns>
+ protected override Identifier ConvertFrom(string value) {
+ return value;
+ }
+
+ /// <summary>
+ /// Creates the reflection instructions for recreating an instance later.
+ /// </summary>
+ /// <param name="value">The value to recreate later.</param>
+ /// <returns>
+ /// The description of how to recreate an instance.
+ /// </returns>
+ protected override InstanceDescriptor CreateFrom(Identifier value) {
+ if (value == null) {
+ return null;
+ }
+
+ MemberInfo identifierParse = typeof(Identifier).GetMethod("Parse", BindingFlags.Static | BindingFlags.Public);
+ return CreateInstanceDescriptor(identifierParse, new object[] { value.ToString() });
+ }
+
+ /// <summary>
+ /// Converts the strongly-typed value to a string.
+ /// </summary>
+ /// <param name="value">The value to convert.</param>
+ /// <returns>The string representation of the object.</returns>
+ protected override string ConvertToString(Identifier value) {
+ return value;
+ }
+
+ /// <summary>
+ /// Gets the standard values to suggest with Intellisense in the designer.
+ /// </summary>
+ /// <returns>A collection of the standard values.</returns>
+ protected override ICollection GetStandardValuesForCache() {
+ return SuggestedStringsConverter.GetStandardValuesForCacheShared(typeof(WellKnownProviders));
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth/ComponentModel/SuggestedStringsConverter.cs b/src/DotNetOpenAuth/ComponentModel/SuggestedStringsConverter.cs
index 3b60bd7..1c8c555 100644
--- a/src/DotNetOpenAuth/ComponentModel/SuggestedStringsConverter.cs
+++ b/src/DotNetOpenAuth/ComponentModel/SuggestedStringsConverter.cs
@@ -30,6 +30,19 @@ namespace DotNetOpenAuth.ComponentModel {
protected abstract Type WellKnownValuesType { get; }
/// <summary>
+ /// Gets the values of public static fields and properties on a given type.
+ /// </summary>
+ /// <param name="type">The type to reflect over.</param>
+ /// <returns>A collection of values.</returns>
+ internal static ICollection GetStandardValuesForCacheShared(Type type) {
+ var fields = from field in type.GetFields(BindingFlags.Static | BindingFlags.Public)
+ select field.GetValue(null);
+ var properties = from prop in type.GetProperties(BindingFlags.Static | BindingFlags.Public)
+ select prop.GetValue(null, null);
+ return (fields.Concat(properties)).ToArray();
+ }
+
+ /// <summary>
/// Converts a value from its string representation to its strongly-typed object.
/// </summary>
/// <param name="value">The value.</param>
@@ -68,11 +81,7 @@ namespace DotNetOpenAuth.ComponentModel {
/// <returns>A collection of the standard values.</returns>
[Pure]
protected override ICollection GetStandardValuesForCache() {
- var fields = from field in this.WellKnownValuesType.GetFields(BindingFlags.Static | BindingFlags.Public)
- select field.GetValue(null);
- var properties = from prop in this.WellKnownValuesType.GetProperties(BindingFlags.Static | BindingFlags.Public)
- select prop.GetValue(null, null);
- return (fields.Concat(properties)).ToArray();
+ return GetStandardValuesForCacheShared(this.WellKnownValuesType);
}
}
}
diff --git a/src/DotNetOpenAuth/ComponentModel/UriConverter.cs b/src/DotNetOpenAuth/ComponentModel/UriConverter.cs
index 4412199..cf8dde3 100644
--- a/src/DotNetOpenAuth/ComponentModel/UriConverter.cs
+++ b/src/DotNetOpenAuth/ComponentModel/UriConverter.cs
@@ -76,7 +76,7 @@ namespace DotNetOpenAuth.ComponentModel {
}
MemberInfo uriCtor = typeof(Uri).GetConstructor(new Type[] { typeof(string) });
- return new InstanceDescriptor(uriCtor, new object[] { value.AbsoluteUri });
+ return CreateInstanceDescriptor(uriCtor, new object[] { value.AbsoluteUri });
}
/// <summary>
diff --git a/src/DotNetOpenAuth/DotNetOpenAuth.csproj b/src/DotNetOpenAuth/DotNetOpenAuth.csproj
index 7a28841..80e1a72 100644
--- a/src/DotNetOpenAuth/DotNetOpenAuth.csproj
+++ b/src/DotNetOpenAuth/DotNetOpenAuth.csproj
@@ -174,6 +174,7 @@
<Compile Include="ComponentModel\ClaimTypeSuggestions.cs" />
<Compile Include="ComponentModel\ConverterBase.cs" />
<Compile Include="ComponentModel\IssuersSuggestions.cs" />
+ <Compile Include="ComponentModel\IdentifierConverter.cs" />
<Compile Include="ComponentModel\SuggestedStringsConverter.cs" />
<Compile Include="ComponentModel\UriConverter.cs" />
<Compile Include="Configuration\AssociationTypeCollection.cs" />
@@ -210,6 +211,7 @@
<Compile Include="Messaging\CachedDirectWebResponse.cs" />
<Compile Include="Messaging\ChannelContract.cs" />
<Compile Include="Messaging\DirectWebRequestOptions.cs" />
+ <Compile Include="Messaging\EnumerableCache.cs" />
<Compile Include="Messaging\HostErrorException.cs" />
<Compile Include="Messaging\IHttpDirectResponse.cs" />
<Compile Include="Messaging\IExtensionMessage.cs" />
@@ -429,11 +431,14 @@
<Compile Include="OpenId\RelyingParty\AssociationPreference.cs" />
<Compile Include="OpenId\RelyingParty\AuthenticationRequest.cs" />
<Compile Include="OpenId\RelyingParty\AuthenticationRequestMode.cs" />
+ <Compile Include="OpenId\RelyingParty\OpenIdRelyingPartyAjaxControlBase.cs" />
<Compile Include="OpenId\RelyingParty\NegativeAuthenticationResponse.cs" />
<Compile Include="OpenId\RelyingParty\OpenIdAjaxTextBox.cs" />
+ <Compile Include="OpenId\RelyingParty\OpenIdButton.cs" />
<Compile Include="OpenId\RelyingParty\OpenIdEventArgs.cs" />
<Compile Include="OpenId\RelyingParty\OpenIdLogin.cs" />
<Compile Include="OpenId\RelyingParty\OpenIdMobileTextBox.cs" />
+ <Compile Include="OpenId\RelyingParty\OpenIdRelyingPartyControlBase.cs" />
<Compile Include="OpenId\RelyingParty\OpenIdTextBox.cs" />
<Compile Include="OpenId\RelyingParty\PopupBehavior.cs" />
<Compile Include="OpenId\RelyingParty\PositiveAnonymousResponse.cs" />
@@ -461,6 +466,7 @@
<Compile Include="OpenId\RelyingParty\ServiceEndpoint.cs" />
<Compile Include="OpenId\OpenIdXrdsHelper.cs" />
<Compile Include="OpenId\RelyingParty\StandardRelyingPartyApplicationStore.cs" />
+ <Compile Include="OpenId\RelyingParty\WellKnownProviders.cs" />
<Compile Include="OpenId\SecuritySettings.cs" />
<Compile Include="Messaging\UntrustedWebRequestHandler.cs" />
<Compile Include="OpenId\UriIdentifier.cs" />
@@ -556,6 +562,12 @@
<EmbeddedResource Include="InfoCard\infocard_92x64.png" />
<EmbeddedResource Include="InfoCard\SupportingScript.js" />
</ItemGroup>
+ <ItemGroup>
+ <EmbeddedResource Include="OpenId\RelyingParty\OpenIdRelyingPartyControlBase.js" />
+ </ItemGroup>
+ <ItemGroup>
+ <EmbeddedResource Include="OpenId\RelyingParty\OpenIdRelyingPartyAjaxControlBase.js" />
+ </ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<Import Project="..\..\tools\DotNetOpenAuth.Versioning.targets" />
-</Project>
+</Project> \ No newline at end of file
diff --git a/src/DotNetOpenAuth/Messaging/EnumerableCache.cs b/src/DotNetOpenAuth/Messaging/EnumerableCache.cs
new file mode 100644
index 0000000..d343410
--- /dev/null
+++ b/src/DotNetOpenAuth/Messaging/EnumerableCache.cs
@@ -0,0 +1,244 @@
+//-----------------------------------------------------------------------
+// <copyright file="EnumerableCache.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// This code is released under the Microsoft Public License (Ms-PL).
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.Messaging {
+ using System;
+ using System.Collections;
+ using System.Collections.Generic;
+
+ /// <summary>
+ /// Extension methods for <see cref="IEnumerable&lt;T&gt;"/> types.
+ /// </summary>
+ public static class EnumerableCacheExtensions {
+ /// <summary>
+ /// Caches the results of enumerating over a given object so that subsequence enumerations
+ /// don't require interacting with the object a second time.
+ /// </summary>
+ /// <typeparam name="T">The type of element found in the enumeration.</typeparam>
+ /// <param name="sequence">The enumerable object.</param>
+ /// <returns>
+ /// Either a new enumerable object that caches enumerated results, or the original, <paramref name="sequence"/>
+ /// object if no caching is necessary to avoid additional CPU work.
+ /// </returns>
+ /// <remarks>
+ /// <para>This is designed for use on the results of generator methods (the ones with <c>yield return</c> in them)
+ /// so that only those elements in the sequence that are needed are ever generated, while not requiring
+ /// regeneration of elements that are enumerated over multiple times.</para>
+ /// <para>This can be a huge performance gain if enumerating multiple times over an expensive generator method.</para>
+ /// <para>Some enumerable types such as collections, lists, and already-cached generators do not require
+ /// any (additional) caching, and this method will simply return those objects rather than caching them
+ /// to avoid double-caching.</para>
+ /// </remarks>
+ public static IEnumerable<T> CacheGeneratedResults<T>(this IEnumerable<T> sequence) {
+ // Don't create a cache for types that don't need it.
+ if (sequence is IList<T> ||
+ sequence is ICollection<T> ||
+ sequence is Array ||
+ sequence is EnumerableCache<T>) {
+ return sequence;
+ }
+
+ return new EnumerableCache<T>(sequence);
+ }
+
+ /// <summary>
+ /// A wrapper for <see cref="IEnumerable&lt;T&gt;"/> types and returns a caching <see cref="IEnumerator&lt;T&gt;"/>
+ /// from its <see cref="IEnumerable&lt;T&gt;.GetEnumerator"/> method.
+ /// </summary>
+ /// <typeparam name="T">The type of element in the sequence.</typeparam>
+ private class EnumerableCache<T> : IEnumerable<T> {
+ /// <summary>
+ /// The results from enumeration of the live object that have been collected thus far.
+ /// </summary>
+ private List<T> cache;
+
+ /// <summary>
+ /// The original generator method or other enumerable object whose contents should only be enumerated once.
+ /// </summary>
+ private IEnumerable<T> generator;
+
+ /// <summary>
+ /// The enumerator we're using over the generator method's results.
+ /// </summary>
+ private IEnumerator<T> generatorEnumerator;
+
+ /// <summary>
+ /// The sync object our caching enumerators use when adding a new live generator method result to the cache.
+ /// </summary>
+ /// <remarks>
+ /// Although individual enumerators are not thread-safe, this <see cref="IEnumerable&lt;T&gt;"/> should be
+ /// thread safe so that multiple enumerators can be created from it and used from different threads.
+ /// </remarks>
+ private object generatorLock = new object();
+
+ /// <summary>
+ /// Initializes a new instance of the EnumerableCache class.
+ /// </summary>
+ /// <param name="generator">The generator.</param>
+ internal EnumerableCache(IEnumerable<T> generator) {
+ if (generator == null) {
+ throw new ArgumentNullException("generator");
+ }
+
+ this.generator = generator;
+ }
+
+ #region IEnumerable<T> Members
+
+ /// <summary>
+ /// Returns an enumerator that iterates through the collection.
+ /// </summary>
+ /// <returns>
+ /// A <see cref="T:System.Collections.Generic.IEnumerator`1"/> that can be used to iterate through the collection.
+ /// </returns>
+ public IEnumerator<T> GetEnumerator() {
+ if (this.generatorEnumerator == null) {
+ this.cache = new List<T>();
+ this.generatorEnumerator = this.generator.GetEnumerator();
+ }
+
+ return new EnumeratorCache(this);
+ }
+
+ #endregion
+
+ #region IEnumerable Members
+
+ /// <summary>
+ /// Returns an enumerator that iterates through a collection.
+ /// </summary>
+ /// <returns>
+ /// An <see cref="T:System.Collections.IEnumerator"/> object that can be used to iterate through the collection.
+ /// </returns>
+ System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() {
+ return this.GetEnumerator();
+ }
+
+ #endregion
+
+ /// <summary>
+ /// An enumerator that uses cached enumeration results whenever they are available,
+ /// and caches whatever results it has to pull from the original <see cref="IEnumerable&lt;T&gt;"/> object.
+ /// </summary>
+ private class EnumeratorCache : IEnumerator<T> {
+ /// <summary>
+ /// The parent enumeration wrapper class that stores the cached results.
+ /// </summary>
+ private EnumerableCache<T> parent;
+
+ /// <summary>
+ /// The position of this enumerator in the cached list.
+ /// </summary>
+ private int cachePosition = -1;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="EnumerableCache&lt;T&gt;.EnumeratorCache"/> class.
+ /// </summary>
+ /// <param name="parent">The parent cached enumerable whose GetEnumerator method is calling this constructor.</param>
+ internal EnumeratorCache(EnumerableCache<T> parent) {
+ if (parent == null) {
+ throw new ArgumentNullException("parent");
+ }
+
+ this.parent = parent;
+ }
+
+ #region IEnumerator<T> Members
+
+ /// <summary>
+ /// Gets the element in the collection at the current position of the enumerator.
+ /// </summary>
+ /// <returns>
+ /// The element in the collection at the current position of the enumerator.
+ /// </returns>
+ public T Current {
+ get {
+ if (this.cachePosition < 0 || this.cachePosition >= this.parent.cache.Count) {
+ throw new InvalidOperationException();
+ }
+
+ return this.parent.cache[this.cachePosition];
+ }
+ }
+
+ #endregion
+
+ #region IEnumerator Properties
+
+ /// <summary>
+ /// Gets the element in the collection at the current position of the enumerator.
+ /// </summary>
+ /// <returns>
+ /// The element in the collection at the current position of the enumerator.
+ /// </returns>
+ object System.Collections.IEnumerator.Current {
+ get { return this.Current; }
+ }
+
+ #endregion
+
+ #region IDisposable Members
+
+ /// <summary>
+ /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
+ /// </summary>
+ public void Dispose() {
+ this.Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ #endregion
+
+ #region IEnumerator Methods
+
+ /// <summary>
+ /// Advances the enumerator to the next element of the collection.
+ /// </summary>
+ /// <returns>
+ /// true if the enumerator was successfully advanced to the next element; false if the enumerator has passed the end of the collection.
+ /// </returns>
+ /// <exception cref="T:System.InvalidOperationException">
+ /// The collection was modified after the enumerator was created.
+ /// </exception>
+ public bool MoveNext() {
+ this.cachePosition++;
+ if (this.cachePosition >= this.parent.cache.Count) {
+ lock (this.parent.generatorLock) {
+ if (this.parent.generatorEnumerator.MoveNext()) {
+ this.parent.cache.Add(this.parent.generatorEnumerator.Current);
+ } else {
+ return false;
+ }
+ }
+ }
+
+ return true;
+ }
+
+ /// <summary>
+ /// Sets the enumerator to its initial position, which is before the first element in the collection.
+ /// </summary>
+ /// <exception cref="T:System.InvalidOperationException">
+ /// The collection was modified after the enumerator was created.
+ /// </exception>
+ public void Reset() {
+ this.cachePosition = -1;
+ }
+
+ #endregion
+
+ /// <summary>
+ /// Releases unmanaged and - optionally - managed resources
+ /// </summary>
+ /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
+ protected virtual void Dispose(bool disposing) {
+ // Nothing to do here.
+ }
+ }
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth/Messaging/ErrorUtilities.cs b/src/DotNetOpenAuth/Messaging/ErrorUtilities.cs
index 995df23..9f67796 100644
--- a/src/DotNetOpenAuth/Messaging/ErrorUtilities.cs
+++ b/src/DotNetOpenAuth/Messaging/ErrorUtilities.cs
@@ -35,10 +35,18 @@ namespace DotNetOpenAuth.Messaging {
/// Throws an internal error exception.
/// </summary>
/// <param name="errorMessage">The error message.</param>
+ /// <returns>Nothing. But included here so callers can "throw" this method for C# safety.</returns>
/// <exception cref="InternalErrorException">Always thrown.</exception>
[Pure]
- internal static void ThrowInternal(string errorMessage) {
- VerifyInternal(false, errorMessage);
+ internal static Exception ThrowInternal(string errorMessage) {
+ // Since internal errors are really bad, take this chance to
+ // help the developer find the cause by breaking into the
+ // debugger if one is attached.
+ if (Debugger.IsAttached) {
+ Debugger.Break();
+ }
+
+ throw new InternalErrorException(errorMessage);
}
/// <summary>
@@ -52,14 +60,7 @@ namespace DotNetOpenAuth.Messaging {
Contract.Ensures(condition);
Contract.EnsuresOnThrow<InternalErrorException>(!condition);
if (!condition) {
- // Since internal errors are really bad, take this chance to
- // help the developer find the cause by breaking into the
- // debugger if one is attached.
- if (Debugger.IsAttached) {
- Debugger.Break();
- }
-
- throw new InternalErrorException(errorMessage);
+ ThrowInternal(errorMessage);
}
}
diff --git a/src/DotNetOpenAuth/Messaging/StandardWebRequestHandler.cs b/src/DotNetOpenAuth/Messaging/StandardWebRequestHandler.cs
index e8d8fe1..98c9d93 100644
--- a/src/DotNetOpenAuth/Messaging/StandardWebRequestHandler.cs
+++ b/src/DotNetOpenAuth/Messaging/StandardWebRequestHandler.cs
@@ -140,6 +140,7 @@ namespace DotNetOpenAuth.Messaging {
// We don't want to blindly set all ServicePoints to not use the Expect header
// as that would be a security hole allowing any visitor to a web site change
// the web site's global behavior when calling that host.
+ Logger.Http.InfoFormat("HTTP POST to {0} resulted in 417 Expectation Failed. Changing ServicePoint to not use Expect: Continue next time.", request.RequestUri);
request.ServicePoint.Expect100Continue = false; // TODO: investigate that CAS may throw here
// An alternative to ServicePoint if we don't have permission to set that,
diff --git a/src/DotNetOpenAuth/OpenId/OpenIdStrings.Designer.cs b/src/DotNetOpenAuth/OpenId/OpenIdStrings.Designer.cs
index f0e8033..5d9903f 100644
--- a/src/DotNetOpenAuth/OpenId/OpenIdStrings.Designer.cs
+++ b/src/DotNetOpenAuth/OpenId/OpenIdStrings.Designer.cs
@@ -416,6 +416,15 @@ namespace DotNetOpenAuth.OpenId {
}
/// <summary>
+ /// Looks up a localized string similar to No identifier has been set..
+ /// </summary>
+ internal static string NoIdentifierSet {
+ get {
+ return ResourceManager.GetString("NoIdentifierSet", resourceCulture);
+ }
+ }
+
+ /// <summary>
/// Looks up a localized string similar to No XRDS document containing OpenID relying party endpoint information could be found at {0}..
/// </summary>
internal static string NoRelyingPartyEndpointDiscovered {
@@ -479,6 +488,15 @@ namespace DotNetOpenAuth.OpenId {
}
/// <summary>
+ /// Looks up a localized string similar to This property value is not supported by this control..
+ /// </summary>
+ internal static string PropertyValueNotSupported {
+ get {
+ return ResourceManager.GetString("PropertyValueNotSupported", resourceCulture);
+ }
+ }
+
+ /// <summary>
/// Looks up a localized string similar to Unable to determine the version of the OpenID protocol implemented by the Provider at endpoint &apos;{0}&apos;..
/// </summary>
internal static string ProviderVersionUnrecognized {
diff --git a/src/DotNetOpenAuth/OpenId/OpenIdStrings.resx b/src/DotNetOpenAuth/OpenId/OpenIdStrings.resx
index 4f10cf4..c9df6b3 100644
--- a/src/DotNetOpenAuth/OpenId/OpenIdStrings.resx
+++ b/src/DotNetOpenAuth/OpenId/OpenIdStrings.resx
@@ -325,4 +325,10 @@ Discovered endpoint info:
<data name="UnexpectedEnumPropertyValue" xml:space="preserve">
<value>The property {0} had unexpected value {1}.</value>
</data>
+ <data name="NoIdentifierSet" xml:space="preserve">
+ <value>No identifier has been set.</value>
+ </data>
+ <data name="PropertyValueNotSupported" xml:space="preserve">
+ <value>This property value is not supported by this control.</value>
+ </data>
</root> \ No newline at end of file
diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdAjaxTextBox.js b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdAjaxTextBox.js
index 7d3cbfc..e13af30 100644
--- a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdAjaxTextBox.js
+++ b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdAjaxTextBox.js
@@ -743,8 +743,10 @@ function Uri(url) {
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])))
+ var equalsAt = queryStringPairs[i].indexOf('=');
+ left = (equalsAt >= 0) ? queryStringPairs[i].substring(0, equalsAt) : null;
+ right = (equalsAt >= 0) ? queryStringPairs[i].substring(equalsAt + 1) : queryStringPairs[i];
+ this.Pairs.push(new KeyValuePair(unescape(left), unescape(right)));
}
};
diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdButton.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdButton.cs
new file mode 100644
index 0000000..c6a5476
--- /dev/null
+++ b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdButton.cs
@@ -0,0 +1,137 @@
+//-----------------------------------------------------------------------
+// <copyright file="OpenIdButton.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OpenId.RelyingParty {
+ using System;
+ using System.Collections.Generic;
+ using System.ComponentModel;
+ using System.Diagnostics.CodeAnalysis;
+ using System.Drawing.Design;
+ using System.Globalization;
+ using System.Linq;
+ using System.Text;
+ using System.Web.UI;
+ using DotNetOpenAuth.Messaging;
+
+ /// <summary>
+ /// An ASP.NET control that renders a button that initiates an
+ /// authentication when clicked.
+ /// </summary>
+ public class OpenIdButton : OpenIdRelyingPartyControlBase {
+ #region Property defaults
+
+ /// <summary>
+ /// The default value for the <see cref="Text"/> property.
+ /// </summary>
+ private const string TextDefault = "Log in with [Provider]!";
+
+ #endregion
+
+ #region View state keys
+
+ /// <summary>
+ /// The key under which the value for the <see cref="Text"/> property will be stored.
+ /// </summary>
+ private const string TextViewStateKey = "Text";
+
+ /// <summary>
+ /// The key under which the value for the <see cref="ImageUrl"/> property will be stored.
+ /// </summary>
+ private const string ImageUrlViewStateKey = "ImageUrl";
+
+ #endregion
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="OpenIdButton"/> class.
+ /// </summary>
+ public OpenIdButton() {
+ }
+
+ /// <summary>
+ /// Gets or sets the text to display for the link.
+ /// </summary>
+ [Bindable(true), DefaultValue(TextDefault), Category(AppearanceCategory)]
+ [Description("The text to display for the link.")]
+ public string Text {
+ get { return (string)ViewState[TextViewStateKey] ?? TextDefault; }
+ set { ViewState[TextViewStateKey] = value; }
+ }
+
+ /// <summary>
+ /// Gets or sets the image to display.
+ /// </summary>
+ [SuppressMessage("Microsoft.Design", "CA1056:UriPropertiesShouldNotBeStrings", Justification = "Bindable property must be simple type")]
+ [Bindable(true), Category(AppearanceCategory)]
+ [Description("The image to display.")]
+ [UrlProperty, Editor("System.Web.UI.Design.UrlEditor, System.Design, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a", typeof(UITypeEditor))]
+ public string ImageUrl {
+ get {
+ return (string)ViewState[ImageUrlViewStateKey];
+ }
+
+ set {
+ UriUtil.ValidateResolvableUrl(Page, DesignMode, value);
+ ViewState[ImageUrlViewStateKey] = value;
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets a value indicating when to use a popup window to complete the login experience.
+ /// </summary>
+ /// <value>The default value is <see cref="PopupBehavior.Never"/>.</value>
+ [Bindable(false), Browsable(false)]
+ public override PopupBehavior Popup {
+ get { return base.Popup; }
+ set { ErrorUtilities.VerifySupported(value == base.Popup, OpenIdStrings.PropertyValueNotSupported); }
+ }
+
+ /// <summary>
+ /// Raises the <see cref="E:System.Web.UI.Control.PreRender"/> event.
+ /// </summary>
+ /// <param name="e">An <see cref="T:System.EventArgs"/> object that contains the event data.</param>
+ protected override void OnPreRender(EventArgs e) {
+ base.OnPreRender(e);
+
+ if (!this.DesignMode) {
+ ErrorUtilities.VerifyOperation(this.Identifier != null, OpenIdStrings.NoIdentifierSet);
+ }
+ }
+
+ /// <summary>
+ /// Sends server control content to a provided <see cref="T:System.Web.UI.HtmlTextWriter"/> object, which writes the content to be rendered on the client.
+ /// </summary>
+ /// <param name="writer">The <see cref="T:System.Web.UI.HtmlTextWriter"/> object that receives the server control content.</param>
+ protected override void Render(HtmlTextWriter writer) {
+ if (string.IsNullOrEmpty(this.Identifier)) {
+ writer.WriteEncodedText(string.Format(CultureInfo.CurrentCulture, "[{0}]", OpenIdStrings.NoIdentifierSet));
+ } else {
+ string tooltip = this.Text;
+ IAuthenticationRequest request = this.CreateRequests().FirstOrDefault();
+ if (request != null) {
+ RenderOpenIdMessageTransmissionAsAnchorAttributes(writer, request, tooltip);
+ } else {
+ tooltip = OpenIdStrings.OpenIdEndpointNotFound;
+ }
+
+ writer.AddAttribute(HtmlTextWriterAttribute.Title, tooltip);
+ writer.RenderBeginTag(HtmlTextWriterTag.A);
+
+ if (!string.IsNullOrEmpty(this.ImageUrl)) {
+ writer.AddAttribute(HtmlTextWriterAttribute.Src, this.ResolveClientUrl(this.ImageUrl));
+ writer.AddAttribute(HtmlTextWriterAttribute.Border, "0");
+ writer.AddAttribute(HtmlTextWriterAttribute.Alt, this.Text);
+ writer.AddAttribute(HtmlTextWriterAttribute.Title, this.Text);
+ writer.RenderBeginTag(HtmlTextWriterTag.Img);
+ writer.RenderEndTag();
+ } else if (!string.IsNullOrEmpty(this.Text)) {
+ writer.WriteEncodedText(this.Text);
+ }
+
+ writer.RenderEndTag();
+ }
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingPartyAjaxControlBase.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingPartyAjaxControlBase.cs
new file mode 100644
index 0000000..5ab8053
--- /dev/null
+++ b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingPartyAjaxControlBase.cs
@@ -0,0 +1,320 @@
+//-----------------------------------------------------------------------
+// <copyright file="OpenIdRelyingPartyAjaxControlBase.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+[assembly: System.Web.UI.WebResource(DotNetOpenAuth.OpenId.RelyingParty.OpenIdRelyingPartyAjaxControlBase.EmbeddedAjaxJavascriptResource, "text/javascript")]
+
+namespace DotNetOpenAuth.OpenId.RelyingParty {
+ using System;
+ using System.Collections.Generic;
+ using System.ComponentModel;
+ using System.Diagnostics.CodeAnalysis;
+ using System.Diagnostics.Contracts;
+ using System.Drawing.Design;
+ using System.Globalization;
+ using System.Linq;
+ using System.Text;
+ using System.Text.RegularExpressions;
+ using System.Web;
+ using System.Web.Security;
+ using System.Web.UI;
+ using DotNetOpenAuth.ComponentModel;
+ using DotNetOpenAuth.Configuration;
+ using DotNetOpenAuth.Messaging;
+ using DotNetOpenAuth.OpenId.Extensions;
+ using DotNetOpenAuth.OpenId.Extensions.UI;
+
+ /// <summary>
+ /// A common base class for OpenID Relying Party controls.
+ /// </summary>
+ internal abstract class OpenIdRelyingPartyAjaxControlBase : OpenIdRelyingPartyControlBase, ICallbackEventHandler {
+ /// <summary>
+ /// The manifest resource name of the javascript file to include on the hosting page.
+ /// </summary>
+ internal const string EmbeddedAjaxJavascriptResource = Util.DefaultNamespace + ".OpenId.RelyingParty.OpenIdRelyingPartyAjaxControlBase.js";
+
+ /// <summary>
+ /// The name of the javascript function that will initiate a synchronous callback.
+ /// </summary>
+ protected const string CallbackJsFunction = "window.dnoa_internal.callback";
+
+ /// <summary>
+ /// The name of the javascript function that will initiate an asynchronous callback.
+ /// </summary>
+ protected const string CallbackJsFunctionAsync = "window.dnoa_internal.callbackAsync";
+
+ /// <summary>
+ /// Stores the result of a AJAX callback discovery.
+ /// </summary>
+ private string discoveryResult;
+
+ /// <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>
+ /// Initializes a new instance of the <see cref="OpenIdRelyingPartyAjaxControlBase"/> class.
+ /// </summary>
+ protected OpenIdRelyingPartyAjaxControlBase() {
+ // The AJAX login style always uses popups (or invisible iframes).
+ this.Popup = PopupBehavior.Always;
+ }
+
+ /// <summary>
+ /// Gets or sets a value indicating when to use a popup window to complete the login experience.
+ /// </summary>
+ /// <value>The default value is <see cref="PopupBehavior.Never"/>.</value>
+ [Bindable(false), Browsable(false)]
+ public override PopupBehavior Popup {
+ get { return base.Popup; }
+ set { ErrorUtilities.VerifySupported(value == base.Popup, OpenIdStrings.PropertyValueNotSupported); }
+ }
+
+ #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.OpenId.InfoFormat("AJAX discovery on {0} requested.", userSuppliedIdentifier);
+
+ // We prepare a JSON object with this interface:
+ // class jsonResponse {
+ // string claimedIdentifier;
+ // Array requests; // never null
+ // string error; // null if no error
+ // }
+ // Each element in the requests array looks like this:
+ // class jsonAuthRequest {
+ // string endpoint; // URL to the OP endpoint
+ // string immediate; // URL to initiate an immediate request
+ // string setup; // URL to initiate a setup request.
+ // }
+ StringBuilder discoveryResultBuilder = new StringBuilder();
+ discoveryResultBuilder.Append("{");
+ try {
+ this.Identifier = userSuppliedIdentifier;
+ IEnumerable<IAuthenticationRequest> requests = this.CreateRequests().CacheGeneratedResults();
+ if (requests.Any()) {
+ discoveryResultBuilder.AppendFormat("claimedIdentifier: {0},", MessagingUtilities.GetSafeJavascriptValue(requests.First().ClaimedIdentifier));
+ discoveryResultBuilder.Append("requests: [");
+ foreach (IAuthenticationRequest request in requests) {
+ this.OnLoggingIn(request);
+ discoveryResultBuilder.Append("{");
+ discoveryResultBuilder.AppendFormat("endpoint: {0},", MessagingUtilities.GetSafeJavascriptValue(request.Provider.Uri.AbsoluteUri));
+ request.Mode = AuthenticationRequestMode.Immediate;
+ OutgoingWebResponse response = request.RedirectingResponse;
+ discoveryResultBuilder.AppendFormat("immediate: {0},", MessagingUtilities.GetSafeJavascriptValue(response.GetDirectUriRequest(this.RelyingParty.Channel).AbsoluteUri));
+ request.Mode = AuthenticationRequestMode.Setup;
+ response = request.RedirectingResponse;
+ discoveryResultBuilder.AppendFormat("setup: {0}", MessagingUtilities.GetSafeJavascriptValue(response.GetDirectUriRequest(this.RelyingParty.Channel).AbsoluteUri));
+ discoveryResultBuilder.Append("},");
+ }
+ discoveryResultBuilder.Length -= 1; // trim off last comma
+ discoveryResultBuilder.Append("]");
+ } else {
+ discoveryResultBuilder.Append("requests: new Array(),");
+ discoveryResultBuilder.AppendFormat("error: {0}", MessagingUtilities.GetSafeJavascriptValue(OpenIdStrings.OpenIdEndpointNotFound));
+ }
+ } catch (ProtocolException ex) {
+ discoveryResultBuilder.Append("requests: new Array(),");
+ discoveryResultBuilder.AppendFormat("error: {0}", MessagingUtilities.GetSafeJavascriptValue(ex.Message));
+ }
+ discoveryResultBuilder.Append("}");
+ this.discoveryResult = discoveryResultBuilder.ToString();
+ }
+
+ #endregion
+
+ /// <summary>
+ /// Creates the authentication requests for a given user-supplied Identifier.
+ /// </summary>
+ /// <returns>A sequence of authentication requests, any one of which may be
+ /// used to determine the user's control of the <see cref="IAuthenticationRequest.ClaimedIdentifier"/>.</returns>
+ protected override IEnumerable<IAuthenticationRequest> CreateRequests() {
+ Contract.Requires(this.Identifier != null, OpenIdStrings.NoIdentifierSet);
+ ErrorUtilities.VerifyOperation(this.Identifier != null, OpenIdStrings.NoIdentifierSet);
+
+ // We delegate all our logic to another method, since invoking base. methods
+ // within an iterator method results in unverifiable code.
+ return this.CreateRequestsCore(base.CreateRequests());
+ }
+
+ /// <summary>
+ /// Raises the <see cref="E:System.Web.UI.Control.PreRender"/> event.
+ /// </summary>
+ /// <param name="e">An <see cref="T:System.EventArgs"/> object that contains the event data.</param>
+ protected override void OnPreRender(EventArgs e) {
+ base.OnPreRender(e);
+
+ this.Page.ClientScript.RegisterClientScriptResource(typeof(OpenIdRelyingPartyAjaxControlBase), EmbeddedAjaxJavascriptResource);
+
+ StringBuilder initScript = new StringBuilder();
+
+ initScript.AppendLine(CallbackJsFunctionAsync + " = " + this.GetJsCallbackConvenienceFunction(true));
+ initScript.AppendLine(CallbackJsFunction + " = " + this.GetJsCallbackConvenienceFunction(false));
+
+ this.Page.ClientScript.RegisterClientScriptBlock(typeof(OpenIdRelyingPartyControlBase), "initializer", initScript.ToString(), true);
+ }
+
+ /// <summary>
+ /// Creates the authentication requests for a given user-supplied Identifier.
+ /// </summary>
+ /// <param name="requests">The authentication requests to prepare.</param>
+ /// <returns>
+ /// A sequence of authentication requests, any one of which may be
+ /// used to determine the user's control of the <see cref="IAuthenticationRequest.ClaimedIdentifier"/>.
+ /// </returns>
+ private IEnumerable<IAuthenticationRequest> CreateRequestsCore(IEnumerable<IAuthenticationRequest> requests) {
+ Contract.Requires(requests != null);
+
+ // Configure each generated request.
+ int reqIndex = 0;
+ foreach (var req in requests) {
+ req.AddCallbackArguments("index", (reqIndex++).ToString(CultureInfo.InvariantCulture));
+
+ if (req.Provider.IsExtensionSupported<UIRequest>()) {
+ // Provide a hint for the client javascript about whether the OP supports the UI extension.
+ // This is so the window can be made the correct size for the extension.
+ // If the OP doesn't advertise support for the extension, the javascript will use
+ // a bigger popup window.
+ req.AddCallbackArguments("dotnetopenid.popupUISupported", "1");
+ }
+
+ // If the ReturnToUrl was explicitly set, we'll need to reset our first parameter
+ if (string.IsNullOrEmpty(HttpUtility.ParseQueryString(req.ReturnToUrl.Query)["dotnetopenid.userSuppliedIdentifier"])) {
+ req.AddCallbackArguments("dotnetopenid.userSuppliedIdentifier", this.Identifier);
+ }
+
+ // Our javascript needs to let the user know which endpoint responded. So we force it here.
+ // This gives us the info even for 1.0 OPs and 2.0 setup_required responses.
+ req.AddCallbackArguments("dotnetopenid.op_endpoint", req.Provider.Uri.AbsoluteUri);
+ req.AddCallbackArguments("dotnetopenid.claimed_id", (string)req.ClaimedIdentifier ?? string.Empty);
+
+ // We append a # at the end so that if the OP happens to support it,
+ // the OpenID response "query string" is appended after the hash rather than before, resulting in the
+ // browser being super-speedy in closing the popup window since it doesn't try to pull a newer version
+ // of the static resource down from the server merely because of a changed URL.
+ // http://www.nabble.com/Re:-Defining-how-OpenID-should-behave-with-fragments-in-the-return_to-url-p22694227.html
+ ////TODO:
+
+ yield return req;
+ }
+ }
+
+ /// <summary>
+ /// Constructs a function that will initiate an AJAX callback.
+ /// </summary>
+ /// <param name="async">if set to <c>true</c> causes the AJAX callback to be a little more asynchronous. Note that <c>false</c> does not mean the call is absolutely synchronous.</param>
+ /// <returns>The string defining a javascript anonymous function that initiates a callback.</returns>
+ private string GetJsCallbackConvenienceFunction(bool async) {
+ string argumentParameterName = "argument";
+ string callbackResultParameterName = "resultFunction";
+ string callbackErrorCallbackParameterName = "errorCallback";
+ string callback = Page.ClientScript.GetCallbackEventReference(
+ this,
+ argumentParameterName,
+ callbackResultParameterName,
+ argumentParameterName,
+ callbackErrorCallbackParameterName,
+ async);
+ return string.Format(
+ CultureInfo.InvariantCulture,
+ "function({1}, {2}, {3}) {{{0}\treturn {4};{0}}};",
+ Environment.NewLine,
+ argumentParameterName,
+ callbackResultParameterName,
+ callbackErrorCallbackParameterName,
+ callback);
+ }
+
+ /// <summary>
+ /// Notifies the user agent via an AJAX response of a completed authentication attempt.
+ /// </summary>
+ private void ReportAuthenticationResult() {
+ Logger.OpenId.InfoFormat("AJAX (iframe) callback from OP: {0}", this.Page.Request.Url);
+ List<string> assignments = new List<string>();
+
+ var authResponse = this.RelyingParty.GetResponse();
+ if (authResponse.Status == AuthenticationStatus.Authenticated) {
+ this.OnLoggedIn(authResponse);
+ foreach (var pair in this.clientScriptExtensions) {
+ IClientScriptExtensionResponse extension = (IClientScriptExtensionResponse)authResponse.GetExtension(pair.Key);
+ if (extension == null) {
+ continue;
+ }
+ var positiveResponse = (PositiveAuthenticationResponse)authResponse;
+ string js = extension.InitializeJavaScriptData(positiveResponse.Response);
+ if (string.IsNullOrEmpty(js)) {
+ js = "null";
+ }
+ assignments.Add(pair.Value + " = " + js);
+ }
+ }
+
+ this.CallbackUserAgentMethod("dnoi_internal.processAuthorizationResult(document.URL)", assignments.ToArray());
+ }
+
+ /// <summary>
+ /// Invokes a method on a parent frame/window's OpenIdAjaxTextBox,
+ /// and closes the calling popup window if applicable.
+ /// </summary>
+ /// <param name="methodCall">The method to call on the OpenIdAjaxTextBox, including
+ /// parameters. (i.e. "callback('arg1', 2)"). No escaping is done by this method.</param>
+ private void CallbackUserAgentMethod(string methodCall) {
+ this.CallbackUserAgentMethod(methodCall, null);
+ }
+
+ /// <summary>
+ /// Invokes a method on a parent frame/window's OpenIdAjaxTextBox,
+ /// and closes the calling popup window if applicable.
+ /// </summary>
+ /// <param name="methodCall">The method to call on the OpenIdAjaxTextBox, including
+ /// parameters. (i.e. "callback('arg1', 2)"). No escaping is done by this method.</param>
+ /// <param name="preAssignments">An optional list of assignments to make to the input box object before placing the method call.</param>
+ private void CallbackUserAgentMethod(string methodCall, string[] preAssignments) {
+ Logger.OpenId.InfoFormat("Sending Javascript callback: {0}", methodCall);
+ Page.Response.Write(@"<html><body><script language='javascript'>
+ var inPopup = !window.frameElement;
+ var objSrc = inPopup ? window.opener.waiting_openidBox : window.frameElement.openidBox;
+");
+ if (preAssignments != null) {
+ foreach (string assignment in preAssignments) {
+ Page.Response.Write(string.Format(CultureInfo.InvariantCulture, " objSrc.{0};\n", assignment));
+ }
+ }
+
+ // Something about calling objSrc.{0} can somehow cause FireFox to forget about the inPopup variable,
+ // so we have to actually put the test for it ABOVE the call to objSrc.{0} so that it already
+ // whether to call window.self.close() after the call.
+ string htmlFormat = @" if (inPopup) {{
+ objSrc.{0};
+ window.self.close();
+}} else {{
+ objSrc.{0};
+}}
+</script></body></html>";
+ Page.Response.Write(string.Format(CultureInfo.InvariantCulture, htmlFormat, methodCall));
+ Page.Response.End();
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingPartyAjaxControlBase.js b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingPartyAjaxControlBase.js
new file mode 100644
index 0000000..65b1b99
--- /dev/null
+++ b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingPartyAjaxControlBase.js
@@ -0,0 +1,150 @@
+//-----------------------------------------------------------------------
+// <copyright file="OpenIdRelyingPartyControlBase.js" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+if (window.dnoa_internal === undefined) {
+ window.dnoa_internal = new Object();
+};
+
+window.dnoa_internal.discoveryResults = new Array(); // user supplied identifiers and discovery results
+
+/// <summary>Instantiates an object that represents an OpenID Identifier.</summary>
+window.OpenId = function(identifier) {
+ /// <summary>Performs discovery on the identifier.</summary>
+ /// <param name="onCompleted">A function(DiscoveryResult) callback to be called when discovery has completed.</param>
+ this.discover = function(onCompleted) {
+ /// <summary>Instantiates an object that stores discovery results of some identifier.</summary>
+ function DiscoveryResult(identifier, discoveryInfo) {
+ /// <summary>
+ /// Instantiates an object that describes an OpenID service endpoint and facilitates
+ /// initiating and tracking an authentication request.
+ /// </summary>
+ function ServiceEndpoint(requestInfo, userSuppliedIdentifier) {
+ this.immediate = requestInfo.immediate ? new window.dnoa_internal.Uri(requestInfo.immediate) : null;
+ this.setup = requestInfo.setup ? new window.dnoa_internal.Uri(requestInfo.setup) : null;
+ this.endpoint = new window.dnoa_internal.Uri(requestInfo.endpoint);
+ this.userSuppliedIdentifier = userSuppliedIdentifier;
+ var self = this; // closure so that delegates have the right instance
+ this.loginPopup = function(onSuccess, onFailure) {
+ //self.abort(); // ensure no concurrent attempts
+ window.dnoa_internal.processAuthorizationResult = function(childLocation) {
+ window.dnoa_internal.processAuthorizationResult = null;
+ trace('Received event from child window: ' + childLocation);
+ var success = true; // TODO: discern between success and failure, and fire the correct event.
+
+ if (success) {
+ if (onSuccess) {
+ onSuccess();
+ }
+ } else {
+ if (onFailure) {
+ onFailure();
+ }
+ }
+ };
+ var width = 800;
+ var height = 600;
+ if (self.setup.getQueryArgValue("openid.return_to").indexOf("dotnetopenid.popupUISupported") >= 0) {
+ width = 450;
+ height = 500;
+ }
+
+ var left = (screen.width - width) / 2;
+ var top = (screen.height - height) / 2;
+ self.popup = window.open(self.setup, 'opLogin', 'status=0,toolbar=0,location=1,resizable=1,scrollbars=1,left=' + left + ',top=' + top + ',width=' + width + ',height=' + height);
+
+ // If the OP supports the UI extension it MAY close its own window
+ // for a negative assertion. We must be able to recover from that scenario.
+ var localSelf = self;
+ self.popupCloseChecker = window.setInterval(function() {
+ if (localSelf.popup && localSelf.popup.closed) {
+ // The window closed, either because the user closed it, canceled at the OP,
+ // or approved at the OP and the popup window closed itself due to our script.
+ // If we were graying out the entire page while the child window was up,
+ // we would probably revert that here.
+ window.clearInterval(localSelf.popupCloseChecker);
+ localSelf.popup = null;
+
+ // The popup may have managed to inform us of the result already,
+ // so check whether the callback method was cleared already, which
+ // would indicate we've already processed this.
+ if (window.dnoa_internal.processAuthorizationResult) {
+ trace('User or OP canceled by closing the window.');
+ if (onFailure) {
+ onFailure();
+ }
+ window.dnoa_internal.processAuthorizationResult = null;
+ }
+ }
+ }, 250);
+ };
+ };
+
+ this.userSuppliedIdentifier = identifier;
+ this.claimedIdentifier = discoveryInfo.claimedIdentifier; // The claimed identifier may be null if the user provided an OP Identifier.
+ trace('Discovered claimed identifier: ' + (this.claimedIdentifier ? this.claimedIdentifier : "(directed identity)"));
+
+ if (discoveryInfo) {
+ this.length = discoveryInfo.requests.length;
+ for (var i = 0; i < discoveryInfo.requests.length; i++) {
+ this[i] = new ServiceEndpoint(discoveryInfo.requests[i], identifier);
+ }
+ } else {
+ this.length = 0;
+ }
+ };
+
+ /// <summary>Receives the results of a successful discovery (even if it yielded 0 results).</summary>
+ function successCallback(discoveryResult, identifier) {
+ trace('Discovery completed for: ' + identifier);
+
+ // Deserialize the JSON object and store the result if it was a successful discovery.
+ discoveryResult = eval('(' + discoveryResult + ')');
+
+ // Add behavior for later use.
+ discoveryResult = new DiscoveryResult(identifier, discoveryResult);
+ window.dnoa_internal.discoveryResults[identifier] = discoveryResult;
+
+ if (onCompleted) {
+ onCompleted(discoveryResult);
+ }
+ };
+
+ /// <summary>Receives the discovery failure notification.</summary>
+ failureCallback = function(message, userSuppliedIdentifier) {
+ trace('Discovery failed for: ' + identifier);
+
+ if (onCompleted) {
+ onCompleted();
+ }
+ };
+
+ if (window.dnoa_internal.discoveryResults[identifier]) {
+ trace("We've already discovered " + identifier + " so we're skipping it this time.");
+ onCompleted(window.dnoa_internal.discoveryResults[identifier]);
+ }
+
+ trace('starting discovery on ' + identifier);
+ window.dnoa_internal.callbackAsync(identifier, successCallback, failureCallback);
+ };
+
+ /// <summary>Performs discovery and immediately begins checkid_setup to authenticate the user using a given identifier.</summary>
+ this.login = function(onSuccess, onFailure) {
+ this.discover(function(discoveryResult) {
+ if (discoveryResult) {
+ trace('Discovery succeeded and found ' + discoveryResult.length + ' OpenID service endpoints.');
+ if (discoveryResult.length > 0) {
+ discoveryResult[0].loginPopup(onSuccess, onFailure);
+ } else {
+ trace("This doesn't look like an OpenID Identifier. Aborting login.");
+ if (onFailure) {
+ onFailure();
+ }
+ }
+ }
+ });
+ };
+};
+
diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingPartyControlBase.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingPartyControlBase.cs
new file mode 100644
index 0000000..05c16f6
--- /dev/null
+++ b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingPartyControlBase.cs
@@ -0,0 +1,749 @@
+//-----------------------------------------------------------------------
+// <copyright file="OpenIdRelyingPartyControlBase.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+[assembly: System.Web.UI.WebResource(DotNetOpenAuth.OpenId.RelyingParty.OpenIdRelyingPartyControlBase.EmbeddedJavascriptResource, "text/javascript")]
+
+namespace DotNetOpenAuth.OpenId.RelyingParty {
+ using System;
+ using System.Collections.Generic;
+ using System.ComponentModel;
+ using System.Diagnostics.CodeAnalysis;
+ using System.Diagnostics.Contracts;
+ using System.Drawing.Design;
+ using System.Globalization;
+ using System.Linq;
+ using System.Text;
+ using System.Text.RegularExpressions;
+ using System.Web;
+ using System.Web.Security;
+ using System.Web.UI;
+ using DotNetOpenAuth.ComponentModel;
+ using DotNetOpenAuth.Configuration;
+ using DotNetOpenAuth.Messaging;
+ using DotNetOpenAuth.OpenId.Extensions;
+ using DotNetOpenAuth.OpenId.Extensions.UI;
+
+ /// <summary>
+ /// A common base class for OpenID Relying Party controls.
+ /// </summary>
+ [DefaultProperty("Identifier"), ValidationProperty("Identifier")]
+ public abstract class OpenIdRelyingPartyControlBase : Control {
+ /// <summary>
+ /// The manifest resource name of the javascript file to include on the hosting page.
+ /// </summary>
+ internal const string EmbeddedJavascriptResource = Util.DefaultNamespace + ".OpenId.RelyingParty.OpenIdRelyingPartyControlBase.js";
+
+ #region Property category constants
+
+ /// <summary>
+ /// The "Appearance" category for properties.
+ /// </summary>
+ protected const string AppearanceCategory = "Appearance";
+
+ /// <summary>
+ /// The "Behavior" category for properties.
+ /// </summary>
+ protected const string BehaviorCategory = "Behavior";
+
+ #endregion
+
+ #region Property default values
+
+ /// <summary>
+ /// The default value for the <see cref="Stateless"/> property.
+ /// </summary>
+ private const bool StatelessDefault = false;
+
+ /// <summary>
+ /// The default value for the <see cref="ReturnToUrl"/> property.
+ /// </summary>
+ private const string ReturnToUrlDefault = "";
+
+ /// <summary>
+ /// Default value of <see cref="UsePersistentCookie"/>.
+ /// </summary>
+ private const bool UsePersistentCookieDefault = false;
+
+ /// <summary>
+ /// The default value for the <see cref="RealmUrl"/> property.
+ /// </summary>
+ private const string RealmUrlDefault = "~/";
+
+ /// <summary>
+ /// The default value for the <see cref="Popup"/> property.
+ /// </summary>
+ private const PopupBehavior PopupDefault = PopupBehavior.Never;
+
+ /// <summary>
+ /// The default value for the <see cref="RequireSsl"/> property.
+ /// </summary>
+ private const bool RequireSslDefault = false;
+
+ #endregion
+
+ #region Property view state keys
+
+ /// <summary>
+ /// The viewstate key to use for the <see cref="Stateless"/> property.
+ /// </summary>
+ private const string StatelessViewStateKey = "Stateless";
+
+ /// <summary>
+ /// The viewstate key to use for the <see cref="UsePersistentCookie"/> property.
+ /// </summary>
+ private const string UsePersistentCookieViewStateKey = "UsePersistentCookie";
+
+ /// <summary>
+ /// The viewstate key to use for the <see cref="RealmUrl"/> property.
+ /// </summary>
+ private const string RealmUrlViewStateKey = "RealmUrl";
+
+ /// <summary>
+ /// The viewstate key to use for the <see cref="ReturnToUrl"/> property.
+ /// </summary>
+ private const string ReturnToUrlViewStateKey = "ReturnToUrl";
+
+ /// <summary>
+ /// The key under which the value for the <see cref="Identifier"/> property will be stored.
+ /// </summary>
+ private const string IdentifierViewStateKey = "Identifier";
+
+ /// <summary>
+ /// The viewstate key to use for the <see cref="Popup"/> property.
+ /// </summary>
+ private const string PopupViewStateKey = "Popup";
+
+ /// <summary>
+ /// The viewstate key to use for the <see cref="RequireSsl"/> property.
+ /// </summary>
+ private const string RequireSslViewStateKey = "RequireSsl";
+
+ #endregion
+
+ #region Callback parameter names
+
+ /// <summary>
+ /// The callback parameter for use with persisting the <see cref="UsePersistentCookie"/> property.
+ /// </summary>
+ private const string UsePersistentCookieCallbackKey = OpenIdUtilities.CustomParameterPrefix + "UsePersistentCookie";
+
+ /// <summary>
+ /// The callback parameter to use for recognizing when the callback is in a popup window.
+ /// </summary>
+ private const string UIPopupCallbackKey = OpenIdUtilities.CustomParameterPrefix + "uipopup";
+
+ /// <summary>
+ /// The callback parameter to use for recognizing when the callback is in the parent window.
+ /// </summary>
+ private const string UIPopupCallbackParentKey = OpenIdUtilities.CustomParameterPrefix + "uipopupParent";
+
+ /// <summary>
+ /// The callback parameter name to use to store which control initiated the auth request.
+ /// </summary>
+ private const string ReturnToReceivingControlId = OpenIdUtilities.CustomParameterPrefix + "receiver";
+
+ /// <summary>
+ /// The parameter name to include in the formulated auth request so that javascript can know whether
+ /// the OP advertises support for the UI extension.
+ /// </summary>
+ private const string PopupUISupportedJsHint = "dotnetopenid.popupUISupported";
+
+ #endregion
+
+ /// <summary>
+ /// Backing field for the <see cref="RelyingParty"/> property.
+ /// </summary>
+ private OpenIdRelyingParty relyingParty;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="OpenIdRelyingPartyControlBase"/> class.
+ /// </summary>
+ protected OpenIdRelyingPartyControlBase() {
+ }
+
+ #region Events
+
+ /// <summary>
+ /// Fired after the user clicks the log in button, but before the authentication
+ /// process begins. Offers a chance for the web application to disallow based on
+ /// OpenID URL before redirecting the user to the OpenID Provider.
+ /// </summary>
+ [Description("Fired after the user clicks the log in button, but before the authentication process begins. Offers a chance for the web application to disallow based on OpenID URL before redirecting the user to the OpenID Provider.")]
+ public event EventHandler<OpenIdEventArgs> LoggingIn;
+
+ /// <summary>
+ /// Fired upon completion of a successful login.
+ /// </summary>
+ [Description("Fired upon completion of a successful login.")]
+ public event EventHandler<OpenIdEventArgs> LoggedIn;
+
+ /// <summary>
+ /// Fired when a login attempt fails.
+ /// </summary>
+ [Description("Fired when a login attempt fails.")]
+ public event EventHandler<OpenIdEventArgs> Failed;
+
+ /// <summary>
+ /// Fired when an authentication attempt is canceled at the OpenID Provider.
+ /// </summary>
+ [Description("Fired when an authentication attempt is canceled at the OpenID Provider.")]
+ public event EventHandler<OpenIdEventArgs> Canceled;
+
+ #endregion
+
+ /// <summary>
+ /// Gets or sets the <see cref="OpenIdRelyingParty"/> instance to use.
+ /// </summary>
+ /// <value>The default value is an <see cref="OpenIdRelyingParty"/> instance initialized according to the web.config file.</value>
+ /// <remarks>
+ /// A performance optimization would be to store off the
+ /// instance as a static member in your web site and set it
+ /// to this property in your <see cref="Control.Load">Page.Load</see>
+ /// event since instantiating these instances can be expensive on
+ /// heavily trafficked web pages.
+ /// </remarks>
+ [Browsable(false)]
+ public OpenIdRelyingParty RelyingParty {
+ get {
+ if (this.relyingParty == null) {
+ this.relyingParty = this.CreateRelyingParty();
+ }
+ return this.relyingParty;
+ }
+
+ set {
+ this.relyingParty = value;
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether stateless mode is used.
+ /// </summary>
+ [Bindable(true), DefaultValue(StatelessDefault), Category(BehaviorCategory)]
+ [Description("Controls whether stateless mode is used.")]
+ public bool Stateless {
+ get { return (bool)(ViewState[StatelessViewStateKey] ?? StatelessDefault); }
+ set { ViewState[StatelessViewStateKey] = value; }
+ }
+
+ /// <summary>
+ /// Gets or sets the OpenID <see cref="Realm"/> of the relying party web site.
+ /// </summary>
+ [SuppressMessage("Microsoft.Usage", "CA1806:DoNotIgnoreMethodResults", MessageId = "System.Uri", Justification = "Using Uri.ctor for validation.")]
+ [SuppressMessage("Microsoft.Usage", "CA1806:DoNotIgnoreMethodResults", MessageId = "DotNetOpenAuth.OpenId.Realm", Justification = "Using ctor for validation.")]
+ [SuppressMessage("Microsoft.Design", "CA1056:UriPropertiesShouldNotBeStrings", Justification = "Bindable property must be simple type")]
+ [Bindable(true), DefaultValue(RealmUrlDefault), Category(BehaviorCategory)]
+ [Description("The OpenID Realm of the relying party web site.")]
+ [UrlProperty, Editor("System.Web.UI.Design.UrlEditor, System.Design, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a", typeof(UITypeEditor))]
+ public string RealmUrl {
+ get {
+ return (string)(ViewState[RealmUrlViewStateKey] ?? RealmUrlDefault);
+ }
+
+ set {
+ if (Page != null && !DesignMode) {
+ // Validate new value by trying to construct a Realm object based on it.
+ new Realm(OpenIdUtilities.GetResolvedRealm(this.Page, value, this.RelyingParty.Channel.GetRequestFromContext())); // throws an exception on failure.
+ } else {
+ // We can't fully test it, but it should start with either ~/ or a protocol.
+ if (Regex.IsMatch(value, @"^https?://")) {
+ new Uri(value.Replace("*.", string.Empty)); // make sure it's fully-qualified, but ignore wildcards
+ } else if (value.StartsWith("~/", StringComparison.Ordinal)) {
+ // this is valid too
+ } else {
+ throw new UriFormatException();
+ }
+ }
+ ViewState[RealmUrlViewStateKey] = value;
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets the OpenID ReturnTo of the relying party web site.
+ /// </summary>
+ [SuppressMessage("Microsoft.Usage", "CA2234:PassSystemUriObjectsInsteadOfStrings", Justification = "Bindable property must be simple type")]
+ [SuppressMessage("Microsoft.Usage", "CA1806:DoNotIgnoreMethodResults", MessageId = "System.Uri", Justification = "Using Uri.ctor for validation.")]
+ [SuppressMessage("Microsoft.Design", "CA1056:UriPropertiesShouldNotBeStrings", Justification = "Bindable property must be simple type")]
+ [Bindable(true), DefaultValue(ReturnToUrlDefault), Category(BehaviorCategory)]
+ [Description("The OpenID ReturnTo of the relying party web site.")]
+ [UrlProperty, Editor("System.Web.UI.Design.UrlEditor, System.Design, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a", typeof(UITypeEditor))]
+ public string ReturnToUrl {
+ get {
+ return (string)(this.ViewState[ReturnToUrlViewStateKey] ?? ReturnToUrlDefault);
+ }
+
+ set {
+ if (this.Page != null && !this.DesignMode) {
+ // Validate new value by trying to construct a Uri based on it.
+ new Uri(this.RelyingParty.Channel.GetRequestFromContext().UrlBeforeRewriting, this.Page.ResolveUrl(value)); // throws an exception on failure.
+ } else {
+ // We can't fully test it, but it should start with either ~/ or a protocol.
+ if (Regex.IsMatch(value, @"^https?://")) {
+ new Uri(value); // make sure it's fully-qualified, but ignore wildcards
+ } else if (value.StartsWith("~/", StringComparison.Ordinal)) {
+ // this is valid too
+ } else {
+ throw new UriFormatException();
+ }
+ }
+
+ this.ViewState[ReturnToUrlViewStateKey] = value;
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether to send a persistent cookie upon successful
+ /// login so the user does not have to log in upon returning to this site.
+ /// </summary>
+ [Bindable(true), DefaultValue(UsePersistentCookieDefault), Category(BehaviorCategory)]
+ [Description("Whether to send a persistent cookie upon successful " +
+ "login so the user does not have to log in upon returning to this site.")]
+ public virtual bool UsePersistentCookie {
+ get { return (bool)(this.ViewState[UsePersistentCookieViewStateKey] ?? UsePersistentCookieDefault); }
+ set { this.ViewState[UsePersistentCookieViewStateKey] = value; }
+ }
+
+ /// <summary>
+ /// Gets or sets a value indicating when to use a popup window to complete the login experience.
+ /// </summary>
+ /// <value>The default value is <see cref="PopupBehavior.Never"/>.</value>
+ [Bindable(true), DefaultValue(PopupDefault), Category(BehaviorCategory)]
+ [Description("When to use a popup window to complete the login experience.")]
+ public virtual PopupBehavior Popup {
+ get { return (PopupBehavior)(ViewState[PopupViewStateKey] ?? PopupDefault); }
+ set { ViewState[PopupViewStateKey] = value; }
+ }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether to enforce on high security mode,
+ /// which requires the full authentication pipeline to be protected by SSL.
+ /// </summary>
+ [Bindable(true), DefaultValue(RequireSslDefault), Category(BehaviorCategory)]
+ [Description("Turns on high security mode, requiring the full authentication pipeline to be protected by SSL.")]
+ public bool RequireSsl {
+ get { return (bool)(ViewState[RequireSslViewStateKey] ?? RequireSslDefault); }
+ set { ViewState[RequireSslViewStateKey] = value; }
+ }
+
+ /// <summary>
+ /// Gets or sets the URL to your privacy policy page that describes how
+ /// claims will be used and/or shared.
+ /// </summary>
+ [Bindable(true), Category(BehaviorCategory)]
+ [Description("The OpenID Identifier that this button will use to initiate login.")]
+ [TypeConverter(typeof(IdentifierConverter))]
+ public Identifier Identifier {
+ get { return (Identifier)ViewState[IdentifierViewStateKey]; }
+ set { ViewState[IdentifierViewStateKey] = value; }
+ }
+
+ /// <summary>
+ /// Gets or sets the default association preference to set on authentication requests.
+ /// </summary>
+ internal AssociationPreference AssociationPreference { get; set; }
+
+ /// <summary>
+ /// Immediately redirects to the OpenID Provider to verify the Identifier
+ /// provided in the text box.
+ /// </summary>
+ public void LogOn() {
+ IAuthenticationRequest request = this.CreateRequests().FirstOrDefault();
+ if (this.IsPopupAppropriate(request)) {
+ this.ScriptPopupWindow(request);
+ } else {
+ request.RedirectToProvider();
+ }
+ }
+
+ /// <summary>
+ /// Creates the authentication requests for a given user-supplied Identifier.
+ /// </summary>
+ /// <returns>A sequence of authentication requests, any one of which may be
+ /// used to determine the user's control of the <see cref="IAuthenticationRequest.ClaimedIdentifier"/>.</returns>
+ protected virtual IEnumerable<IAuthenticationRequest> CreateRequests() {
+ Contract.Requires(this.Identifier != null, OpenIdStrings.NoIdentifierSet);
+ ErrorUtilities.VerifyOperation(this.Identifier != null, OpenIdStrings.NoIdentifierSet);
+ IEnumerable<IAuthenticationRequest> requests;
+
+ // Approximate the returnTo (either based on the customize property or the page URL)
+ // so we can use it to help with Realm resolution.
+ Uri returnToApproximation = this.ReturnToUrl != null ? new Uri(this.RelyingParty.Channel.GetRequestFromContext().UrlBeforeRewriting, this.ReturnToUrl) : this.Page.Request.Url;
+
+ // Resolve the trust root, and swap out the scheme and port if necessary to match the
+ // return_to URL, since this match is required by OpenId, and the consumer app
+ // may be using HTTP at some times and HTTPS at others.
+ UriBuilder realm = OpenIdUtilities.GetResolvedRealm(this.Page, this.RealmUrl, this.RelyingParty.Channel.GetRequestFromContext());
+ realm.Scheme = returnToApproximation.Scheme;
+ realm.Port = returnToApproximation.Port;
+
+ // Initiate openid request
+ // We use TryParse here to avoid throwing an exception which
+ // might slip through our validator control if it is disabled.
+ Realm typedRealm = new Realm(realm);
+ if (string.IsNullOrEmpty(this.ReturnToUrl)) {
+ requests = this.RelyingParty.CreateRequests(this.Identifier, typedRealm);
+ } else {
+ // Since the user actually gave us a return_to value,
+ // the "approximation" is exactly what we want.
+ requests = this.RelyingParty.CreateRequests(this.Identifier, typedRealm, returnToApproximation);
+ }
+
+ // Some OPs may be listed multiple times (one with HTTPS and the other with HTTP, for example).
+ // Since we're gathering OPs to try one after the other, just take the first choice of each OP
+ // and don't try it multiple times.
+ requests = requests.Distinct(DuplicateRequestedHostsComparer.Instance);
+
+ // Configure each generated request.
+ foreach (var req in requests) {
+ if (this.IsPopupAppropriate(req)) {
+ // Inform the OP that we'll be using a popup window.
+ req.AddExtension(new UIRequest());
+
+ // Inform ourselves in return_to that we're in a popup.
+ req.AddCallbackArguments(UIPopupCallbackKey, "1");
+
+ if (req.Provider.IsExtensionSupported<UIRequest>()) {
+ // Provide a hint for the client javascript about whether the OP supports the UI extension.
+ // This is so the window can be made the correct size for the extension.
+ // If the OP doesn't advertise support for the extension, the javascript will use
+ // a bigger popup window.
+ req.AddCallbackArguments(PopupUISupportedJsHint, "1");
+ }
+ }
+
+ // Add state that needs to survive across the redirect.
+ if (!this.Stateless) {
+ req.AddCallbackArguments(UsePersistentCookieCallbackKey, this.UsePersistentCookie.ToString(CultureInfo.InvariantCulture));
+ req.AddCallbackArguments(ReturnToReceivingControlId, this.ClientID);
+ }
+
+ ((AuthenticationRequest)req).AssociationPreference = this.AssociationPreference;
+ this.OnLoggingIn(req);
+
+ yield return req;
+ }
+ }
+
+ /// <summary>
+ /// Raises the <see cref="E:Load"/> event.
+ /// </summary>
+ /// <param name="e">The <see cref="System.EventArgs"/> instance containing the event data.</param>
+ protected override void OnLoad(EventArgs e) {
+ base.OnLoad(e);
+
+ if (Page.IsPostBack) {
+ // OpenID responses NEVER come in the form of a postback.
+ return;
+ }
+
+ // Take an unreliable sneek peek to see if we're in a popup and an OpenID
+ // assertion is coming in. We shouldn't process assertions in a popup window.
+ if (this.Page.Request.QueryString[UIPopupCallbackKey] == "1" && this.Page.Request.QueryString[UIPopupCallbackParentKey] == null) {
+ // We're in a popup window. We need to close it and pass the
+ // message back to the parent window for processing.
+ this.ScriptClosingPopup();
+ return; // don't do any more processing on it now
+ }
+
+ // Only sniff for an OpenID response if it is targeted at this control. Note that
+ // Stateless mode causes no receiver to be indicated.
+ string receiver = this.Page.Request.QueryString[ReturnToReceivingControlId] ?? this.Page.Request.Form[ReturnToReceivingControlId];
+ if (receiver == null || receiver == this.ClientID) {
+ var response = this.RelyingParty.GetResponse();
+ if (response != null) {
+ string persistentString = response.GetCallbackArgument(UsePersistentCookieCallbackKey);
+ bool persistentBool;
+ if (persistentString != null && bool.TryParse(persistentString, out persistentBool)) {
+ this.UsePersistentCookie = persistentBool;
+ }
+
+ switch (response.Status) {
+ case AuthenticationStatus.Authenticated:
+ this.OnLoggedIn(response);
+ break;
+ case AuthenticationStatus.Canceled:
+ this.OnCanceled(response);
+ break;
+ case AuthenticationStatus.Failed:
+ this.OnFailed(response);
+ break;
+ case AuthenticationStatus.SetupRequired:
+ case AuthenticationStatus.ExtensionsOnly:
+ default:
+ // The NotApplicable (extension-only assertion) is NOT one that we support
+ // in this control because that scenario is primarily interesting to RPs
+ // that are asking a specific OP, and it is not user-initiated as this textbox
+ // is designed for.
+ throw new InvalidOperationException(MessagingStrings.UnexpectedMessageReceivedOfMany);
+ }
+ }
+ }
+ }
+
+ /// <summary>
+ /// Raises the <see cref="E:System.Web.UI.Control.PreRender"/> event.
+ /// </summary>
+ /// <param name="e">An <see cref="T:System.EventArgs"/> object that contains the event data.</param>
+ protected override void OnPreRender(EventArgs e) {
+ base.OnPreRender(e);
+
+ this.Page.ClientScript.RegisterClientScriptResource(typeof(OpenIdRelyingPartyControlBase), EmbeddedJavascriptResource);
+ }
+
+ /// <summary>
+ /// Fires the <see cref="LoggedIn"/> event.
+ /// </summary>
+ /// <param name="response">The response.</param>
+ protected virtual void OnLoggedIn(IAuthenticationResponse response) {
+ Contract.Requires(response != null);
+ Contract.Requires(response.Status == AuthenticationStatus.Authenticated);
+ ErrorUtilities.VerifyArgumentNotNull(response, "response");
+ ErrorUtilities.VerifyInternal(response.Status == AuthenticationStatus.Authenticated, "Firing OnLoggedIn event without an authenticated response.");
+
+ var loggedIn = this.LoggedIn;
+ OpenIdEventArgs args = new OpenIdEventArgs(response);
+ if (loggedIn != null) {
+ loggedIn(this, args);
+ }
+
+ if (!args.Cancel) {
+ FormsAuthentication.RedirectFromLoginPage(response.ClaimedIdentifier, this.UsePersistentCookie);
+ }
+ }
+
+ /// <summary>
+ /// Fires the <see cref="LoggingIn"/> event.
+ /// </summary>
+ /// <param name="request">The request.</param>
+ /// <returns>
+ /// Returns whether the login should proceed. False if some event handler canceled the request.
+ /// </returns>
+ protected virtual bool OnLoggingIn(IAuthenticationRequest request) {
+ Contract.Requires(request != null);
+ ErrorUtilities.VerifyArgumentNotNull(request, "request");
+
+ EventHandler<OpenIdEventArgs> loggingIn = this.LoggingIn;
+
+ OpenIdEventArgs args = new OpenIdEventArgs(request);
+ if (loggingIn != null) {
+ loggingIn(this, args);
+ }
+
+ return !args.Cancel;
+ }
+
+ /// <summary>
+ /// Fires the <see cref="Canceled"/> event.
+ /// </summary>
+ /// <param name="response">The response.</param>
+ protected virtual void OnCanceled(IAuthenticationResponse response) {
+ Contract.Requires(response != null);
+ Contract.Requires(response.Status == AuthenticationStatus.Canceled);
+ ErrorUtilities.VerifyArgumentNotNull(response, "response");
+ ErrorUtilities.VerifyInternal(response.Status == AuthenticationStatus.Canceled, "Firing Canceled event for the wrong response type.");
+
+ var canceled = this.Canceled;
+ if (canceled != null) {
+ canceled(this, new OpenIdEventArgs(response));
+ }
+ }
+
+ /// <summary>
+ /// Fires the <see cref="Failed"/> event.
+ /// </summary>
+ /// <param name="response">The response.</param>
+ protected virtual void OnFailed(IAuthenticationResponse response) {
+ Contract.Requires(response != null);
+ Contract.Requires(response.Status == AuthenticationStatus.Failed);
+ ErrorUtilities.VerifyArgumentNotNull(response, "response");
+ ErrorUtilities.VerifyInternal(response.Status == AuthenticationStatus.Failed, "Firing Failed event for the wrong response type.");
+
+ var failed = this.Failed;
+ if (failed != null) {
+ failed(this, new OpenIdEventArgs(response));
+ }
+ }
+
+ /// <summary>
+ /// Creates the relying party instance used to generate authentication requests.
+ /// </summary>
+ /// <returns>The instantiated relying party.</returns>
+ protected virtual OpenIdRelyingParty CreateRelyingParty() {
+ IRelyingPartyApplicationStore store = this.Stateless ? null : DotNetOpenAuthSection.Configuration.OpenId.RelyingParty.ApplicationStore.CreateInstance(OpenIdRelyingParty.HttpApplicationStore);
+ var rp = new OpenIdRelyingParty(store);
+
+ // Only set RequireSsl to true, as we don't want to override
+ // a .config setting of true with false.
+ if (this.RequireSsl) {
+ rp.SecuritySettings.RequireSsl = true;
+ }
+
+ return rp;
+ }
+
+ /// <summary>
+ /// Detects whether a popup window should be used to show the Provider's UI.
+ /// </summary>
+ /// <param name="request">The request.</param>
+ /// <returns>
+ /// <c>true</c> if a popup should be used; <c>false</c> otherwise.
+ /// </returns>
+ protected virtual bool IsPopupAppropriate(IAuthenticationRequest request) {
+ Contract.Requires(request != null);
+ ErrorUtilities.VerifyArgumentNotNull(request, "request");
+
+ switch (this.Popup) {
+ case PopupBehavior.Never:
+ return false;
+ case PopupBehavior.Always:
+ return true;
+ case PopupBehavior.IfProviderSupported:
+ return request.Provider.IsExtensionSupported<UIRequest>();
+ default:
+ throw ErrorUtilities.ThrowInternal("Unexpected value for Popup property.");
+ }
+ }
+
+ /// <summary>
+ /// Adds attributes to an HTML &lt;A&gt; tag that will be written by the caller using
+ /// <see cref="HtmlTextWriter.RenderBeginTag(HtmlTextWriterTag)"/> after this method.
+ /// </summary>
+ /// <param name="writer">The HTML writer.</param>
+ /// <param name="request">The outgoing authentication request.</param>
+ /// <param name="windowStatus">The text to try to display in the status bar on mouse hover.</param>
+ protected void RenderOpenIdMessageTransmissionAsAnchorAttributes(HtmlTextWriter writer, IAuthenticationRequest request, string windowStatus) {
+ Contract.Requires(writer != null);
+ Contract.Requires(request != null);
+ ErrorUtilities.VerifyArgumentNotNull(writer, "writer");
+ ErrorUtilities.VerifyArgumentNotNull(request, "request");
+
+ // We render a standard HREF attribute for non-javascript browsers.
+ writer.AddAttribute(HtmlTextWriterAttribute.Href, request.RedirectingResponse.GetDirectUriRequest(this.RelyingParty.Channel).AbsoluteUri);
+
+ // And for the Javascript ones we do the extra work to use form POST where necessary.
+ writer.AddAttribute(HtmlTextWriterAttribute.Onclick, this.CreateGetOrPostAHrefValue(request) + " return false;");
+
+ writer.AddStyleAttribute(HtmlTextWriterStyle.Cursor, "pointer");
+ if (!string.IsNullOrEmpty(windowStatus)) {
+ writer.AddAttribute("onMouseOver", "window.status = " + MessagingUtilities.GetSafeJavascriptValue(windowStatus));
+ writer.AddAttribute("onMouseOut", "window.status = null");
+ }
+ }
+
+ /// <summary>
+ /// Gets the javascript to executee to redirect or POST an OpenID message to a remote party.
+ /// </summary>
+ /// <param name="request">The authentication request to send.</param>
+ /// <returns>The javascript that should execute.</returns>
+ private string CreateGetOrPostAHrefValue(IAuthenticationRequest request) {
+ Contract.Requires(request != null);
+ ErrorUtilities.VerifyArgumentNotNull(request, "request");
+
+ Uri directUri = request.RedirectingResponse.GetDirectUriRequest(this.RelyingParty.Channel);
+ return "window.dnoa_internal.GetOrPost(" + MessagingUtilities.GetSafeJavascriptValue(directUri.AbsoluteUri) + ");";
+ }
+
+ /// <summary>
+ /// Wires the return page to immediately display a popup window with the Provider in it.
+ /// </summary>
+ /// <param name="request">The request.</param>
+ private void ScriptPopupWindow(IAuthenticationRequest request) {
+ Contract.Requires(request != null);
+ Contract.Requires(this.RelyingParty != null);
+
+ StringBuilder startupScript = new StringBuilder();
+
+ // Add a callback function that the popup window can call on this, the
+ // parent window, to pass back the authentication result.
+ startupScript.AppendLine("window.dnoa_internal = new Object();");
+ startupScript.AppendLine("window.dnoa_internal.processAuthorizationResult = function(uri) { window.location = uri; };");
+ startupScript.AppendLine("window.dnoa_internal.popupWindow = function() {");
+ startupScript.AppendFormat(
+ @"\tvar openidPopup = {0}",
+ UIUtilities.GetWindowPopupScript(this.RelyingParty, request, "openidPopup"));
+ startupScript.AppendLine("};");
+
+ this.Page.ClientScript.RegisterClientScriptBlock(this.GetType(), "loginPopup", startupScript.ToString(), true);
+ }
+
+ /// <summary>
+ /// Wires the popup window to close itself and pass the authentication result to the parent window.
+ /// </summary>
+ private void ScriptClosingPopup() {
+ StringBuilder startupScript = new StringBuilder();
+ startupScript.AppendLine("window.opener.dnoa_internal.processAuthorizationResult(document.URL + '&" + UIPopupCallbackParentKey + "=1');");
+ startupScript.AppendLine("window.close();");
+
+ this.Page.ClientScript.RegisterStartupScript(typeof(OpenIdRelyingPartyControlBase), "loginPopupClose", startupScript.ToString(), true);
+
+ // TODO: alternately we should probably take over rendering this page here to avoid
+ // a lot of unnecessary work on the server and possible momentary display of the
+ // page in the popup window.
+ }
+
+ /// <summary>
+ /// An authentication request comparer that judges equality solely on the OP endpoint hostname.
+ /// </summary>
+ private class DuplicateRequestedHostsComparer : IEqualityComparer<IAuthenticationRequest> {
+ /// <summary>
+ /// The singleton instance of this comparer.
+ /// </summary>
+ private static IEqualityComparer<IAuthenticationRequest> instance = new DuplicateRequestedHostsComparer();
+
+ /// <summary>
+ /// Prevents a default instance of the <see cref="DuplicateRequestedHostsComparer"/> class from being created.
+ /// </summary>
+ private DuplicateRequestedHostsComparer() {
+ }
+
+ /// <summary>
+ /// Gets the singleton instance of this comparer.
+ /// </summary>
+ internal static IEqualityComparer<IAuthenticationRequest> Instance {
+ get { return instance; }
+ }
+
+ #region IEqualityComparer<IAuthenticationRequest> Members
+
+ /// <summary>
+ /// Determines whether the specified objects are equal.
+ /// </summary>
+ /// <param name="x">The first object of type <paramref name="T"/> to compare.</param>
+ /// <param name="y">The second object of type <paramref name="T"/> to compare.</param>
+ /// <returns>
+ /// true if the specified objects are equal; otherwise, false.
+ /// </returns>
+ public bool Equals(IAuthenticationRequest x, IAuthenticationRequest y) {
+ if (x == null && y == null) {
+ return true;
+ }
+
+ if (x == null || y == null) {
+ return false;
+ }
+
+ // 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.
+ return string.Equals(x.Provider.Uri.Host, y.Provider.Uri.Host, StringComparison.OrdinalIgnoreCase);
+ }
+
+ /// <summary>
+ /// Returns a hash code for the specified object.
+ /// </summary>
+ /// <param name="obj">The <see cref="T:System.Object"/> for which a hash code is to be returned.</param>
+ /// <returns>A hash code for the specified object.</returns>
+ /// <exception cref="T:System.ArgumentNullException">
+ /// The type of <paramref name="obj"/> is a reference type and <paramref name="obj"/> is null.
+ /// </exception>
+ public int GetHashCode(IAuthenticationRequest obj) {
+ return obj.Provider.Uri.Host.GetHashCode();
+ }
+
+ #endregion
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingPartyControlBase.js b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingPartyControlBase.js
new file mode 100644
index 0000000..3a17b7b
--- /dev/null
+++ b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingPartyControlBase.js
@@ -0,0 +1,174 @@
+//-----------------------------------------------------------------------
+// <copyright file="OpenIdRelyingPartyControlBase.js" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+// Options that can be set on the host page:
+//window.openid_visible_iframe = true; // causes the hidden iframe to show up
+//window.openid_trace = true; // causes lots of messages
+
+trace = function(msg) {
+ if (window.openid_trace) {
+ if (!window.openid_tracediv) {
+ window.openid_tracediv = document.createElement("ol");
+ document.body.appendChild(window.openid_tracediv);
+ }
+ var el = document.createElement("li");
+ el.appendChild(document.createTextNode(msg));
+ window.openid_tracediv.appendChild(el);
+ //alert(msg);
+ }
+};
+
+if (window.dnoa_internal === undefined) {
+ window.dnoa_internal = new Object();
+};
+
+// The possible authentication results
+window.dnoa_internal.authSuccess = new Object();
+window.dnoa_internal.authRefused = new Object();
+window.dnoa_internal.timedOut = new Object();
+
+/// <summary>Instantiates an object that provides string manipulation services for URIs.</summary>
+window.dnoa_internal.Uri = function(url) {
+ this.originalUri = url.toString();
+
+ 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 window.dnoa_internal.Uri(this.originalUri.substr(0, hashmark));
+ }
+ return this;
+ };
+
+ this.appendQueryVariable = function(name, value) {
+ var pair = encodeURI(name) + "=" + encodeURI(value);
+ if (this.originalUri.indexOf('?') >= 0) {
+ this.originalUri = this.originalUri + "&" + pair;
+ } else {
+ this.originalUri = this.originalUri + "?" + pair;
+ }
+ };
+
+ function KeyValuePair(key, value) {
+ this.key = key;
+ this.value = value;
+ };
+
+ this.pairs = new Array();
+
+ var queryBeginsAt = this.originalUri.indexOf('?');
+ if (queryBeginsAt >= 0) {
+ this.queryString = url.substr(queryBeginsAt + 1);
+ var queryStringPairs = this.queryString.split('&');
+
+ for (var i = 0; i < queryStringPairs.length; i++) {
+ var equalsAt = queryStringPairs[i].indexOf('=');
+ left = (equalsAt >= 0) ? queryStringPairs[i].substring(0, equalsAt) : null;
+ right = (equalsAt >= 0) ? queryStringPairs[i].substring(equalsAt + 1) : queryStringPairs[i];
+ this.pairs.push(new KeyValuePair(unescape(left), unescape(right)));
+ }
+ };
+
+ this.getQueryArgValue = function(key) {
+ for (var i = 0; i < this.pairs.length; i++) {
+ if (this.pairs[i].key == key) {
+ return this.pairs[i].value;
+ }
+ }
+ };
+
+ this.getPairs = function() {
+ return this.pairs;
+ }
+
+ this.containsQueryArg = function(key) {
+ return this.getQueryArgValue(key);
+ };
+
+ this.getUriWithoutQueryOrFragement = function() {
+ var queryBeginsAt = this.originalUri.indexOf('?');
+ if (queryBeginsAt >= 0) {
+ return this.originalUri.substring(0, queryBeginsAt);
+ } else {
+ var fragmentBeginsAt = this.originalUri.indexOf('#');
+ if (fragmentBeginsAt >= 0) {
+ return this.originalUri.substring(0, fragmentBeginsAt);
+ } else {
+ return this.originalUri;
+ }
+ }
+ };
+
+ this.indexOf = function(args) {
+ return this.originalUri.indexOf(args);
+ };
+
+ return this;
+};
+
+/// <summary>Creates a hidden iframe.</summary>
+window.dnoa_internal.createHiddenIFrame = function() {
+ 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("border", 0);
+ }
+
+ return iframe;
+}
+
+/// <summary>Redirects the current window/frame to the given URI,
+/// either using a GET or a POST as required by the length of the URL.</summary>
+window.dnoa_internal.GetOrPost = function(uri) {
+ var maxGetLength = 2 * 1024; // keep in sync with DotNetOpenAuth.Messaging.Channel.IndirectMessageGetToPostThreshold
+ uri = new window.dnoa_internal.Uri(uri);
+
+ if (uri.toString().length <= maxGetLength) {
+ window.location = uri.toString();
+ } else {
+ trace("Preparing to POST: " + uri.toString());
+ var iframe = window.dnoa_internal.createHiddenIFrame();
+ document.body.appendChild(iframe);
+ var doc = iframe.ownerDocument;
+ var form = doc.createElement('form');
+ form.action = uri.getUriWithoutQueryOrFragement();
+ form.method = "POST";
+ form.target = "_top";
+ for (var i = 0; i < uri.getPairs().length; i++) {
+ var input = doc.createElement('input');
+ input.type = 'hidden';
+ input.name = uri.getPairs()[i].key;
+ input.value = uri.getPairs()[i].value;
+ trace(input.name + " = " + input.value);
+ form.appendChild(input);
+ }
+ doc.body.appendChild(form);
+ form.submit();
+ }
+};
diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdTextBox.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdTextBox.cs
index 0c87b1f..b7c879e 100644
--- a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdTextBox.cs
+++ b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdTextBox.cs
@@ -1254,7 +1254,9 @@ namespace DotNetOpenAuth.OpenId.RelyingParty {
startupScript.AppendLine("window.opener.dnoa_internal.processAuthorizationResult(document.URL + '&" + UIPopupCallbackParentKey + "=1');");
startupScript.AppendLine("window.close();");
- this.Page.ClientScript.RegisterStartupScript(this.GetType(), "loginPopupClose", startupScript.ToString(), true);
+ // We're referencing the OpenIdRelyingPartyControlBase type here to avoid double-registering this script
+ // if the other control exists on the page.
+ this.Page.ClientScript.RegisterStartupScript(typeof(OpenIdRelyingPartyControlBase), "loginPopupClose", startupScript.ToString(), true);
}
}
}
diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/WellKnownProviders.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/WellKnownProviders.cs
new file mode 100644
index 0000000..bd45842
--- /dev/null
+++ b/src/DotNetOpenAuth/OpenId/RelyingParty/WellKnownProviders.cs
@@ -0,0 +1,33 @@
+//-----------------------------------------------------------------------
+// <copyright file="WellKnownProviders.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OpenId.RelyingParty {
+ /// <summary>
+ /// Common OpenID Provider Identifiers.
+ /// </summary>
+ public sealed class WellKnownProviders {
+ /// <summary>
+ /// The Yahoo OP Identifier.
+ /// </summary>
+ public static readonly Identifier Yahoo = "https://me.yahoo.com/";
+
+ /// <summary>
+ /// The Google OP Identifier.
+ /// </summary>
+ public static readonly Identifier Google = "https://www.google.com/accounts/o8/id";
+
+ /// <summary>
+ /// The MyOpenID OP Identifier.
+ /// </summary>
+ public static readonly Identifier MyOpenId = "https://www.myopenid.com/";
+
+ /// <summary>
+ /// Prevents a default instance of the <see cref="WellKnownProviders"/> class from being created.
+ /// </summary>
+ private WellKnownProviders() {
+ }
+ }
+}