diff options
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<T>"/> 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<T>"/> types and returns a caching <see cref="IEnumerator<T>"/> + /// from its <see cref="IEnumerable<T>.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<T>"/> 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<T>"/> 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<T>.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 '{0}'.. /// </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 <A> 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() { + } + } +} |