summaryrefslogtreecommitdiffstats
path: root/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId
diff options
context:
space:
mode:
Diffstat (limited to 'src/DotNetOpenAuth.OpenId.RelyingParty/OpenId')
-rw-r--r--src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/Behaviors/AXFetchAsSregTransform.cs75
-rw-r--r--src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/Behaviors/GsaIcamRelyingPartyProfile.cs123
-rw-r--r--src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/ChannelElements/ExtensionsBindingElementRelyingParty.cs28
-rw-r--r--src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/ChannelElements/OpenIdRelyingPartyChannel.cs119
-rw-r--r--src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/ChannelElements/RelyingPartySecurityOptions.cs98
-rw-r--r--src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/ChannelElements/RelyingPartySigningBindingElement.cs86
-rw-r--r--src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/ChannelElements/ReturnToNonceBindingElement.cs291
-rw-r--r--src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/Extensions/ExtensionsInteropRelyingPartyHelper.cs151
-rw-r--r--src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/Extensions/UI/UIUtilities.cs52
-rw-r--r--src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/HostMetaDiscoveryService.cs516
-rw-r--r--src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/IIdentifierDiscoveryService.cs67
-rw-r--r--src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/IdentifierDiscoveryResult.cs497
-rw-r--r--src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/Interop/AuthenticationResponseShim.cs120
-rw-r--r--src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/Interop/ClaimsResponseShim.cs108
-rw-r--r--src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/Interop/OpenIdRelyingPartyShim.cs190
-rw-r--r--src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/Messages/AssociateDiffieHellmanResponse.cs51
-rw-r--r--src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/Messages/AssociateRequestRelyingParty.cs70
-rw-r--r--src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/Messages/AssociateSuccessfulResponseContract.cs17
-rw-r--r--src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/Messages/AssociateSuccessfulResponseRelyingParty.cs15
-rw-r--r--src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/Messages/AssociateUnencryptedResponseRelyingParty.cs20
-rw-r--r--src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/Mvc/OpenIdAjaxOptions.cs76
-rw-r--r--src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/Mvc/OpenIdHelper.cs431
-rw-r--r--src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/OpenIdXrdsHelper.cs163
-rw-r--r--src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/ProviderEndpointDescription.cs134
-rw-r--r--src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/AssociationManager.cs246
-rw-r--r--src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/AssociationPreference.cs42
-rw-r--r--src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/Associations.cs127
-rw-r--r--src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/AuthenticationRequest.cs594
-rw-r--r--src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/AuthenticationStatus.cs43
-rw-r--r--src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/Controls.cd112
-rw-r--r--src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/CryptoKeyStoreAsRelyingPartyAssociationStore.cs87
-rw-r--r--src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/DuplicateRequestedHostsComparer.cs74
-rw-r--r--src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/FailedAuthenticationResponse.cs299
-rw-r--r--src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/IAuthenticationRequest.cs186
-rw-r--r--src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/IAuthenticationRequestContract.cs111
-rw-r--r--src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/IAuthenticationResponse.cs532
-rw-r--r--src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/IProviderEndpoint.cs144
-rw-r--r--src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/IRelyingPartyAssociationStore.cs153
-rw-r--r--src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/IRelyingPartyBehavior.cs92
-rw-r--r--src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/ISetupRequiredAuthenticationResponse.cs51
-rw-r--r--src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/NegativeAuthenticationResponse.cs312
-rw-r--r--src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/OpenIdAjaxRelyingParty.cs242
-rw-r--r--src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/OpenIdAjaxTextBox.cs877
-rw-r--r--src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/OpenIdAjaxTextBox.css49
-rw-r--r--src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/OpenIdAjaxTextBox.js644
-rw-r--r--src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/OpenIdButton.cs179
-rw-r--r--src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/OpenIdEventArgs.cs74
-rw-r--r--src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/OpenIdLogin.cs1001
-rw-r--r--src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/OpenIdMobileTextBox.cs778
-rw-r--r--src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/OpenIdRelyingParty.cd1
-rw-r--r--src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/OpenIdRelyingParty.cs891
-rw-r--r--src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/OpenIdRelyingPartyAjaxControlBase.cs468
-rw-r--r--src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/OpenIdRelyingPartyAjaxControlBase.js751
-rw-r--r--src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/OpenIdRelyingPartyControlBase.cs1054
-rw-r--r--src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/OpenIdRelyingPartyControlBase.js172
-rw-r--r--src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/OpenIdSelector.cs455
-rw-r--r--src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/OpenIdSelector.css109
-rw-r--r--src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/OpenIdSelector.js196
-rw-r--r--src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/OpenIdTextBox.cs708
-rw-r--r--src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/PopupBehavior.cs31
-rw-r--r--src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/PositiveAnonymousResponse.cs347
-rw-r--r--src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/PositiveAuthenticationResponse.cs174
-rw-r--r--src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/PositiveAuthenticationResponseSnapshot.cs304
-rw-r--r--src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/RelyingPartySecuritySettings.cs187
-rw-r--r--src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/SelectorButton.cs46
-rw-r--r--src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/SelectorButtonContract.cs46
-rw-r--r--src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/SelectorInfoCardButton.cs102
-rw-r--r--src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/SelectorOpenIdButton.cs82
-rw-r--r--src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/SelectorProviderButton.cs113
-rw-r--r--src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/SimpleXrdsProviderEndpoint.cs78
-rw-r--r--src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/StandardRelyingPartyApplicationStore.cs110
-rw-r--r--src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/WellKnownProviders.cs52
-rw-r--r--src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/login_failure.pngbin0 -> 714 bytes
-rw-r--r--src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/login_success (lock).pngbin0 -> 571 bytes
-rw-r--r--src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/login_success.pngbin0 -> 464 bytes
-rw-r--r--src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/openid_login.pngbin0 -> 457 bytes
-rw-r--r--src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/spinner.gifbin0 -> 725 bytes
-rw-r--r--src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/UriDiscoveryService.cs139
-rw-r--r--src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/XriDiscoveryProxyService.cs108
79 files changed, 17271 insertions, 0 deletions
diff --git a/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/Behaviors/AXFetchAsSregTransform.cs b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/Behaviors/AXFetchAsSregTransform.cs
new file mode 100644
index 0000000..70dbe64
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/Behaviors/AXFetchAsSregTransform.cs
@@ -0,0 +1,75 @@
+//-----------------------------------------------------------------------
+// <copyright file="AXFetchAsSregTransform.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OpenId.Behaviors {
+ using System;
+ using System.Collections.Generic;
+ using System.Diagnostics.CodeAnalysis;
+ using System.Linq;
+ using System.Text;
+ using DotNetOpenAuth.Messaging;
+ using DotNetOpenAuth.OpenId.Extensions;
+ using DotNetOpenAuth.OpenId.Extensions.AttributeExchange;
+ using DotNetOpenAuth.OpenId.Extensions.SimpleRegistration;
+ using DotNetOpenAuth.OpenId.Provider;
+ using DotNetOpenAuth.OpenId.RelyingParty;
+
+ /// <summary>
+ /// An Attribute Exchange and Simple Registration filter to make all incoming attribute
+ /// requests look like Simple Registration requests, and to convert the response
+ /// to the originally requested extension and format.
+ /// </summary>
+ [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Sreg", Justification = "Abbreviation")]
+ public sealed class AXFetchAsSregRelyingPartyTransform : AXFetchAsSregTransform, IRelyingPartyBehavior {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="AXFetchAsSregRelyingPartyTransform"/> class.
+ /// </summary>
+ public AXFetchAsSregRelyingPartyTransform() {
+ }
+
+ #region IRelyingPartyBehavior Members
+
+ /// <summary>
+ /// Applies a well known set of security requirements to a default set of security settings.
+ /// </summary>
+ /// <param name="securitySettings">The security settings to enhance with the requirements of this profile.</param>
+ /// <remarks>
+ /// Care should be taken to never decrease security when applying a profile.
+ /// Profiles should only enhance security requirements to avoid being
+ /// incompatible with each other.
+ /// </remarks>
+ void IRelyingPartyBehavior.ApplySecuritySettings(RelyingPartySecuritySettings securitySettings) {
+ }
+
+ /// <summary>
+ /// Called when an authentication request is about to be sent.
+ /// </summary>
+ /// <param name="request">The request.</param>
+ /// <remarks>
+ /// Implementations should be prepared to be called multiple times on the same outgoing message
+ /// without malfunctioning.
+ /// </remarks>
+ void IRelyingPartyBehavior.OnOutgoingAuthenticationRequest(RelyingParty.IAuthenticationRequest request) {
+ // Don't create AX extensions for OpenID 1.x messages, since AX requires OpenID 2.0.
+ if (request.Provider.Version.Major >= 2) {
+ request.SpreadSregToAX(AXFormats);
+ }
+ }
+
+ /// <summary>
+ /// Called when an incoming positive assertion is received.
+ /// </summary>
+ /// <param name="assertion">The positive assertion.</param>
+ void IRelyingPartyBehavior.OnIncomingPositiveAssertion(IAuthenticationResponse assertion) {
+ if (assertion.GetExtension<ClaimsResponse>() == null) {
+ ClaimsResponse sreg = assertion.UnifyExtensionsAsSreg(true);
+ ((PositiveAnonymousResponse)assertion).Response.Extensions.Add(sreg);
+ }
+ }
+
+ #endregion
+ }
+}
diff --git a/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/Behaviors/GsaIcamRelyingPartyProfile.cs b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/Behaviors/GsaIcamRelyingPartyProfile.cs
new file mode 100644
index 0000000..e8532b2
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/Behaviors/GsaIcamRelyingPartyProfile.cs
@@ -0,0 +1,123 @@
+//-----------------------------------------------------------------------
+// <copyright file="GsaIcamRelyingPartyProfile.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OpenId.Behaviors {
+ using System;
+ using System.Diagnostics.CodeAnalysis;
+ using System.Diagnostics.Contracts;
+ using System.Linq;
+ using DotNetOpenAuth.Configuration;
+ using DotNetOpenAuth.Messaging;
+ using DotNetOpenAuth.OpenId.Extensions.AttributeExchange;
+ using DotNetOpenAuth.OpenId.Extensions.ProviderAuthenticationPolicy;
+ using DotNetOpenAuth.OpenId.Extensions.SimpleRegistration;
+ using DotNetOpenAuth.OpenId.RelyingParty;
+
+ /// <summary>
+ /// Implements the Identity, Credential, &amp; Access Management (ICAM) OpenID 2.0 Profile
+ /// for the General Services Administration (GSA).
+ /// </summary>
+ /// <remarks>
+ /// <para>Relying parties that include this profile are always held to the terms required by the profile,
+ /// but Providers are only affected by the special behaviors of the profile when the RP specifically
+ /// indicates that they want to use this profile. </para>
+ /// </remarks>
+ [Serializable]
+ [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Icam", Justification = "Acronym")]
+ public sealed class GsaIcamRelyingPartyProfile : GsaIcamProfile, IRelyingPartyBehavior {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="GsaIcamRelyingPartyProfile"/> class.
+ /// </summary>
+ public GsaIcamRelyingPartyProfile() {
+ if (DisableSslRequirement) {
+ Logger.OpenId.Warn("GSA level 1 behavior has its RequireSsl requirement disabled.");
+ }
+ }
+
+ #region IRelyingPartyBehavior Members
+
+ /// <summary>
+ /// Applies a well known set of security requirements.
+ /// </summary>
+ /// <param name="securitySettings">The security settings to enhance with the requirements of this profile.</param>
+ /// <remarks>
+ /// Care should be taken to never decrease security when applying a profile.
+ /// Profiles should only enhance security requirements to avoid being
+ /// incompatible with each other.
+ /// </remarks>
+ void IRelyingPartyBehavior.ApplySecuritySettings(RelyingPartySecuritySettings securitySettings) {
+ if (securitySettings.MaximumHashBitLength < 256) {
+ securitySettings.MaximumHashBitLength = 256;
+ }
+
+ securitySettings.RequireSsl = !DisableSslRequirement;
+ securitySettings.RequireDirectedIdentity = true;
+ securitySettings.RequireAssociation = true;
+ securitySettings.RejectDelegatingIdentifiers = true;
+ securitySettings.IgnoreUnsignedExtensions = true;
+ securitySettings.MinimumRequiredOpenIdVersion = ProtocolVersion.V20;
+ }
+
+ /// <summary>
+ /// Called when an authentication request is about to be sent.
+ /// </summary>
+ /// <param name="request">The request.</param>
+ void IRelyingPartyBehavior.OnOutgoingAuthenticationRequest(RelyingParty.IAuthenticationRequest request) {
+ RelyingParty.AuthenticationRequest requestInternal = (RelyingParty.AuthenticationRequest)request;
+ ErrorUtilities.VerifyProtocol(string.Equals(request.Realm.Scheme, Uri.UriSchemeHttps, StringComparison.Ordinal) || DisableSslRequirement, BehaviorStrings.RealmMustBeHttps);
+
+ var pape = requestInternal.AppliedExtensions.OfType<PolicyRequest>().SingleOrDefault();
+ if (pape == null) {
+ request.AddExtension(pape = new PolicyRequest());
+ }
+
+ if (!pape.PreferredPolicies.Contains(AuthenticationPolicies.PrivatePersonalIdentifier)) {
+ pape.PreferredPolicies.Add(AuthenticationPolicies.PrivatePersonalIdentifier);
+ }
+
+ if (!pape.PreferredPolicies.Contains(AuthenticationPolicies.USGovernmentTrustLevel1)) {
+ pape.PreferredPolicies.Add(AuthenticationPolicies.USGovernmentTrustLevel1);
+ }
+
+ if (!AllowPersonallyIdentifiableInformation && !pape.PreferredPolicies.Contains(AuthenticationPolicies.NoPersonallyIdentifiableInformation)) {
+ pape.PreferredPolicies.Add(AuthenticationPolicies.NoPersonallyIdentifiableInformation);
+ }
+
+ if (pape.PreferredPolicies.Contains(AuthenticationPolicies.NoPersonallyIdentifiableInformation)) {
+ ErrorUtilities.VerifyProtocol(
+ (!requestInternal.AppliedExtensions.OfType<ClaimsRequest>().Any() &&
+ !requestInternal.AppliedExtensions.OfType<FetchRequest>().Any()),
+ BehaviorStrings.PiiIncludedWithNoPiiPolicy);
+ }
+
+ Reporting.RecordEventOccurrence(this, "RP");
+ }
+
+ /// <summary>
+ /// Called when an incoming positive assertion is received.
+ /// </summary>
+ /// <param name="assertion">The positive assertion.</param>
+ void IRelyingPartyBehavior.OnIncomingPositiveAssertion(IAuthenticationResponse assertion) {
+ PolicyResponse pape = assertion.GetExtension<PolicyResponse>();
+ ErrorUtilities.VerifyProtocol(
+ pape != null &&
+ pape.ActualPolicies.Contains(AuthenticationPolicies.USGovernmentTrustLevel1) &&
+ pape.ActualPolicies.Contains(AuthenticationPolicies.PrivatePersonalIdentifier),
+ BehaviorStrings.PapeResponseOrRequiredPoliciesMissing);
+
+ ErrorUtilities.VerifyProtocol(AllowPersonallyIdentifiableInformation || pape.ActualPolicies.Contains(AuthenticationPolicies.NoPersonallyIdentifiableInformation), BehaviorStrings.PapeResponseOrRequiredPoliciesMissing);
+
+ if (pape.ActualPolicies.Contains(AuthenticationPolicies.NoPersonallyIdentifiableInformation)) {
+ ErrorUtilities.VerifyProtocol(
+ assertion.GetExtension<ClaimsResponse>() == null &&
+ assertion.GetExtension<FetchResponse>() == null,
+ BehaviorStrings.PiiIncludedWithNoPiiPolicy);
+ }
+ }
+
+ #endregion
+ }
+}
diff --git a/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/ChannelElements/ExtensionsBindingElementRelyingParty.cs b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/ChannelElements/ExtensionsBindingElementRelyingParty.cs
new file mode 100644
index 0000000..f6f6707
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/ChannelElements/ExtensionsBindingElementRelyingParty.cs
@@ -0,0 +1,28 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Diagnostics.Contracts;
+using DotNetOpenAuth.OpenId.RelyingParty;
+
+namespace DotNetOpenAuth.OpenId.ChannelElements {
+ internal class ExtensionsBindingElementRelyingParty : ExtensionsBindingElement {
+ /// <summary>
+ /// The security settings that apply to this relying party, if it is a relying party.
+ /// </summary>
+ private readonly RelyingPartySecuritySettings relyingPartySecuritySettings;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ExtensionsBindingElement"/> class.
+ /// </summary>
+ /// <param name="extensionFactory">The extension factory.</param>
+ /// <param name="securitySettings">The security settings.</param>
+ internal ExtensionsBindingElementRelyingParty(IOpenIdExtensionFactory extensionFactory, RelyingPartySecuritySettings securitySettings)
+ : base(extensionFactory, securitySettings, !securitySettings.IgnoreUnsignedExtensions) {
+ Contract.Requires<ArgumentNullException>(extensionFactory != null);
+ Contract.Requires<ArgumentNullException>(securitySettings != null);
+
+ this.relyingPartySecuritySettings = securitySettings;
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/ChannelElements/OpenIdRelyingPartyChannel.cs b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/ChannelElements/OpenIdRelyingPartyChannel.cs
new file mode 100644
index 0000000..585dbcd
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/ChannelElements/OpenIdRelyingPartyChannel.cs
@@ -0,0 +1,119 @@
+//-----------------------------------------------------------------------
+// <copyright file="OpenIdRelyingPartyChannel.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OpenId.ChannelElements {
+ using System;
+ using System.Collections.Generic;
+ using System.Diagnostics.Contracts;
+ using System.Linq;
+ using System.Text;
+ using DotNetOpenAuth.Messaging;
+ using DotNetOpenAuth.Messaging.Bindings;
+ using DotNetOpenAuth.OpenId.Extensions;
+ using DotNetOpenAuth.OpenId.RelyingParty;
+
+ internal class OpenIdRelyingPartyChannel : OpenIdChannel {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="OpenIdChannel"/> class
+ /// for use by a Relying Party.
+ /// </summary>
+ /// <param name="cryptoKeyStore">The association store to use.</param>
+ /// <param name="nonceStore">The nonce store to use.</param>
+ /// <param name="securitySettings">The security settings to apply.</param>
+ internal OpenIdRelyingPartyChannel(ICryptoKeyStore cryptoKeyStore, INonceStore nonceStore, RelyingPartySecuritySettings securitySettings)
+ : this(cryptoKeyStore, nonceStore, new OpenIdMessageFactory(), securitySettings, false) {
+ Contract.Requires<ArgumentNullException>(securitySettings != null);
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="OpenIdChannel"/> class
+ /// for use by a Relying Party.
+ /// </summary>
+ /// <param name="cryptoKeyStore">The association store to use.</param>
+ /// <param name="nonceStore">The nonce store to use.</param>
+ /// <param name="messageTypeProvider">An object that knows how to distinguish the various OpenID message types for deserialization purposes.</param>
+ /// <param name="securitySettings">The security settings to apply.</param>
+ /// <param name="nonVerifying">A value indicating whether the channel is set up with no functional security binding elements.</param>
+ private OpenIdRelyingPartyChannel(ICryptoKeyStore cryptoKeyStore, INonceStore nonceStore, IMessageFactory messageTypeProvider, RelyingPartySecuritySettings securitySettings, bool nonVerifying) :
+ this(messageTypeProvider, InitializeBindingElements(cryptoKeyStore, nonceStore, securitySettings, nonVerifying)) {
+ Contract.Requires<ArgumentNullException>(messageTypeProvider != null);
+ Contract.Requires<ArgumentNullException>(securitySettings != null);
+ Contract.Requires<ArgumentException>(!nonVerifying || securitySettings is RelyingPartySecuritySettings);
+ }
+
+ /// <summary>
+ /// A value indicating whether the channel is set up
+ /// with no functional security binding elements.
+ /// </summary>
+ /// <returns>A new <see cref="OpenIdChannel"/> instance that will not perform verification on incoming messages or apply any security to outgoing messages.</returns>
+ /// <remarks>
+ /// <para>A value of <c>true</c> allows the relying party to preview incoming
+ /// messages without invalidating nonces or checking signatures.</para>
+ /// <para>Setting this to <c>true</c> poses a great security risk and is only
+ /// present to support the <see cref="OpenIdAjaxTextBox"/> which needs to preview
+ /// messages, and will validate them later.</para>
+ /// </remarks>
+ internal static OpenIdChannel CreateNonVerifyingChannel() {
+ Contract.Ensures(Contract.Result<OpenIdChannel>() != null);
+
+ return new OpenIdRelyingPartyChannel(null, null, new OpenIdMessageFactory(), new RelyingPartySecuritySettings(), true);
+ }
+
+ /// <summary>
+ /// Initializes the binding elements.
+ /// </summary>
+ /// <param name="cryptoKeyStore">The crypto key store.</param>
+ /// <param name="nonceStore">The nonce store to use.</param>
+ /// <param name="securitySettings">The security settings to apply. Must be an instance of either <see cref="RelyingPartySecuritySettings"/> or <see cref="ProviderSecuritySettings"/>.</param>
+ /// <param name="nonVerifying">A value indicating whether the channel is set up with no functional security binding elements.</param>
+ /// <returns>
+ /// An array of binding elements which may be used to construct the channel.
+ /// </returns>
+ private static IChannelBindingElement[] InitializeBindingElements(ICryptoKeyStore cryptoKeyStore, INonceStore nonceStore, RelyingPartySecuritySettings securitySettings, bool nonVerifying) {
+ Contract.Requires<ArgumentNullException>(securitySettings != null);
+
+ SigningBindingElement signingElement;
+ signingElement = nonVerifying ? null : new SigningBindingElement(new CryptoKeyStoreAsRelyingPartyAssociationStore(cryptoKeyStore ?? new MemoryCryptoKeyStore()));
+
+ var extensionFactory = OpenIdExtensionFactoryAggregator.LoadFromConfiguration();
+
+ List<IChannelBindingElement> elements = new List<IChannelBindingElement>(8);
+ elements.Add(new ExtensionsBindingElement(extensionFactory, securitySettings));
+ elements.Add(new RelyingPartySecurityOptions(securitySettings));
+ elements.Add(new BackwardCompatibilityBindingElement());
+ ReturnToNonceBindingElement requestNonceElement = null;
+
+ if (cryptoKeyStore != null) {
+ if (nonceStore != null) {
+ // There is no point in having a ReturnToNonceBindingElement without
+ // a ReturnToSignatureBindingElement because the nonce could be
+ // artificially changed without it.
+ requestNonceElement = new ReturnToNonceBindingElement(nonceStore, securitySettings);
+ elements.Add(requestNonceElement);
+ }
+
+ // It is important that the return_to signing element comes last
+ // so that the nonce is included in the signature.
+ elements.Add(new ReturnToSignatureBindingElement(cryptoKeyStore));
+ }
+
+ ErrorUtilities.VerifyOperation(!securitySettings.RejectUnsolicitedAssertions || requestNonceElement != null, OpenIdStrings.UnsolicitedAssertionRejectionRequiresNonceStore);
+
+ if (nonVerifying) {
+ elements.Add(new SkipSecurityBindingElement());
+ } else {
+ if (nonceStore != null) {
+ elements.Add(new StandardReplayProtectionBindingElement(nonceStore, true));
+ }
+
+ elements.Add(new StandardExpirationBindingElement());
+ elements.Add(signingElement);
+ }
+
+ return elements.ToArray();
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/ChannelElements/RelyingPartySecurityOptions.cs b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/ChannelElements/RelyingPartySecurityOptions.cs
new file mode 100644
index 0000000..d8fc103
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/ChannelElements/RelyingPartySecurityOptions.cs
@@ -0,0 +1,98 @@
+//-----------------------------------------------------------------------
+// <copyright file="RelyingPartySecurityOptions.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OpenId.ChannelElements {
+ using DotNetOpenAuth.Messaging;
+ using DotNetOpenAuth.OpenId.Messages;
+ using DotNetOpenAuth.OpenId.RelyingParty;
+
+ /// <summary>
+ /// Helps ensure compliance to some properties in the <see cref="RelyingPartySecuritySettings"/>.
+ /// </summary>
+ internal class RelyingPartySecurityOptions : IChannelBindingElement {
+ /// <summary>
+ /// The security settings that are active on the relying party.
+ /// </summary>
+ private RelyingPartySecuritySettings securitySettings;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="RelyingPartySecurityOptions"/> class.
+ /// </summary>
+ /// <param name="securitySettings">The security settings.</param>
+ internal RelyingPartySecurityOptions(RelyingPartySecuritySettings securitySettings) {
+ this.securitySettings = securitySettings;
+ }
+
+ #region IChannelBindingElement Members
+
+ /// <summary>
+ /// Gets or sets the channel that this binding element belongs to.
+ /// </summary>
+ /// <remarks>
+ /// This property is set by the channel when it is first constructed.
+ /// </remarks>
+ public Channel Channel { get; set; }
+
+ /// <summary>
+ /// Gets the protection commonly offered (if any) by this binding element.
+ /// </summary>
+ /// <remarks>
+ /// This value is used to assist in sorting binding elements in the channel stack.
+ /// </remarks>
+ public MessageProtections Protection {
+ get { return MessageProtections.None; }
+ }
+
+ /// <summary>
+ /// Prepares a message for sending based on the rules of this channel binding element.
+ /// </summary>
+ /// <param name="message">The message to prepare for sending.</param>
+ /// <returns>
+ /// The protections (if any) that this binding element applied to the message.
+ /// Null if this binding element did not even apply to this binding element.
+ /// </returns>
+ /// <remarks>
+ /// Implementations that provide message protection must honor the
+ /// <see cref="MessagePartAttribute.RequiredProtection"/> properties where applicable.
+ /// </remarks>
+ public MessageProtections? ProcessOutgoingMessage(IProtocolMessage message) {
+ return null;
+ }
+
+ /// <summary>
+ /// Performs any transformation on an incoming message that may be necessary and/or
+ /// validates an incoming message based on the rules of this channel binding element.
+ /// </summary>
+ /// <param name="message">The incoming message to process.</param>
+ /// <returns>
+ /// The protections (if any) that this binding element applied to the message.
+ /// Null if this binding element did not even apply to this binding element.
+ /// </returns>
+ /// <exception cref="ProtocolException">
+ /// Thrown when the binding element rules indicate that this message is invalid and should
+ /// NOT be processed.
+ /// </exception>
+ /// <remarks>
+ /// Implementations that provide message protection must honor the
+ /// <see cref="MessagePartAttribute.RequiredProtection"/> properties where applicable.
+ /// </remarks>
+ public MessageProtections? ProcessIncomingMessage(IProtocolMessage message) {
+ var positiveAssertion = message as PositiveAssertionResponse;
+ if (positiveAssertion != null) {
+ ErrorUtilities.VerifyProtocol(
+ !this.securitySettings.RejectDelegatingIdentifiers ||
+ positiveAssertion.LocalIdentifier == positiveAssertion.ClaimedIdentifier,
+ OpenIdStrings.DelegatingIdentifiersNotAllowed);
+
+ return MessageProtections.None;
+ }
+
+ return null;
+ }
+
+ #endregion
+ }
+}
diff --git a/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/ChannelElements/RelyingPartySigningBindingElement.cs b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/ChannelElements/RelyingPartySigningBindingElement.cs
new file mode 100644
index 0000000..1d86152
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/ChannelElements/RelyingPartySigningBindingElement.cs
@@ -0,0 +1,86 @@
+//-----------------------------------------------------------------------
+// <copyright file="RelyingPartySigningBindingElement.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OpenId.ChannelElements {
+ using System;
+ using System.Collections.Generic;
+ using System.Linq;
+ using System.Text;
+ using DotNetOpenAuth.OpenId.RelyingParty;
+ using DotNetOpenAuth.OpenId.Messages;
+ using System.Diagnostics.Contracts;
+ using DotNetOpenAuth.Messaging;
+ using DotNetOpenAuth.Messaging.Bindings;
+
+ internal class RelyingPartySigningBindingElement : SigningBindingElement {
+ /// <summary>
+ /// The association store used by Relying Parties to look up the secrets needed for signing.
+ /// </summary>
+ private readonly IRelyingPartyAssociationStore rpAssociations;
+
+ /// <summary>
+ /// Initializes a new instance of the SigningBindingElement class for use by a Relying Party.
+ /// </summary>
+ /// <param name="associationStore">The association store used to look up the secrets needed for signing. May be null for dumb Relying Parties.</param>
+ internal RelyingPartySigningBindingElement(IRelyingPartyAssociationStore associationStore) {
+ this.rpAssociations = associationStore;
+ }
+
+ protected override Association GetSpecificAssociation(ITamperResistantOpenIdMessage signedMessage) {
+ Association association = null;
+
+ if (!string.IsNullOrEmpty(signedMessage.AssociationHandle)) {
+ IndirectSignedResponse indirectSignedMessage = signedMessage as IndirectSignedResponse;
+ if (this.rpAssociations != null) { // if on a smart RP
+ Uri providerEndpoint = indirectSignedMessage.ProviderEndpoint;
+ association = this.rpAssociations.GetAssociation(providerEndpoint, signedMessage.AssociationHandle);
+ }
+ }
+
+ return association;
+ }
+
+ protected override Association GetAssociation(ITamperResistantOpenIdMessage signedMessage) {
+ Contract.Requires<ArgumentNullException>(signedMessage != null);
+
+ // We're on a Relying Party verifying a signature.
+ IDirectedProtocolMessage directedMessage = (IDirectedProtocolMessage)signedMessage;
+ if (this.rpAssociations != null) {
+ return this.rpAssociations.GetAssociation(directedMessage.Recipient, signedMessage.AssociationHandle);
+ } else {
+ return null;
+ }
+ }
+
+ protected override MessageProtections VerifySignatureByUnrecognizedHandle(IProtocolMessage message, ITamperResistantOpenIdMessage signedMessage, MessageProtections protectionsApplied) {
+ // We did not recognize the association the provider used to sign the message.
+ // Ask the provider to check the signature then.
+ var indirectSignedResponse = (IndirectSignedResponse)signedMessage;
+ var checkSignatureRequest = new CheckAuthenticationRequest(indirectSignedResponse, this.Channel);
+ var checkSignatureResponse = this.Channel.Request<CheckAuthenticationResponse>(checkSignatureRequest);
+ if (!checkSignatureResponse.IsValid) {
+ Logger.Bindings.Error("Provider reports signature verification failed.");
+ throw new InvalidSignatureException(message);
+ }
+
+ // If the OP confirms that a handle should be invalidated as well, do that.
+ if (!string.IsNullOrEmpty(checkSignatureResponse.InvalidateHandle)) {
+ if (this.rpAssociations != null) {
+ this.rpAssociations.RemoveAssociation(indirectSignedResponse.ProviderEndpoint, checkSignatureResponse.InvalidateHandle);
+ }
+ }
+
+ // When we're in dumb mode we can't provide our own replay protection,
+ // but for OpenID 2.0 Providers we can rely on them providing it as part
+ // of signature verification.
+ if (message.Version.Major >= 2) {
+ protectionsApplied |= MessageProtections.ReplayProtection;
+ }
+
+ return protectionsApplied;
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/ChannelElements/ReturnToNonceBindingElement.cs b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/ChannelElements/ReturnToNonceBindingElement.cs
new file mode 100644
index 0000000..3649543
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/ChannelElements/ReturnToNonceBindingElement.cs
@@ -0,0 +1,291 @@
+//-----------------------------------------------------------------------
+// <copyright file="ReturnToNonceBindingElement.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OpenId.ChannelElements {
+ using System;
+ using System.Collections.Generic;
+ using System.Diagnostics.Contracts;
+ using System.Linq;
+ using System.Text;
+ using DotNetOpenAuth.Messaging;
+ using DotNetOpenAuth.Messaging.Bindings;
+ using DotNetOpenAuth.OpenId.Messages;
+ using DotNetOpenAuth.OpenId.RelyingParty;
+
+ /// <summary>
+ /// This binding element adds a nonce to a Relying Party's outgoing
+ /// authentication request when working against an OpenID 1.0 Provider
+ /// in order to protect against replay attacks or on all authentication
+ /// requests to distinguish solicited from unsolicited assertions.
+ /// </summary>
+ /// <remarks>
+ /// <para>This nonce goes beyond the OpenID 1.x spec, but adds to security.
+ /// Since this library's Provider implementation also provides special nonce
+ /// protection for 1.0 messages, this security feature overlaps with that one.
+ /// This means that if an RP from this library were talking to an OP from this
+ /// library, but the Identifier being authenticated advertised the OP as a 1.x
+ /// OP, then both RP and OP might try to use a nonce for protecting the assertion.
+ /// There's no problem with that--it will still all work out. And it would be a
+ /// very rare combination of elements anyway.
+ /// </para>
+ /// <para>
+ /// This binding element deactivates itself for OpenID 2.0 (or later) messages
+ /// since they are automatically protected in the protocol by the Provider's
+ /// openid.response_nonce parameter. The exception to this is when
+ /// <see cref="RelyingPartySecuritySettings.RejectUnsolicitedAssertions"/> is
+ /// set to <c>true</c>, which will not only add a request nonce to every outgoing
+ /// authentication request but also require that it be present in positive
+ /// assertions, effectively disabling unsolicited assertions.
+ /// </para>
+ /// <para>In the messaging stack, this binding element looks like an ordinary
+ /// transform-type of binding element rather than a protection element,
+ /// due to its required order in the channel stack and that it exists
+ /// only on the RP side and only on some messages.</para>
+ /// </remarks>
+ internal class ReturnToNonceBindingElement : IChannelBindingElement {
+ /// <summary>
+ /// The parameter of the callback parameter we tack onto the return_to URL
+ /// to store the replay-detection nonce.
+ /// </summary>
+ internal const string NonceParameter = OpenIdUtilities.CustomParameterPrefix + "request_nonce";
+
+ /// <summary>
+ /// The context within which return_to nonces must be unique -- they all go into the same bucket.
+ /// </summary>
+ private const string ReturnToNonceContext = "https://localhost/dnoa/return_to_nonce";
+
+ /// <summary>
+ /// The length of the generated nonce's random part.
+ /// </summary>
+ private const int NonceByteLength = 128 / 8; // 128-bit nonce
+
+ /// <summary>
+ /// The nonce store that will allow us to recall which nonces we've seen before.
+ /// </summary>
+ private INonceStore nonceStore;
+
+ /// <summary>
+ /// The security settings at the RP.
+ /// </summary>
+ private RelyingPartySecuritySettings securitySettings;
+
+ /// <summary>
+ /// Backing field for the <see cref="Channel"/> property.
+ /// </summary>
+ private Channel channel;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ReturnToNonceBindingElement"/> class.
+ /// </summary>
+ /// <param name="nonceStore">The nonce store to use.</param>
+ /// <param name="securitySettings">The security settings of the RP.</param>
+ internal ReturnToNonceBindingElement(INonceStore nonceStore, RelyingPartySecuritySettings securitySettings) {
+ Contract.Requires<ArgumentNullException>(nonceStore != null);
+ Contract.Requires<ArgumentNullException>(securitySettings != null);
+
+ this.nonceStore = nonceStore;
+ this.securitySettings = securitySettings;
+ }
+
+ #region IChannelBindingElement Properties
+
+ /// <summary>
+ /// Gets or sets the channel that this binding element belongs to.
+ /// </summary>
+ /// <remarks>
+ /// This property is set by the channel when it is first constructed.
+ /// </remarks>
+ public Channel Channel {
+ get {
+ return this.channel;
+ }
+
+ set {
+ if (this.channel == value) {
+ return;
+ }
+
+ this.channel = value;
+ }
+ }
+
+ /// <summary>
+ /// Gets the protection offered (if any) by this binding element.
+ /// </summary>
+ public MessageProtections Protection {
+ get { return MessageProtections.ReplayProtection; }
+ }
+
+ #endregion
+
+ /// <summary>
+ /// Gets the maximum message age from the standard expiration binding element.
+ /// </summary>
+ private static TimeSpan MaximumMessageAge {
+ get { return StandardExpirationBindingElement.MaximumMessageAge; }
+ }
+
+ #region IChannelBindingElement Methods
+
+ /// <summary>
+ /// Prepares a message for sending based on the rules of this channel binding element.
+ /// </summary>
+ /// <param name="message">The message to prepare for sending.</param>
+ /// <returns>
+ /// The protections (if any) that this binding element applied to the message.
+ /// Null if this binding element did not even apply to this binding element.
+ /// </returns>
+ /// <remarks>
+ /// Implementations that provide message protection must honor the
+ /// <see cref="MessagePartAttribute.RequiredProtection"/> properties where applicable.
+ /// </remarks>
+ public MessageProtections? ProcessOutgoingMessage(IProtocolMessage message) {
+ // We only add a nonce to some auth requests.
+ SignedResponseRequest request = message as SignedResponseRequest;
+ if (this.UseRequestNonce(request)) {
+ request.AddReturnToArguments(NonceParameter, CustomNonce.NewNonce().Serialize());
+ request.SignReturnTo = true; // a nonce without a signature is completely pointless
+
+ return MessageProtections.ReplayProtection;
+ }
+
+ return null;
+ }
+
+ /// <summary>
+ /// Performs any transformation on an incoming message that may be necessary and/or
+ /// validates an incoming message based on the rules of this channel binding element.
+ /// </summary>
+ /// <param name="message">The incoming message to process.</param>
+ /// <returns>
+ /// The protections (if any) that this binding element applied to the message.
+ /// Null if this binding element did not even apply to this binding element.
+ /// </returns>
+ /// <exception cref="ProtocolException">
+ /// Thrown when the binding element rules indicate that this message is invalid and should
+ /// NOT be processed.
+ /// </exception>
+ /// <remarks>
+ /// Implementations that provide message protection must honor the
+ /// <see cref="MessagePartAttribute.RequiredProtection"/> properties where applicable.
+ /// </remarks>
+ public MessageProtections? ProcessIncomingMessage(IProtocolMessage message) {
+ IndirectSignedResponse response = message as IndirectSignedResponse;
+ if (this.UseRequestNonce(response)) {
+ if (!response.ReturnToParametersSignatureValidated) {
+ Logger.OpenId.Error("Incoming message is expected to have a nonce, but the return_to parameter is not signed.");
+ }
+
+ string nonceValue = response.GetReturnToArgument(NonceParameter);
+ ErrorUtilities.VerifyProtocol(
+ nonceValue != null && response.ReturnToParametersSignatureValidated,
+ this.securitySettings.RejectUnsolicitedAssertions ? OpenIdStrings.UnsolicitedAssertionsNotAllowed : OpenIdStrings.UnsolicitedAssertionsNotAllowedFrom1xOPs);
+
+ CustomNonce nonce = CustomNonce.Deserialize(nonceValue);
+ DateTime expirationDate = nonce.CreationDateUtc + MaximumMessageAge;
+ if (expirationDate < DateTime.UtcNow) {
+ throw new ExpiredMessageException(expirationDate, message);
+ }
+
+ IReplayProtectedProtocolMessage replayResponse = response;
+ if (!this.nonceStore.StoreNonce(ReturnToNonceContext, nonce.RandomPartAsString, nonce.CreationDateUtc)) {
+ Logger.OpenId.ErrorFormat("Replayed nonce detected ({0} {1}). Rejecting message.", replayResponse.Nonce, replayResponse.UtcCreationDate);
+ throw new ReplayedMessageException(message);
+ }
+
+ return MessageProtections.ReplayProtection;
+ }
+
+ return null;
+ }
+
+ #endregion
+
+ /// <summary>
+ /// Determines whether a request nonce should be applied the request
+ /// or should be expected in the response.
+ /// </summary>
+ /// <param name="message">The authentication request or the positive assertion response.</param>
+ /// <returns>
+ /// <c>true</c> if the message exchanged with an OpenID 1.x provider
+ /// or if unsolicited assertions should be rejected at the RP; otherwise <c>false</c>.
+ /// </returns>
+ private bool UseRequestNonce(IMessage message) {
+ return message != null && (this.securitySettings.RejectUnsolicitedAssertions ||
+ (message.Version.Major < 2 && this.securitySettings.ProtectDownlevelReplayAttacks));
+ }
+
+ /// <summary>
+ /// A special DotNetOpenAuth-only nonce used by the RP when talking to 1.0 OPs in order
+ /// to protect against replay attacks.
+ /// </summary>
+ private class CustomNonce {
+ /// <summary>
+ /// The random bits generated for the nonce.
+ /// </summary>
+ private byte[] randomPart;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="CustomNonce"/> class.
+ /// </summary>
+ /// <param name="creationDate">The creation date of the nonce.</param>
+ /// <param name="randomPart">The random bits that help make the nonce unique.</param>
+ private CustomNonce(DateTime creationDate, byte[] randomPart) {
+ this.CreationDateUtc = creationDate;
+ this.randomPart = randomPart;
+ }
+
+ /// <summary>
+ /// Gets the creation date.
+ /// </summary>
+ internal DateTime CreationDateUtc { get; private set; }
+
+ /// <summary>
+ /// Gets the random part of the nonce as a base64 encoded string.
+ /// </summary>
+ internal string RandomPartAsString {
+ get { return Convert.ToBase64String(this.randomPart); }
+ }
+
+ /// <summary>
+ /// Creates a new nonce.
+ /// </summary>
+ /// <returns>The newly instantiated instance.</returns>
+ internal static CustomNonce NewNonce() {
+ return new CustomNonce(DateTime.UtcNow, MessagingUtilities.GetCryptoRandomData(NonceByteLength));
+ }
+
+ /// <summary>
+ /// Deserializes a nonce from the return_to parameter.
+ /// </summary>
+ /// <param name="value">The base64-encoded value of the nonce.</param>
+ /// <returns>The instantiated and initialized nonce.</returns>
+ internal static CustomNonce Deserialize(string value) {
+ Contract.Requires<ArgumentException>(!String.IsNullOrEmpty(value));
+
+ byte[] nonce = MessagingUtilities.FromBase64WebSafeString(value);
+ Contract.Assume(nonce != null);
+ DateTime creationDateUtc = new DateTime(BitConverter.ToInt64(nonce, 0), DateTimeKind.Utc);
+ byte[] randomPart = new byte[NonceByteLength];
+ Array.Copy(nonce, sizeof(long), randomPart, 0, NonceByteLength);
+ return new CustomNonce(creationDateUtc, randomPart);
+ }
+
+ /// <summary>
+ /// Serializes the entire nonce for adding to the return_to URL.
+ /// </summary>
+ /// <returns>The base64-encoded string representing the nonce.</returns>
+ internal string Serialize() {
+ byte[] timestamp = BitConverter.GetBytes(this.CreationDateUtc.Ticks);
+ byte[] nonce = new byte[timestamp.Length + this.randomPart.Length];
+ timestamp.CopyTo(nonce, 0);
+ this.randomPart.CopyTo(nonce, timestamp.Length);
+ string base64Nonce = MessagingUtilities.ConvertToBase64WebSafeString(nonce);
+ return base64Nonce;
+ }
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/Extensions/ExtensionsInteropRelyingPartyHelper.cs b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/Extensions/ExtensionsInteropRelyingPartyHelper.cs
new file mode 100644
index 0000000..a864da8
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/Extensions/ExtensionsInteropRelyingPartyHelper.cs
@@ -0,0 +1,151 @@
+//-----------------------------------------------------------------------
+// <copyright file="ExtensionsInteropRelyingPartyHelper.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OpenId.Extensions {
+ using System;
+ using System.Collections.Generic;
+ using System.Diagnostics.CodeAnalysis;
+ using System.Diagnostics.Contracts;
+ using System.Linq;
+ using DotNetOpenAuth.Messaging;
+ using DotNetOpenAuth.OpenId.Extensions.AttributeExchange;
+ using DotNetOpenAuth.OpenId.Extensions.SimpleRegistration;
+ using DotNetOpenAuth.OpenId.Messages;
+
+ /// <summary>
+ /// A set of methods designed to assist in improving interop across different
+ /// OpenID implementations and their extensions.
+ /// </summary>
+ public static class ExtensionsInteropRelyingPartyHelper : ExtensionsInteropHelper {
+ /// <summary>
+ /// Adds an Attribute Exchange (AX) extension to the authentication request
+ /// that asks for the same attributes as the Simple Registration (sreg) extension
+ /// that is already applied.
+ /// </summary>
+ /// <param name="request">The authentication request.</param>
+ /// <param name="attributeFormats">The attribute formats to use in the AX request.</param>
+ /// <remarks>
+ /// <para>If discovery on the user-supplied identifier yields hints regarding which
+ /// extensions and attribute formats the Provider supports, this method MAY ignore the
+ /// <paramref name="attributeFormats"/> argument and accomodate the Provider to minimize
+ /// the size of the request.</para>
+ /// <para>If the request does not carry an sreg extension, the method logs a warning but
+ /// otherwise quietly returns doing nothing.</para>
+ /// </remarks>
+ [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Sreg", Justification = "Abbreviation")]
+ public static void SpreadSregToAX(this RelyingParty.IAuthenticationRequest request, AXAttributeFormats attributeFormats) {
+ Contract.Requires<ArgumentNullException>(request != null);
+
+ var req = (RelyingParty.AuthenticationRequest)request;
+ var sreg = req.AppliedExtensions.OfType<ClaimsRequest>().SingleOrDefault();
+ if (sreg == null) {
+ Logger.OpenId.Debug("No Simple Registration (ClaimsRequest) extension present in the request to spread to AX.");
+ return;
+ }
+
+ if (req.DiscoveryResult.IsExtensionSupported<ClaimsRequest>()) {
+ Logger.OpenId.Debug("Skipping generation of AX request because the Identifier advertises the Provider supports the Sreg extension.");
+ return;
+ }
+
+ var ax = req.AppliedExtensions.OfType<FetchRequest>().SingleOrDefault();
+ if (ax == null) {
+ ax = new FetchRequest();
+ req.AddExtension(ax);
+ }
+
+ // Try to use just one AX Type URI format if we can figure out which type the OP accepts.
+ AXAttributeFormats detectedFormat;
+ if (TryDetectOPAttributeFormat(request, out detectedFormat)) {
+ Logger.OpenId.Debug("Detected OP support for AX but not for Sreg. Removing Sreg extension request and using AX instead.");
+ attributeFormats = detectedFormat;
+ req.Extensions.Remove(sreg);
+ } else {
+ Logger.OpenId.Debug("Could not determine whether OP supported Sreg or AX. Using both extensions.");
+ }
+
+ foreach (AXAttributeFormats format in ExtensionsInteropHelper.ForEachFormat(attributeFormats)) {
+ ExtensionsInteropHelper.FetchAttribute(ax, format, WellKnownAttributes.BirthDate.WholeBirthDate, sreg.BirthDate);
+ ExtensionsInteropHelper.FetchAttribute(ax, format, WellKnownAttributes.Contact.HomeAddress.Country, sreg.Country);
+ ExtensionsInteropHelper.FetchAttribute(ax, format, WellKnownAttributes.Contact.Email, sreg.Email);
+ ExtensionsInteropHelper.FetchAttribute(ax, format, WellKnownAttributes.Name.FullName, sreg.FullName);
+ ExtensionsInteropHelper.FetchAttribute(ax, format, WellKnownAttributes.Person.Gender, sreg.Gender);
+ ExtensionsInteropHelper.FetchAttribute(ax, format, WellKnownAttributes.Preferences.Language, sreg.Language);
+ ExtensionsInteropHelper.FetchAttribute(ax, format, WellKnownAttributes.Name.Alias, sreg.Nickname);
+ ExtensionsInteropHelper.FetchAttribute(ax, format, WellKnownAttributes.Contact.HomeAddress.PostalCode, sreg.PostalCode);
+ ExtensionsInteropHelper.FetchAttribute(ax, format, WellKnownAttributes.Preferences.TimeZone, sreg.TimeZone);
+ }
+ }
+
+ /// <summary>
+ /// Looks for Simple Registration and Attribute Exchange (all known formats)
+ /// response extensions and returns them as a Simple Registration extension.
+ /// </summary>
+ /// <param name="response">The authentication response.</param>
+ /// <param name="allowUnsigned">if set to <c>true</c> unsigned extensions will be included in the search.</param>
+ /// <returns>
+ /// The Simple Registration response if found,
+ /// or a fabricated one based on the Attribute Exchange extension if found,
+ /// or just an empty <see cref="ClaimsResponse"/> if there was no data.
+ /// Never <c>null</c>.</returns>
+ [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Sreg", Justification = "Abbreviation")]
+ public static ClaimsResponse UnifyExtensionsAsSreg(this RelyingParty.IAuthenticationResponse response, bool allowUnsigned) {
+ Contract.Requires<ArgumentNullException>(response != null);
+
+ var resp = (RelyingParty.IAuthenticationResponse)response;
+ var sreg = allowUnsigned ? resp.GetUntrustedExtension<ClaimsResponse>() : resp.GetExtension<ClaimsResponse>();
+ if (sreg != null) {
+ return sreg;
+ }
+
+ AXAttributeFormats formats = AXAttributeFormats.All;
+ sreg = new ClaimsResponse();
+ var fetchResponse = allowUnsigned ? resp.GetUntrustedExtension<FetchResponse>() : resp.GetExtension<FetchResponse>();
+ if (fetchResponse != null) {
+ ((IOpenIdMessageExtension)sreg).IsSignedByRemoteParty = fetchResponse.IsSignedByProvider;
+ sreg.BirthDateRaw = fetchResponse.GetAttributeValue(WellKnownAttributes.BirthDate.WholeBirthDate, formats);
+ sreg.Country = fetchResponse.GetAttributeValue(WellKnownAttributes.Contact.HomeAddress.Country, formats);
+ sreg.PostalCode = fetchResponse.GetAttributeValue(WellKnownAttributes.Contact.HomeAddress.PostalCode, formats);
+ sreg.Email = fetchResponse.GetAttributeValue(WellKnownAttributes.Contact.Email, formats);
+ sreg.FullName = fetchResponse.GetAttributeValue(WellKnownAttributes.Name.FullName, formats);
+ sreg.Language = fetchResponse.GetAttributeValue(WellKnownAttributes.Preferences.Language, formats);
+ sreg.Nickname = fetchResponse.GetAttributeValue(WellKnownAttributes.Name.Alias, formats);
+ sreg.TimeZone = fetchResponse.GetAttributeValue(WellKnownAttributes.Preferences.TimeZone, formats);
+ string gender = fetchResponse.GetAttributeValue(WellKnownAttributes.Person.Gender, formats);
+ if (gender != null) {
+ sreg.Gender = (Gender)ExtensionsInteropHelper.genderEncoder.Decode(gender);
+ }
+ }
+
+ return sreg;
+ }
+
+ /// <summary>
+ /// Gets the attribute value if available.
+ /// </summary>
+ /// <param name="fetchResponse">The AX fetch response extension to look for the attribute value.</param>
+ /// <param name="typeUri">The type URI of the attribute, using the axschema.org format of <see cref="WellKnownAttributes"/>.</param>
+ /// <param name="formats">The AX type URI formats to search.</param>
+ /// <returns>
+ /// The first value of the attribute, if available.
+ /// </returns>
+ internal static string GetAttributeValue(this FetchResponse fetchResponse, string typeUri, AXAttributeFormats formats) {
+ return ExtensionsInteropHelper.ForEachFormat(formats).Select(format => fetchResponse.GetAttributeValue(ExtensionsInteropHelper.TransformAXFormat(typeUri, format))).FirstOrDefault(s => s != null);
+ }
+
+ /// <summary>
+ /// Tries to find the exact format of AX attribute Type URI supported by the Provider.
+ /// </summary>
+ /// <param name="request">The authentication request.</param>
+ /// <param name="attributeFormat">The attribute formats the RP will try if this discovery fails.</param>
+ /// <returns>The AX format(s) to use based on the Provider's advertised AX support.</returns>
+ private static bool TryDetectOPAttributeFormat(RelyingParty.IAuthenticationRequest request, out AXAttributeFormats attributeFormat) {
+ Contract.Requires<ArgumentNullException>(request != null);
+ attributeFormat = ExtensionsInteropHelper.DetectAXFormat(request.DiscoveryResult.Capabilities);
+ return attributeFormat != AXAttributeFormats.None;
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/Extensions/UI/UIUtilities.cs b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/Extensions/UI/UIUtilities.cs
new file mode 100644
index 0000000..cee6882
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/Extensions/UI/UIUtilities.cs
@@ -0,0 +1,52 @@
+//-----------------------------------------------------------------------
+// <copyright file="UIUtilities.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OpenId.Extensions.UI {
+ using System;
+ using System.Diagnostics.Contracts;
+ using System.Globalization;
+ using DotNetOpenAuth.Messaging;
+ using DotNetOpenAuth.OpenId.RelyingParty;
+
+ /// <summary>
+ /// Constants used in implementing support for the UI extension.
+ /// </summary>
+ public static class UIUtilities {
+ /// <summary>
+ /// The required width of the popup window the relying party creates for the provider.
+ /// </summary>
+ public const int PopupWidth = 500; // UI extension calls for 450px, but Yahoo needs 500px
+
+ /// <summary>
+ /// The required height of the popup window the relying party creates for the provider.
+ /// </summary>
+ public const int PopupHeight = 500;
+
+ /// <summary>
+ /// Gets the <c>window.open</c> javascript snippet to use to open a popup window
+ /// compliant with the UI extension.
+ /// </summary>
+ /// <param name="relyingParty">The relying party.</param>
+ /// <param name="request">The authentication request to place in the window.</param>
+ /// <param name="windowName">The name to assign to the popup window.</param>
+ /// <returns>A string starting with 'window.open' and forming just that one method call.</returns>
+ internal static string GetWindowPopupScript(OpenIdRelyingParty relyingParty, IAuthenticationRequest request, string windowName) {
+ Contract.Requires<ArgumentNullException>(relyingParty != null);
+ Contract.Requires<ArgumentNullException>(request != null);
+ Contract.Requires<ArgumentException>(!string.IsNullOrEmpty(windowName));
+
+ Uri popupUrl = request.RedirectingResponse.GetDirectUriRequest(relyingParty.Channel);
+
+ return string.Format(
+ CultureInfo.InvariantCulture,
+ "(window.showModalDialog ? window.showModalDialog({0}, {1}, 'status:0;resizable:1;scroll:1;center:1;dialogWidth:{2}px; dialogHeight:{3}') : window.open({0}, {1}, 'status=0,toolbar=0,location=1,resizable=1,scrollbars=1,left=' + ((screen.width - {2}) / 2) + ',top=' + ((screen.height - {3}) / 2) + ',width={2},height={3}'));",
+ MessagingUtilities.GetSafeJavascriptValue(popupUrl.AbsoluteUri),
+ MessagingUtilities.GetSafeJavascriptValue(windowName),
+ PopupWidth,
+ PopupHeight);
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/HostMetaDiscoveryService.cs b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/HostMetaDiscoveryService.cs
new file mode 100644
index 0000000..215ea24
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/HostMetaDiscoveryService.cs
@@ -0,0 +1,516 @@
+//-----------------------------------------------------------------------
+// <copyright file="HostMetaDiscoveryService.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OpenId {
+ using System;
+ using System.Collections.Generic;
+ using System.Diagnostics.CodeAnalysis;
+ using System.Diagnostics.Contracts;
+ using System.Globalization;
+ using System.IO;
+ using System.Linq;
+ using System.Net;
+ using System.Security;
+ using System.Security.Cryptography;
+ using System.Security.Cryptography.X509Certificates;
+ using System.Security.Permissions;
+ using System.Text;
+ using System.Text.RegularExpressions;
+ using System.Xml;
+ using System.Xml.XPath;
+ using DotNetOpenAuth.Messaging;
+ using DotNetOpenAuth.OpenId.RelyingParty;
+ using DotNetOpenAuth.Xrds;
+ using DotNetOpenAuth.Yadis;
+
+ /// <summary>
+ /// The discovery service to support host-meta based discovery, such as Google Apps for Domains.
+ /// </summary>
+ /// <remarks>
+ /// The spec for this discovery mechanism can be found at:
+ /// http://groups.google.com/group/google-federated-login-api/web/openid-discovery-for-hosted-domains
+ /// and the XMLDSig spec referenced in that spec can be found at:
+ /// http://wiki.oasis-open.org/xri/XrdOne/XmlDsigProfile
+ /// </remarks>
+ public class HostMetaDiscoveryService : IIdentifierDiscoveryService {
+ /// <summary>
+ /// The URI template for discovery host-meta on domains hosted by
+ /// Google Apps for Domains.
+ /// </summary>
+ private static readonly HostMetaProxy GoogleHostedHostMeta = new HostMetaProxy("https://www.google.com/accounts/o8/.well-known/host-meta?hd={0}", "hosted-id.google.com");
+
+ /// <summary>
+ /// Path to the well-known location of the host-meta document at a domain.
+ /// </summary>
+ private const string LocalHostMetaPath = "/.well-known/host-meta";
+
+ /// <summary>
+ /// The pattern within a host-meta file to look for to obtain the URI to the XRDS document.
+ /// </summary>
+ private static readonly Regex HostMetaLink = new Regex(@"^Link: <(?<location>.+?)>; rel=""describedby http://reltype.google.com/openid/xrd-op""; type=""application/xrds\+xml""$");
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="HostMetaDiscoveryService"/> class.
+ /// </summary>
+ public HostMetaDiscoveryService() {
+ this.TrustedHostMetaProxies = new List<HostMetaProxy>();
+ }
+
+ /// <summary>
+ /// Gets the set of URI templates to use to contact host-meta hosting proxies
+ /// for domain discovery.
+ /// </summary>
+ public IList<HostMetaProxy> TrustedHostMetaProxies { get; private set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether to trust Google to host domains' host-meta documents.
+ /// </summary>
+ /// <remarks>
+ /// This property is just a convenient mechanism for checking or changing the set of
+ /// trusted host-meta proxies in the <see cref="TrustedHostMetaProxies"/> property.
+ /// </remarks>
+ public bool UseGoogleHostedHostMeta {
+ get {
+ return this.TrustedHostMetaProxies.Contains(GoogleHostedHostMeta);
+ }
+
+ set {
+ if (value != this.UseGoogleHostedHostMeta) {
+ if (value) {
+ this.TrustedHostMetaProxies.Add(GoogleHostedHostMeta);
+ } else {
+ this.TrustedHostMetaProxies.Remove(GoogleHostedHostMeta);
+ }
+ }
+ }
+ }
+
+ #region IIdentifierDiscoveryService Members
+
+ /// <summary>
+ /// Performs discovery on the specified identifier.
+ /// </summary>
+ /// <param name="identifier">The identifier to perform discovery on.</param>
+ /// <param name="requestHandler">The means to place outgoing HTTP requests.</param>
+ /// <param name="abortDiscoveryChain">if set to <c>true</c>, no further discovery services will be called for this identifier.</param>
+ /// <returns>
+ /// A sequence of service endpoints yielded by discovery. Must not be null, but may be empty.
+ /// </returns>
+ public IEnumerable<IdentifierDiscoveryResult> Discover(Identifier identifier, IDirectWebRequestHandler requestHandler, out bool abortDiscoveryChain) {
+ abortDiscoveryChain = false;
+
+ // Google Apps are always URIs -- not XRIs.
+ var uriIdentifier = identifier as UriIdentifier;
+ if (uriIdentifier == null) {
+ return Enumerable.Empty<IdentifierDiscoveryResult>();
+ }
+
+ var results = new List<IdentifierDiscoveryResult>();
+ string signingHost;
+ using (var response = GetXrdsResponse(uriIdentifier, requestHandler, out signingHost)) {
+ if (response != null) {
+ try {
+ var document = new XrdsDocument(XmlReader.Create(response.ResponseStream));
+ ValidateXmlDSig(document, uriIdentifier, response, signingHost);
+ var xrds = GetXrdElements(document, uriIdentifier.Uri.Host);
+
+ // Look for claimed identifier template URIs for an additional XRDS document.
+ results.AddRange(GetExternalServices(xrds, uriIdentifier, requestHandler));
+
+ // If we couldn't find any claimed identifiers, look for OP identifiers.
+ // Normally this would be the opposite (OP Identifiers take precedence over
+ // claimed identifiers, but for Google Apps, XRDS' always have OP Identifiers
+ // mixed in, which the OpenID spec mandate should eclipse Claimed Identifiers,
+ // which would break positive assertion checks).
+ if (results.Count == 0) {
+ results.AddRange(xrds.CreateServiceEndpoints(uriIdentifier, uriIdentifier));
+ }
+
+ abortDiscoveryChain = true;
+ } catch (XmlException ex) {
+ Logger.Yadis.ErrorFormat("Error while parsing XRDS document at {0} pointed to by host-meta: {1}", response.FinalUri, ex);
+ }
+ }
+ }
+
+ return results;
+ }
+
+ #endregion
+
+ /// <summary>
+ /// Gets the XRD elements that have a given CanonicalID.
+ /// </summary>
+ /// <param name="document">The XRDS document.</param>
+ /// <param name="canonicalId">The CanonicalID to match on.</param>
+ /// <returns>A sequence of XRD elements.</returns>
+ private static IEnumerable<XrdElement> GetXrdElements(XrdsDocument document, string canonicalId) {
+ // filter to include only those XRD elements describing the host whose host-meta pointed us to this document.
+ return document.XrdElements.Where(xrd => string.Equals(xrd.CanonicalID, canonicalId, StringComparison.Ordinal));
+ }
+
+ /// <summary>
+ /// Gets the described-by services in XRD elements.
+ /// </summary>
+ /// <param name="xrds">The XRDs to search.</param>
+ /// <returns>A sequence of services.</returns>
+ private static IEnumerable<ServiceElement> GetDescribedByServices(IEnumerable<XrdElement> xrds) {
+ Contract.Requires<ArgumentNullException>(xrds != null);
+ Contract.Ensures(Contract.Result<IEnumerable<ServiceElement>>() != null);
+
+ var describedBy = from xrd in xrds
+ from service in xrd.SearchForServiceTypeUris(p => "http://www.iana.org/assignments/relation/describedby")
+ select service;
+ return describedBy;
+ }
+
+ /// <summary>
+ /// Gets the services for an identifier that are described by an external XRDS document.
+ /// </summary>
+ /// <param name="xrds">The XRD elements to search for described-by services.</param>
+ /// <param name="identifier">The identifier under discovery.</param>
+ /// <param name="requestHandler">The request handler.</param>
+ /// <returns>The discovered services.</returns>
+ private static IEnumerable<IdentifierDiscoveryResult> GetExternalServices(IEnumerable<XrdElement> xrds, UriIdentifier identifier, IDirectWebRequestHandler requestHandler) {
+ Contract.Requires<ArgumentNullException>(xrds != null);
+ Contract.Requires<ArgumentNullException>(identifier != null);
+ Contract.Requires<ArgumentNullException>(requestHandler != null);
+ Contract.Ensures(Contract.Result<IEnumerable<IdentifierDiscoveryResult>>() != null);
+
+ var results = new List<IdentifierDiscoveryResult>();
+ foreach (var serviceElement in GetDescribedByServices(xrds)) {
+ var templateNode = serviceElement.Node.SelectSingleNode("google:URITemplate", serviceElement.XmlNamespaceResolver);
+ var nextAuthorityNode = serviceElement.Node.SelectSingleNode("google:NextAuthority", serviceElement.XmlNamespaceResolver);
+ if (templateNode != null) {
+ Uri externalLocation = new Uri(templateNode.Value.Trim().Replace("{%uri}", Uri.EscapeDataString(identifier.Uri.AbsoluteUri)));
+ string nextAuthority = nextAuthorityNode != null ? nextAuthorityNode.Value.Trim() : identifier.Uri.Host;
+ try {
+ using (var externalXrdsResponse = GetXrdsResponse(identifier, requestHandler, externalLocation)) {
+ XrdsDocument externalXrds = new XrdsDocument(XmlReader.Create(externalXrdsResponse.ResponseStream));
+ ValidateXmlDSig(externalXrds, identifier, externalXrdsResponse, nextAuthority);
+ results.AddRange(GetXrdElements(externalXrds, identifier).CreateServiceEndpoints(identifier, identifier));
+ }
+ } catch (ProtocolException ex) {
+ Logger.Yadis.WarnFormat("HTTP GET error while retrieving described-by XRDS document {0}: {1}", externalLocation.AbsoluteUri, ex);
+ } catch (XmlException ex) {
+ Logger.Yadis.ErrorFormat("Error while parsing described-by XRDS document {0}: {1}", externalLocation.AbsoluteUri, ex);
+ }
+ }
+ }
+
+ return results;
+ }
+
+ /// <summary>
+ /// Validates the XML digital signature on an XRDS document.
+ /// </summary>
+ /// <param name="document">The XRDS document whose signature should be validated.</param>
+ /// <param name="identifier">The identifier under discovery.</param>
+ /// <param name="response">The response.</param>
+ /// <param name="signingHost">The host name on the certificate that should be used to verify the signature in the XRDS.</param>
+ /// <exception cref="ProtocolException">Thrown if the XRDS document has an invalid or a missing signature.</exception>
+ [SuppressMessage("Microsoft.Naming", "CA2204:Literals should be spelled correctly", MessageId = "XmlDSig", Justification = "xml")]
+ private static void ValidateXmlDSig(XrdsDocument document, UriIdentifier identifier, IncomingWebResponse response, string signingHost) {
+ Contract.Requires<ArgumentNullException>(document != null);
+ Contract.Requires<ArgumentNullException>(identifier != null);
+ Contract.Requires<ArgumentNullException>(response != null);
+
+ var signatureNode = document.Node.SelectSingleNode("/xrds:XRDS/ds:Signature", document.XmlNamespaceResolver);
+ ErrorUtilities.VerifyProtocol(signatureNode != null, OpenIdStrings.MissingElement, "Signature");
+ var signedInfoNode = signatureNode.SelectSingleNode("ds:SignedInfo", document.XmlNamespaceResolver);
+ ErrorUtilities.VerifyProtocol(signedInfoNode != null, OpenIdStrings.MissingElement, "SignedInfo");
+ ErrorUtilities.VerifyProtocol(
+ signedInfoNode.SelectSingleNode("ds:CanonicalizationMethod[@Algorithm='http://docs.oasis-open.org/xri/xrd/2009/01#canonicalize-raw-octets']", document.XmlNamespaceResolver) != null,
+ OpenIdStrings.UnsupportedCanonicalizationMethod);
+ ErrorUtilities.VerifyProtocol(
+ signedInfoNode.SelectSingleNode("ds:SignatureMethod[@Algorithm='http://www.w3.org/2000/09/xmldsig#rsa-sha1']", document.XmlNamespaceResolver) != null,
+ OpenIdStrings.UnsupportedSignatureMethod);
+ var certNodes = signatureNode.Select("ds:KeyInfo/ds:X509Data/ds:X509Certificate", document.XmlNamespaceResolver);
+ ErrorUtilities.VerifyProtocol(certNodes.Count > 0, OpenIdStrings.MissingElement, "X509Certificate");
+ var certs = certNodes.Cast<XPathNavigator>().Select(n => new X509Certificate2(Convert.FromBase64String(n.Value.Trim()))).ToList();
+
+ // Verify that we trust the signer of the certificates.
+ // Start by trying to validate just the certificate used to sign the XRDS document,
+ // since we can do that with partial trust.
+ Logger.OpenId.Debug("Verifying that we trust the certificate used to sign the discovery document.");
+ if (!certs[0].Verify()) {
+ // We couldn't verify just the signing certificate, so try to verify the whole certificate chain.
+ try {
+ Logger.OpenId.Debug("Verifying the whole certificate chain.");
+ VerifyCertChain(certs);
+ Logger.OpenId.Debug("Certificate chain verified.");
+ } catch (SecurityException) {
+ Logger.Yadis.Warn("Signing certificate verification failed and we have insufficient code access security permissions to perform certificate chain validation.");
+ ErrorUtilities.ThrowProtocol(OpenIdStrings.X509CertificateNotTrusted);
+ }
+ }
+
+ // Verify that the certificate is issued to the host on whom we are performing discovery.
+ string hostName = certs[0].GetNameInfo(X509NameType.DnsName, false);
+ ErrorUtilities.VerifyProtocol(string.Equals(hostName, signingHost, StringComparison.OrdinalIgnoreCase), OpenIdStrings.MisdirectedSigningCertificate, hostName, signingHost);
+
+ // Verify the signature itself
+ byte[] signature = Convert.FromBase64String(response.Headers["Signature"]);
+ var provider = (RSACryptoServiceProvider)certs.First().PublicKey.Key;
+ byte[] data = new byte[response.ResponseStream.Length];
+ response.ResponseStream.Seek(0, SeekOrigin.Begin);
+ response.ResponseStream.Read(data, 0, data.Length);
+ ErrorUtilities.VerifyProtocol(provider.VerifyData(data, "SHA1", signature), OpenIdStrings.InvalidDSig);
+ }
+
+ /// <summary>
+ /// Verifies the cert chain.
+ /// </summary>
+ /// <param name="certs">The certs.</param>
+ /// <remarks>
+ /// This must be in a method of its own because there is a LinkDemand on the <see cref="X509Chain.Build"/>
+ /// method. By being in a method of its own, the caller of this method may catch a
+ /// <see cref="SecurityException"/> that is thrown if we're not running with full trust and execute
+ /// an alternative plan.
+ /// </remarks>
+ /// <exception cref="ProtocolException">Thrown if the certificate chain is invalid or unverifiable.</exception>
+ [SuppressMessage("Microsoft.Globalization", "CA1303:Do not pass literals as localized parameters", MessageId = "DotNetOpenAuth.Messaging.ErrorUtilities.ThrowProtocol(System.String,System.Object[])", Justification = "The localized portion is a string resource already."), SuppressMessage("Microsoft.Security", "CA2122:DoNotIndirectlyExposeMethodsWithLinkDemands", Justification = "By design")]
+ private static void VerifyCertChain(List<X509Certificate2> certs) {
+ var chain = new X509Chain();
+ foreach (var cert in certs) {
+ chain.Build(cert);
+ }
+
+ if (chain.ChainStatus.Length > 0) {
+ ErrorUtilities.ThrowProtocol(
+ string.Format(
+ CultureInfo.CurrentCulture,
+ OpenIdStrings.X509CertificateNotTrusted + " {0}",
+ string.Join(", ", chain.ChainStatus.Select(status => status.StatusInformation).ToArray())));
+ }
+ }
+
+ /// <summary>
+ /// Gets the XRDS HTTP response for a given identifier.
+ /// </summary>
+ /// <param name="identifier">The identifier.</param>
+ /// <param name="requestHandler">The request handler.</param>
+ /// <param name="xrdsLocation">The location of the XRDS document to retrieve.</param>
+ /// <returns>
+ /// A HTTP response carrying an XRDS document.
+ /// </returns>
+ /// <exception cref="ProtocolException">Thrown if the XRDS document could not be obtained.</exception>
+ private static IncomingWebResponse GetXrdsResponse(UriIdentifier identifier, IDirectWebRequestHandler requestHandler, Uri xrdsLocation) {
+ Contract.Requires<ArgumentNullException>(identifier != null);
+ Contract.Requires<ArgumentNullException>(requestHandler != null);
+ Contract.Requires<ArgumentNullException>(xrdsLocation != null);
+ Contract.Ensures(Contract.Result<IncomingWebResponse>() != null);
+
+ var request = (HttpWebRequest)WebRequest.Create(xrdsLocation);
+ request.CachePolicy = Yadis.IdentifierDiscoveryCachePolicy;
+ request.Accept = ContentTypes.Xrds;
+ var options = identifier.IsDiscoverySecureEndToEnd ? DirectWebRequestOptions.RequireSsl : DirectWebRequestOptions.None;
+ var response = requestHandler.GetResponse(request, options).GetSnapshot(Yadis.MaximumResultToScan);
+ if (!string.Equals(response.ContentType.MediaType, ContentTypes.Xrds, StringComparison.Ordinal)) {
+ Logger.Yadis.WarnFormat("Host-meta pointed to XRDS at {0}, but Content-Type at that URL was unexpected value '{1}'.", xrdsLocation, response.ContentType);
+ }
+
+ return response;
+ }
+
+ /// <summary>
+ /// Gets the XRDS HTTP response for a given identifier.
+ /// </summary>
+ /// <param name="identifier">The identifier.</param>
+ /// <param name="requestHandler">The request handler.</param>
+ /// <param name="signingHost">The host name on the certificate that should be used to verify the signature in the XRDS.</param>
+ /// <returns>A HTTP response carrying an XRDS document, or <c>null</c> if one could not be obtained.</returns>
+ /// <exception cref="ProtocolException">Thrown if the XRDS document could not be obtained.</exception>
+ private IncomingWebResponse GetXrdsResponse(UriIdentifier identifier, IDirectWebRequestHandler requestHandler, out string signingHost) {
+ Contract.Requires<ArgumentNullException>(identifier != null);
+ Contract.Requires<ArgumentNullException>(requestHandler != null);
+ Uri xrdsLocation = this.GetXrdsLocation(identifier, requestHandler, out signingHost);
+ if (xrdsLocation == null) {
+ return null;
+ }
+
+ var response = GetXrdsResponse(identifier, requestHandler, xrdsLocation);
+
+ return response;
+ }
+
+ /// <summary>
+ /// Gets the location of the XRDS document that describes a given identifier.
+ /// </summary>
+ /// <param name="identifier">The identifier under discovery.</param>
+ /// <param name="requestHandler">The request handler.</param>
+ /// <param name="signingHost">The host name on the certificate that should be used to verify the signature in the XRDS.</param>
+ /// <returns>An absolute URI, or <c>null</c> if one could not be determined.</returns>
+ private Uri GetXrdsLocation(UriIdentifier identifier, IDirectWebRequestHandler requestHandler, out string signingHost) {
+ Contract.Requires<ArgumentNullException>(identifier != null);
+ Contract.Requires<ArgumentNullException>(requestHandler != null);
+ using (var hostMetaResponse = this.GetHostMeta(identifier, requestHandler, out signingHost)) {
+ if (hostMetaResponse == null) {
+ return null;
+ }
+
+ using (var sr = hostMetaResponse.GetResponseReader()) {
+ string line = sr.ReadLine();
+ Match m = HostMetaLink.Match(line);
+ if (m.Success) {
+ Uri location = new Uri(m.Groups["location"].Value);
+ Logger.Yadis.InfoFormat("Found link to XRDS at {0} in host-meta document {1}.", location, hostMetaResponse.FinalUri);
+ return location;
+ }
+ }
+
+ Logger.Yadis.WarnFormat("Could not find link to XRDS in host-meta document: {0}", hostMetaResponse.FinalUri);
+ return null;
+ }
+ }
+
+ /// <summary>
+ /// Gets the host-meta for a given identifier.
+ /// </summary>
+ /// <param name="identifier">The identifier.</param>
+ /// <param name="requestHandler">The request handler.</param>
+ /// <param name="signingHost">The host name on the certificate that should be used to verify the signature in the XRDS.</param>
+ /// <returns>
+ /// The host-meta response, or <c>null</c> if no host-meta document could be obtained.
+ /// </returns>
+ private IncomingWebResponse GetHostMeta(UriIdentifier identifier, IDirectWebRequestHandler requestHandler, out string signingHost) {
+ Contract.Requires<ArgumentNullException>(identifier != null);
+ Contract.Requires<ArgumentNullException>(requestHandler != null);
+ foreach (var hostMetaProxy in this.GetHostMetaLocations(identifier)) {
+ var hostMetaLocation = hostMetaProxy.GetProxy(identifier);
+ var request = (HttpWebRequest)WebRequest.Create(hostMetaLocation);
+ request.CachePolicy = Yadis.IdentifierDiscoveryCachePolicy;
+ var options = DirectWebRequestOptions.AcceptAllHttpResponses;
+ if (identifier.IsDiscoverySecureEndToEnd) {
+ options |= DirectWebRequestOptions.RequireSsl;
+ }
+ var response = requestHandler.GetResponse(request, options).GetSnapshot(Yadis.MaximumResultToScan);
+ try {
+ if (response.Status == HttpStatusCode.OK) {
+ Logger.Yadis.InfoFormat("Found host-meta for {0} at: {1}", identifier.Uri.Host, hostMetaLocation);
+ signingHost = hostMetaProxy.GetSigningHost(identifier);
+ return response;
+ } else {
+ Logger.Yadis.InfoFormat("Could not obtain host-meta for {0} from {1}", identifier.Uri.Host, hostMetaLocation);
+ response.Dispose();
+ }
+ } catch {
+ response.Dispose();
+ throw;
+ }
+ }
+
+ signingHost = null;
+ return null;
+ }
+
+ /// <summary>
+ /// Gets the URIs authorized to host host-meta documents on behalf of a given domain.
+ /// </summary>
+ /// <param name="identifier">The identifier.</param>
+ /// <returns>A sequence of URIs that MAY provide the host-meta for a given identifier.</returns>
+ private IEnumerable<HostMetaProxy> GetHostMetaLocations(UriIdentifier identifier) {
+ Contract.Requires<ArgumentNullException>(identifier != null);
+
+ // First try the proxies, as they are considered more "secure" than the local
+ // host-meta for a domain since the domain may be defaced.
+ IEnumerable<HostMetaProxy> result = this.TrustedHostMetaProxies;
+
+ // Finally, look for the local host-meta.
+ UriBuilder localHostMetaBuilder = new UriBuilder();
+ localHostMetaBuilder.Scheme = identifier.IsDiscoverySecureEndToEnd || identifier.Uri.IsTransportSecure() ? Uri.UriSchemeHttps : Uri.UriSchemeHttp;
+ localHostMetaBuilder.Host = identifier.Uri.Host;
+ localHostMetaBuilder.Path = LocalHostMetaPath;
+ result = result.Concat(new[] { new HostMetaProxy(localHostMetaBuilder.Uri.AbsoluteUri, identifier.Uri.Host) });
+
+ return result;
+ }
+
+ /// <summary>
+ /// A description of a web server that hosts host-meta documents.
+ /// </summary>
+ [SuppressMessage("Microsoft.Design", "CA1034:NestedTypesShouldNotBeVisible", Justification = "By design")]
+ public class HostMetaProxy {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="HostMetaProxy"/> class.
+ /// </summary>
+ /// <param name="proxyFormat">The proxy formatting string.</param>
+ /// <param name="signingHostFormat">The signing host formatting string.</param>
+ public HostMetaProxy(string proxyFormat, string signingHostFormat) {
+ Contract.Requires<ArgumentException>(!String.IsNullOrEmpty(proxyFormat));
+ Contract.Requires<ArgumentException>(!String.IsNullOrEmpty(signingHostFormat));
+ this.ProxyFormat = proxyFormat;
+ this.SigningHostFormat = signingHostFormat;
+ }
+
+ /// <summary>
+ /// Gets the URL of the host-meta proxy.
+ /// </summary>
+ /// <value>The absolute proxy URL, which may include {0} to be replaced with the host of the identifier to be discovered.</value>
+ public string ProxyFormat { get; private set; }
+
+ /// <summary>
+ /// Gets the formatting string to determine the expected host name on the certificate
+ /// that is expected to be used to sign the XRDS document.
+ /// </summary>
+ /// <value>
+ /// Either a string literal, or a formatting string where these placeholders may exist:
+ /// {0} the host on the identifier discovery was originally performed on;
+ /// {1} the host on this proxy.
+ /// </value>
+ public string SigningHostFormat { get; private set; }
+
+ /// <summary>
+ /// Gets the absolute proxy URI.
+ /// </summary>
+ /// <param name="identifier">The identifier being discovered.</param>
+ /// <returns>The an absolute URI.</returns>
+ public virtual Uri GetProxy(UriIdentifier identifier) {
+ Contract.Requires<ArgumentNullException>(identifier != null);
+ return new Uri(string.Format(CultureInfo.InvariantCulture, this.ProxyFormat, Uri.EscapeDataString(identifier.Uri.Host)));
+ }
+
+ /// <summary>
+ /// Gets the signing host URI.
+ /// </summary>
+ /// <param name="identifier">The identifier being discovered.</param>
+ /// <returns>A host name.</returns>
+ public virtual string GetSigningHost(UriIdentifier identifier) {
+ Contract.Requires<ArgumentNullException>(identifier != null);
+ return string.Format(CultureInfo.InvariantCulture, this.SigningHostFormat, identifier.Uri.Host, this.GetProxy(identifier).Host);
+ }
+
+ /// <summary>
+ /// Determines whether the specified <see cref="T:System.Object"/> is equal to the current <see cref="T:System.Object"/>.
+ /// </summary>
+ /// <param name="obj">The <see cref="T:System.Object"/> to compare with the current <see cref="T:System.Object"/>.</param>
+ /// <returns>
+ /// true if the specified <see cref="T:System.Object"/> is equal to the current <see cref="T:System.Object"/>; otherwise, false.
+ /// </returns>
+ /// <exception cref="T:System.NullReferenceException">
+ /// The <paramref name="obj"/> parameter is null.
+ /// </exception>
+ public override bool Equals(object obj) {
+ var other = obj as HostMetaProxy;
+ if (other == null) {
+ return false;
+ }
+
+ return this.ProxyFormat == other.ProxyFormat && this.SigningHostFormat == other.SigningHostFormat;
+ }
+
+ /// <summary>
+ /// Serves as a hash function for a particular type.
+ /// </summary>
+ /// <returns>
+ /// A hash code for the current <see cref="T:System.Object"/>.
+ /// </returns>
+ public override int GetHashCode() {
+ return this.ProxyFormat.GetHashCode();
+ }
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/IIdentifierDiscoveryService.cs b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/IIdentifierDiscoveryService.cs
new file mode 100644
index 0000000..fcea327
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/IIdentifierDiscoveryService.cs
@@ -0,0 +1,67 @@
+//-----------------------------------------------------------------------
+// <copyright file="IIdentifierDiscoveryService.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OpenId {
+ using System;
+ using System.Collections.Generic;
+ using System.Diagnostics.CodeAnalysis;
+ using System.Diagnostics.Contracts;
+ using System.Linq;
+ using System.Text;
+ using DotNetOpenAuth.Messaging;
+ using DotNetOpenAuth.OpenId.RelyingParty;
+
+ /// <summary>
+ /// A module that provides discovery services for OpenID identifiers.
+ /// </summary>
+ [ContractClass(typeof(IIdentifierDiscoveryServiceContract))]
+ public interface IIdentifierDiscoveryService {
+ /// <summary>
+ /// Performs discovery on the specified identifier.
+ /// </summary>
+ /// <param name="identifier">The identifier to perform discovery on.</param>
+ /// <param name="requestHandler">The means to place outgoing HTTP requests.</param>
+ /// <param name="abortDiscoveryChain">if set to <c>true</c>, no further discovery services will be called for this identifier.</param>
+ /// <returns>
+ /// A sequence of service endpoints yielded by discovery. Must not be null, but may be empty.
+ /// </returns>
+ [SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "2#", Justification = "By design")]
+ [Pure]
+ IEnumerable<IdentifierDiscoveryResult> Discover(Identifier identifier, IDirectWebRequestHandler requestHandler, out bool abortDiscoveryChain);
+ }
+
+ /// <summary>
+ /// Code contract for the <see cref="IIdentifierDiscoveryService"/> interface.
+ /// </summary>
+ [ContractClassFor(typeof(IIdentifierDiscoveryService))]
+ internal abstract class IIdentifierDiscoveryServiceContract : IIdentifierDiscoveryService {
+ /// <summary>
+ /// Prevents a default instance of the <see cref="IIdentifierDiscoveryServiceContract"/> class from being created.
+ /// </summary>
+ private IIdentifierDiscoveryServiceContract() {
+ }
+
+ #region IDiscoveryService Members
+
+ /// <summary>
+ /// Performs discovery on the specified identifier.
+ /// </summary>
+ /// <param name="identifier">The identifier to perform discovery on.</param>
+ /// <param name="requestHandler">The means to place outgoing HTTP requests.</param>
+ /// <param name="abortDiscoveryChain">if set to <c>true</c>, no further discovery services will be called for this identifier.</param>
+ /// <returns>
+ /// A sequence of service endpoints yielded by discovery. Must not be null, but may be empty.
+ /// </returns>
+ IEnumerable<IdentifierDiscoveryResult> IIdentifierDiscoveryService.Discover(Identifier identifier, IDirectWebRequestHandler requestHandler, out bool abortDiscoveryChain) {
+ Contract.Requires<ArgumentNullException>(identifier != null);
+ Contract.Requires<ArgumentNullException>(requestHandler != null);
+ Contract.Ensures(Contract.Result<IEnumerable<IdentifierDiscoveryResult>>() != null);
+ throw new NotImplementedException();
+ }
+
+ #endregion
+ }
+}
diff --git a/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/IdentifierDiscoveryResult.cs b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/IdentifierDiscoveryResult.cs
new file mode 100644
index 0000000..c851f24
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/IdentifierDiscoveryResult.cs
@@ -0,0 +1,497 @@
+//-----------------------------------------------------------------------
+// <copyright file="IdentifierDiscoveryResult.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OpenId {
+ using System;
+ using System.Collections.Generic;
+ using System.Collections.ObjectModel;
+ using System.Diagnostics;
+ using System.Diagnostics.CodeAnalysis;
+ using System.Diagnostics.Contracts;
+ using System.Globalization;
+ using System.IO;
+ using System.Linq;
+ using System.Text;
+ using DotNetOpenAuth.Messaging;
+ using DotNetOpenAuth.OpenId.Messages;
+ using DotNetOpenAuth.OpenId.RelyingParty;
+
+ /// <summary>
+ /// Represents a single OP endpoint from discovery on some OpenID Identifier.
+ /// </summary>
+ [DebuggerDisplay("ClaimedIdentifier: {ClaimedIdentifier}, ProviderEndpoint: {ProviderEndpoint}, OpenId: {Protocol.Version}")]
+ public sealed class IdentifierDiscoveryResult : IProviderEndpoint {
+ /// <summary>
+ /// Backing field for the <see cref="Protocol"/> property.
+ /// </summary>
+ private Protocol protocol;
+
+ /// <summary>
+ /// Backing field for the <see cref="ClaimedIdentifier"/> property.
+ /// </summary>
+ private Identifier claimedIdentifier;
+
+ /// <summary>
+ /// Backing field for the <see cref="FriendlyIdentifierForDisplay"/> property.
+ /// </summary>
+ private string friendlyIdentifierForDisplay;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="IdentifierDiscoveryResult"/> class.
+ /// </summary>
+ /// <param name="providerEndpoint">The provider endpoint.</param>
+ /// <param name="claimedIdentifier">The Claimed Identifier.</param>
+ /// <param name="userSuppliedIdentifier">The User-supplied Identifier.</param>
+ /// <param name="providerLocalIdentifier">The Provider Local Identifier.</param>
+ /// <param name="servicePriority">The service priority.</param>
+ /// <param name="uriPriority">The URI priority.</param>
+ private IdentifierDiscoveryResult(ProviderEndpointDescription providerEndpoint, Identifier claimedIdentifier, Identifier userSuppliedIdentifier, Identifier providerLocalIdentifier, int? servicePriority, int? uriPriority) {
+ Contract.Requires<ArgumentNullException>(providerEndpoint != null);
+ Contract.Requires<ArgumentNullException>(claimedIdentifier != null);
+ this.ProviderEndpoint = providerEndpoint.Uri;
+ this.Capabilities = new ReadOnlyCollection<string>(providerEndpoint.Capabilities);
+ this.Version = providerEndpoint.Version;
+ this.ClaimedIdentifier = claimedIdentifier;
+ this.ProviderLocalIdentifier = providerLocalIdentifier ?? claimedIdentifier;
+ this.UserSuppliedIdentifier = userSuppliedIdentifier;
+ this.ServicePriority = servicePriority;
+ this.ProviderEndpointPriority = uriPriority;
+ }
+
+ /// <summary>
+ /// Gets the detected version of OpenID implemented by the Provider.
+ /// </summary>
+ public Version Version { get; private set; }
+
+ /// <summary>
+ /// Gets the Identifier that was presented by the end user to the Relying Party,
+ /// or selected by the user at the OpenID Provider.
+ /// During the initiation phase of the protocol, an end user may enter
+ /// either their own Identifier or an OP Identifier. If an OP Identifier
+ /// is used, the OP may then assist the end user in selecting an Identifier
+ /// to share with the Relying Party.
+ /// </summary>
+ public Identifier UserSuppliedIdentifier { get; private set; }
+
+ /// <summary>
+ /// Gets the Identifier that the end user claims to control.
+ /// </summary>
+ public Identifier ClaimedIdentifier {
+ get {
+ return this.claimedIdentifier;
+ }
+
+ internal set {
+ // Take care to reparse the incoming identifier to make sure it's
+ // not a derived type that will override expected behavior.
+ // Elsewhere in this class, we count on the fact that this property
+ // is either UriIdentifier or XriIdentifier. MockIdentifier messes it up.
+ this.claimedIdentifier = value != null ? Identifier.Reparse(value) : null;
+ }
+ }
+
+ /// <summary>
+ /// Gets an alternate Identifier for an end user that is local to a
+ /// particular OP and thus not necessarily under the end user's
+ /// control.
+ /// </summary>
+ public Identifier ProviderLocalIdentifier { get; private set; }
+
+ /// <summary>
+ /// Gets a more user-friendly (but NON-secure!) string to display to the user as his identifier.
+ /// </summary>
+ /// <returns>A human-readable, abbreviated (but not secure) identifier the user MAY recognize as his own.</returns>
+ public string FriendlyIdentifierForDisplay {
+ get {
+ if (this.friendlyIdentifierForDisplay == null) {
+ XriIdentifier xri = this.ClaimedIdentifier as XriIdentifier;
+ UriIdentifier uri = this.ClaimedIdentifier as UriIdentifier;
+ if (xri != null) {
+ if (this.UserSuppliedIdentifier == null || String.Equals(this.UserSuppliedIdentifier, this.ClaimedIdentifier, StringComparison.OrdinalIgnoreCase)) {
+ this.friendlyIdentifierForDisplay = this.ClaimedIdentifier;
+ } else {
+ this.friendlyIdentifierForDisplay = this.UserSuppliedIdentifier;
+ }
+ } else if (uri != null) {
+ if (uri != this.Protocol.ClaimedIdentifierForOPIdentifier) {
+ string displayUri = uri.Uri.Host;
+
+ // We typically want to display the path, because that will often have the username in it.
+ // As Google Apps for Domains and the like become more popular, a standard /openid path
+ // will often appear, which is not helpful to identifying the user so we'll avoid including
+ // that path if it's present.
+ if (!string.Equals(uri.Uri.AbsolutePath, "/openid", StringComparison.OrdinalIgnoreCase)) {
+ displayUri += uri.Uri.AbsolutePath.TrimEnd('/');
+ }
+
+ // Multi-byte unicode characters get encoded by the Uri class for transit.
+ // Since this is for display purposes, we want to reverse this and display a readable
+ // representation of these foreign characters.
+ this.friendlyIdentifierForDisplay = Uri.UnescapeDataString(displayUri);
+ }
+ } else {
+ ErrorUtilities.ThrowInternal("ServiceEndpoint.ClaimedIdentifier neither XRI nor URI.");
+ this.friendlyIdentifierForDisplay = this.ClaimedIdentifier;
+ }
+ }
+
+ return this.friendlyIdentifierForDisplay;
+ }
+ }
+
+ /// <summary>
+ /// Gets the provider endpoint.
+ /// </summary>
+ public Uri ProviderEndpoint { get; private set; }
+
+ /// <summary>
+ /// Gets the @priority given in the XRDS document for this specific OP endpoint.
+ /// </summary>
+ public int? ProviderEndpointPriority { get; private set; }
+
+ /// <summary>
+ /// Gets the @priority given in the XRDS document for this service
+ /// (which may consist of several endpoints).
+ /// </summary>
+ public int? ServicePriority { get; private set; }
+
+ /// <summary>
+ /// Gets the collection of service type URIs found in the XRDS document describing this Provider.
+ /// </summary>
+ /// <value>Should never be null, but may be empty.</value>
+ public ReadOnlyCollection<string> Capabilities { get; private set; }
+
+ #region IProviderEndpoint Members
+
+ /// <summary>
+ /// Gets the URL that the OpenID Provider receives authentication requests at.
+ /// </summary>
+ /// <value>This value MUST be an absolute HTTP or HTTPS URL.</value>
+ Uri IProviderEndpoint.Uri {
+ get { return this.ProviderEndpoint; }
+ }
+
+ #endregion
+
+ /// <summary>
+ /// Gets an XRDS sorting routine that uses the XRDS Service/@Priority
+ /// attribute to determine order.
+ /// </summary>
+ /// <remarks>
+ /// Endpoints lacking any priority value are sorted to the end of the list.
+ /// </remarks>
+ internal static Comparison<IdentifierDiscoveryResult> EndpointOrder {
+ get {
+ // Sort first by service type (OpenID 2.0, 1.1, 1.0),
+ // then by Service/@priority, then by Service/Uri/@priority
+ return (se1, se2) => {
+ int result = GetEndpointPrecedenceOrderByServiceType(se1).CompareTo(GetEndpointPrecedenceOrderByServiceType(se2));
+ if (result != 0) {
+ return result;
+ }
+ if (se1.ServicePriority.HasValue && se2.ServicePriority.HasValue) {
+ result = se1.ServicePriority.Value.CompareTo(se2.ServicePriority.Value);
+ if (result != 0) {
+ return result;
+ }
+ if (se1.ProviderEndpointPriority.HasValue && se2.ProviderEndpointPriority.HasValue) {
+ return se1.ProviderEndpointPriority.Value.CompareTo(se2.ProviderEndpointPriority.Value);
+ } else if (se1.ProviderEndpointPriority.HasValue) {
+ return -1;
+ } else if (se2.ProviderEndpointPriority.HasValue) {
+ return 1;
+ } else {
+ return 0;
+ }
+ } else {
+ if (se1.ServicePriority.HasValue) {
+ return -1;
+ } else if (se2.ServicePriority.HasValue) {
+ return 1;
+ } else {
+ // neither service defines a priority, so base ordering by uri priority.
+ if (se1.ProviderEndpointPriority.HasValue && se2.ProviderEndpointPriority.HasValue) {
+ return se1.ProviderEndpointPriority.Value.CompareTo(se2.ProviderEndpointPriority.Value);
+ } else if (se1.ProviderEndpointPriority.HasValue) {
+ return -1;
+ } else if (se2.ProviderEndpointPriority.HasValue) {
+ return 1;
+ } else {
+ return 0;
+ }
+ }
+ }
+ };
+ }
+ }
+
+ /// <summary>
+ /// Gets the protocol used by the OpenID Provider.
+ /// </summary>
+ internal Protocol Protocol {
+ get {
+ if (this.protocol == null) {
+ this.protocol = Protocol.Lookup(this.Version);
+ }
+
+ return this.protocol;
+ }
+ }
+
+ /// <summary>
+ /// Implements the operator ==.
+ /// </summary>
+ /// <param name="se1">The first service endpoint.</param>
+ /// <param name="se2">The second service endpoint.</param>
+ /// <returns>The result of the operator.</returns>
+ public static bool operator ==(IdentifierDiscoveryResult se1, IdentifierDiscoveryResult se2) {
+ return se1.EqualsNullSafe(se2);
+ }
+
+ /// <summary>
+ /// Implements the operator !=.
+ /// </summary>
+ /// <param name="se1">The first service endpoint.</param>
+ /// <param name="se2">The second service endpoint.</param>
+ /// <returns>The result of the operator.</returns>
+ public static bool operator !=(IdentifierDiscoveryResult se1, IdentifierDiscoveryResult se2) {
+ return !(se1 == se2);
+ }
+
+ /// <summary>
+ /// Determines whether the specified <see cref="T:System.Object"/> is equal to the current <see cref="T:System.Object"/>.
+ /// </summary>
+ /// <param name="obj">The <see cref="T:System.Object"/> to compare with the current <see cref="T:System.Object"/>.</param>
+ /// <returns>
+ /// true if the specified <see cref="T:System.Object"/> is equal to the current <see cref="T:System.Object"/>; otherwise, false.
+ /// </returns>
+ /// <exception cref="T:System.NullReferenceException">
+ /// The <paramref name="obj"/> parameter is null.
+ /// </exception>
+ public override bool Equals(object obj) {
+ var other = obj as IdentifierDiscoveryResult;
+ if (other == null) {
+ return false;
+ }
+
+ // We specifically do not check our ProviderSupportedServiceTypeUris array
+ // or the priority field
+ // as that is not persisted in our tokens, and it is not part of the
+ // important assertion validation that is part of the spec.
+ return
+ this.ClaimedIdentifier == other.ClaimedIdentifier &&
+ this.ProviderEndpoint == other.ProviderEndpoint &&
+ this.ProviderLocalIdentifier == other.ProviderLocalIdentifier &&
+ this.Protocol.EqualsPractically(other.Protocol);
+ }
+
+ /// <summary>
+ /// Serves as a hash function for a particular type.
+ /// </summary>
+ /// <returns>
+ /// A hash code for the current <see cref="T:System.Object"/>.
+ /// </returns>
+ public override int GetHashCode() {
+ return this.ClaimedIdentifier.GetHashCode();
+ }
+
+ /// <summary>
+ /// Returns a <see cref="T:System.String"/> that represents the current <see cref="T:System.Object"/>.
+ /// </summary>
+ /// <returns>
+ /// A <see cref="T:System.String"/> that represents the current <see cref="T:System.Object"/>.
+ /// </returns>
+ public override string ToString() {
+ StringBuilder builder = new StringBuilder();
+ builder.AppendLine("ClaimedIdentifier: " + this.ClaimedIdentifier);
+ builder.AppendLine("ProviderLocalIdentifier: " + this.ProviderLocalIdentifier);
+ builder.AppendLine("ProviderEndpoint: " + this.ProviderEndpoint);
+ builder.AppendLine("OpenID version: " + this.Version);
+ builder.AppendLine("Service Type URIs:");
+ foreach (string serviceTypeUri in this.Capabilities) {
+ builder.Append("\t");
+ builder.AppendLine(serviceTypeUri);
+ }
+ builder.Length -= Environment.NewLine.Length; // trim last newline
+ return builder.ToString();
+ }
+
+ /// <summary>
+ /// Checks whether the OpenId Identifier claims support for a given extension.
+ /// </summary>
+ /// <typeparam name="T">The extension whose support is being queried.</typeparam>
+ /// <returns>
+ /// True if support for the extension is advertised. False otherwise.
+ /// </returns>
+ /// <remarks>
+ /// Note that a true or false return value is no guarantee of a Provider's
+ /// support for or lack of support for an extension. The return value is
+ /// determined by how the authenticating user filled out his/her XRDS document only.
+ /// The only way to be sure of support for a given extension is to include
+ /// the extension in the request and see if a response comes back for that extension.
+ /// </remarks>
+ [SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter", Justification = "No parameter at all.")]
+ public bool IsExtensionSupported<T>() where T : IOpenIdMessageExtension, new() {
+ T extension = new T();
+ return this.IsExtensionSupported(extension);
+ }
+
+ /// <summary>
+ /// Checks whether the OpenId Identifier claims support for a given extension.
+ /// </summary>
+ /// <param name="extensionType">The extension whose support is being queried.</param>
+ /// <returns>
+ /// True if support for the extension is advertised. False otherwise.
+ /// </returns>
+ /// <remarks>
+ /// Note that a true or false return value is no guarantee of a Provider's
+ /// support for or lack of support for an extension. The return value is
+ /// determined by how the authenticating user filled out his/her XRDS document only.
+ /// The only way to be sure of support for a given extension is to include
+ /// the extension in the request and see if a response comes back for that extension.
+ /// </remarks>
+ public bool IsExtensionSupported(Type extensionType) {
+ var extension = (IOpenIdMessageExtension)Activator.CreateInstance(extensionType);
+ return this.IsExtensionSupported(extension);
+ }
+
+ /// <summary>
+ /// Determines whether a given extension is supported by this endpoint.
+ /// </summary>
+ /// <param name="extension">An instance of the extension to check support for.</param>
+ /// <returns>
+ /// <c>true</c> if the extension is supported by this endpoint; otherwise, <c>false</c>.
+ /// </returns>
+ public bool IsExtensionSupported(IOpenIdMessageExtension extension) {
+ Contract.Requires<ArgumentNullException>(extension != null);
+
+ // Consider the primary case.
+ if (this.IsTypeUriPresent(extension.TypeUri)) {
+ return true;
+ }
+
+ // Consider the secondary cases.
+ if (extension.AdditionalSupportedTypeUris != null) {
+ if (extension.AdditionalSupportedTypeUris.Any(typeUri => this.IsTypeUriPresent(typeUri))) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /// <summary>
+ /// Creates a <see cref="IdentifierDiscoveryResult"/> instance to represent some OP Identifier.
+ /// </summary>
+ /// <param name="providerIdentifier">The provider identifier (actually the user-supplied identifier).</param>
+ /// <param name="providerEndpoint">The provider endpoint.</param>
+ /// <param name="servicePriority">The service priority.</param>
+ /// <param name="uriPriority">The URI priority.</param>
+ /// <returns>The created <see cref="IdentifierDiscoveryResult"/> instance</returns>
+ internal static IdentifierDiscoveryResult CreateForProviderIdentifier(Identifier providerIdentifier, ProviderEndpointDescription providerEndpoint, int? servicePriority, int? uriPriority) {
+ Contract.Requires<ArgumentNullException>(providerEndpoint != null);
+
+ Protocol protocol = Protocol.Lookup(providerEndpoint.Version);
+
+ return new IdentifierDiscoveryResult(
+ providerEndpoint,
+ protocol.ClaimedIdentifierForOPIdentifier,
+ providerIdentifier,
+ protocol.ClaimedIdentifierForOPIdentifier,
+ servicePriority,
+ uriPriority);
+ }
+
+ /// <summary>
+ /// Creates a <see cref="IdentifierDiscoveryResult"/> instance to represent some Claimed Identifier.
+ /// </summary>
+ /// <param name="claimedIdentifier">The claimed identifier.</param>
+ /// <param name="providerLocalIdentifier">The provider local identifier.</param>
+ /// <param name="providerEndpoint">The provider endpoint.</param>
+ /// <param name="servicePriority">The service priority.</param>
+ /// <param name="uriPriority">The URI priority.</param>
+ /// <returns>The created <see cref="IdentifierDiscoveryResult"/> instance</returns>
+ internal static IdentifierDiscoveryResult CreateForClaimedIdentifier(Identifier claimedIdentifier, Identifier providerLocalIdentifier, ProviderEndpointDescription providerEndpoint, int? servicePriority, int? uriPriority) {
+ return CreateForClaimedIdentifier(claimedIdentifier, null, providerLocalIdentifier, providerEndpoint, servicePriority, uriPriority);
+ }
+
+ /// <summary>
+ /// Creates a <see cref="IdentifierDiscoveryResult"/> instance to represent some Claimed Identifier.
+ /// </summary>
+ /// <param name="claimedIdentifier">The claimed identifier.</param>
+ /// <param name="userSuppliedIdentifier">The user supplied identifier.</param>
+ /// <param name="providerLocalIdentifier">The provider local identifier.</param>
+ /// <param name="providerEndpoint">The provider endpoint.</param>
+ /// <param name="servicePriority">The service priority.</param>
+ /// <param name="uriPriority">The URI priority.</param>
+ /// <returns>The created <see cref="IdentifierDiscoveryResult"/> instance</returns>
+ internal static IdentifierDiscoveryResult CreateForClaimedIdentifier(Identifier claimedIdentifier, Identifier userSuppliedIdentifier, Identifier providerLocalIdentifier, ProviderEndpointDescription providerEndpoint, int? servicePriority, int? uriPriority) {
+ return new IdentifierDiscoveryResult(providerEndpoint, claimedIdentifier, userSuppliedIdentifier, providerLocalIdentifier, servicePriority, uriPriority);
+ }
+
+ /// <summary>
+ /// Determines whether a given type URI is present on the specified provider endpoint.
+ /// </summary>
+ /// <param name="typeUri">The type URI.</param>
+ /// <returns>
+ /// <c>true</c> if the type URI is present on the specified provider endpoint; otherwise, <c>false</c>.
+ /// </returns>
+ internal bool IsTypeUriPresent(string typeUri) {
+ Contract.Requires<ArgumentException>(!String.IsNullOrEmpty(typeUri));
+ return this.Capabilities.Contains(typeUri);
+ }
+
+ /// <summary>
+ /// Sets the Capabilities property (this method is a test hook.)
+ /// </summary>
+ /// <param name="value">The value.</param>
+ /// <remarks>The publicize.exe tool should work for the unit tests, but for some reason it fails on the build server.</remarks>
+ internal void SetCapabilitiesForTestHook(ReadOnlyCollection<string> value) {
+ this.Capabilities = value;
+ }
+
+ /// <summary>
+ /// Gets the priority rating for a given type of endpoint, allowing a
+ /// priority sorting of endpoints.
+ /// </summary>
+ /// <param name="endpoint">The endpoint to prioritize.</param>
+ /// <returns>An arbitary integer, which may be used for sorting against other returned values from this method.</returns>
+ private static double GetEndpointPrecedenceOrderByServiceType(IdentifierDiscoveryResult endpoint) {
+ // The numbers returned from this method only need to compare against other numbers
+ // from this method, which makes them arbitrary but relational to only others here.
+ if (endpoint.Capabilities.Contains(Protocol.V20.OPIdentifierServiceTypeURI)) {
+ return 0;
+ }
+ if (endpoint.Capabilities.Contains(Protocol.V20.ClaimedIdentifierServiceTypeURI)) {
+ return 1;
+ }
+ if (endpoint.Capabilities.Contains(Protocol.V11.ClaimedIdentifierServiceTypeURI)) {
+ return 2;
+ }
+ if (endpoint.Capabilities.Contains(Protocol.V10.ClaimedIdentifierServiceTypeURI)) {
+ return 3;
+ }
+ return 10;
+ }
+
+#if CONTRACTS_FULL
+ /// <summary>
+ /// Verifies conditions that should be true for any valid state of this object.
+ /// </summary>
+ [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "Called by code contracts.")]
+ [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "Called by code contracts.")]
+ [ContractInvariantMethod]
+ private void ObjectInvariant() {
+ Contract.Invariant(this.ProviderEndpoint != null);
+ Contract.Invariant(this.ClaimedIdentifier != null);
+ Contract.Invariant(this.ProviderLocalIdentifier != null);
+ Contract.Invariant(this.Capabilities != null);
+ Contract.Invariant(this.Version != null);
+ Contract.Invariant(this.Protocol != null);
+ }
+#endif
+ }
+}
diff --git a/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/Interop/AuthenticationResponseShim.cs b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/Interop/AuthenticationResponseShim.cs
new file mode 100644
index 0000000..c0354ac
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/Interop/AuthenticationResponseShim.cs
@@ -0,0 +1,120 @@
+//-----------------------------------------------------------------------
+// <copyright file="AuthenticationResponseShim.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OpenId.Interop {
+ using System;
+ using System.Diagnostics.CodeAnalysis;
+ using System.Diagnostics.Contracts;
+ using System.Runtime.InteropServices;
+ using System.Web;
+ using DotNetOpenAuth.Messaging;
+ using DotNetOpenAuth.OpenId.Extensions.SimpleRegistration;
+ using DotNetOpenAuth.OpenId.RelyingParty;
+
+ /// <summary>
+ /// The COM type used to provide details of an authentication result to a relying party COM client.
+ /// </summary>
+ [SuppressMessage("Microsoft.Interoperability", "CA1409:ComVisibleTypesShouldBeCreatable", Justification = "It's only creatable on the inside. It must be ComVisible for ASP to see it.")]
+ [ComVisible(true), Obsolete("This class acts as a COM Server and should not be called directly from .NET code.")]
+ public sealed class AuthenticationResponseShim {
+ /// <summary>
+ /// The response read in by the Relying Party.
+ /// </summary>
+ private readonly IAuthenticationResponse response;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="AuthenticationResponseShim"/> class.
+ /// </summary>
+ /// <param name="response">The response.</param>
+ internal AuthenticationResponseShim(IAuthenticationResponse response) {
+ Contract.Requires<ArgumentNullException>(response != null);
+
+ this.response = response;
+ var claimsResponse = this.response.GetExtension<ClaimsResponse>();
+ if (claimsResponse != null) {
+ this.ClaimsResponse = new ClaimsResponseShim(claimsResponse);
+ }
+ }
+
+ /// <summary>
+ /// Gets an Identifier that the end user claims to own. For use with user database storage and lookup.
+ /// May be null for some failed authentications (i.e. failed directed identity authentications).
+ /// </summary>
+ /// <remarks>
+ /// <para>
+ /// This is the secure identifier that should be used for database storage and lookup.
+ /// It is not always friendly (i.e. =Arnott becomes =!9B72.7DD1.50A9.5CCD), but it protects
+ /// user identities against spoofing and other attacks.
+ /// </para>
+ /// <para>
+ /// For user-friendly identifiers to display, use the
+ /// <see cref="FriendlyIdentifierForDisplay"/> property.
+ /// </para>
+ /// </remarks>
+ public string ClaimedIdentifier {
+ get { return this.response.ClaimedIdentifier; }
+ }
+
+ /// <summary>
+ /// Gets a user-friendly OpenID Identifier for display purposes ONLY.
+ /// </summary>
+ /// <remarks>
+ /// <para>
+ /// This <i>should</i> be put through <see cref="HttpUtility.HtmlEncode(string)"/> before
+ /// sending to a browser to secure against javascript injection attacks.
+ /// </para>
+ /// <para>
+ /// This property retains some aspects of the user-supplied identifier that get lost
+ /// in the <see cref="ClaimedIdentifier"/>. For example, XRIs used as user-supplied
+ /// identifiers (i.e. =Arnott) become unfriendly unique strings (i.e. =!9B72.7DD1.50A9.5CCD).
+ /// For display purposes, such as text on a web page that says "You're logged in as ...",
+ /// this property serves to provide the =Arnott string, or whatever else is the most friendly
+ /// string close to what the user originally typed in.
+ /// </para>
+ /// <para>
+ /// If the user-supplied identifier is a URI, this property will be the URI after all
+ /// redirects, and with the protocol and fragment trimmed off.
+ /// If the user-supplied identifier is an XRI, this property will be the original XRI.
+ /// If the user-supplied identifier is an OpenID Provider identifier (i.e. yahoo.com),
+ /// this property will be the Claimed Identifier, with the protocol stripped if it is a URI.
+ /// </para>
+ /// <para>
+ /// It is <b>very</b> important that this property <i>never</i> be used for database storage
+ /// or lookup to avoid identity spoofing and other security risks. For database storage
+ /// and lookup please use the <see cref="ClaimedIdentifier"/> property.
+ /// </para>
+ /// </remarks>
+ public string FriendlyIdentifierForDisplay {
+ get { return this.response.FriendlyIdentifierForDisplay; }
+ }
+
+ /// <summary>
+ /// Gets the provider endpoint that sent the assertion.
+ /// </summary>
+ public string ProviderEndpoint {
+ get { return this.response.Provider != null ? this.response.Provider.Uri.AbsoluteUri : null; }
+ }
+
+ /// <summary>
+ /// Gets a value indicating whether the authentication attempt succeeded.
+ /// </summary>
+ public bool Successful {
+ get { return this.response.Status == AuthenticationStatus.Authenticated; }
+ }
+
+ /// <summary>
+ /// Gets the Simple Registration response.
+ /// </summary>
+ public ClaimsResponseShim ClaimsResponse { get; private set; }
+
+ /// <summary>
+ /// Gets details regarding a failed authentication attempt, if available.
+ /// </summary>
+ public string ExceptionMessage {
+ get { return this.response.Exception != null ? this.response.Exception.Message : null; }
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/Interop/ClaimsResponseShim.cs b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/Interop/ClaimsResponseShim.cs
new file mode 100644
index 0000000..689777b
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/Interop/ClaimsResponseShim.cs
@@ -0,0 +1,108 @@
+//-----------------------------------------------------------------------
+// <copyright file="ClaimsResponseShim.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OpenId.Interop {
+ using System;
+ using System.Diagnostics.CodeAnalysis;
+ using System.Diagnostics.Contracts;
+ using System.Runtime.InteropServices;
+ using DotNetOpenAuth.Messaging;
+ using DotNetOpenAuth.OpenId.Extensions.SimpleRegistration;
+
+ /// <summary>
+ /// A struct storing Simple Registration field values describing an
+ /// authenticating user.
+ /// </summary>
+ [SuppressMessage("Microsoft.Interoperability", "CA1409:ComVisibleTypesShouldBeCreatable", Justification = "It's only creatable on the inside. It must be ComVisible for ASP to see it.")]
+ [ComVisible(true), Obsolete("This class acts as a COM Server and should not be called directly from .NET code.")]
+ [ContractVerification(true)]
+ public sealed class ClaimsResponseShim {
+ /// <summary>
+ /// The Simple Registration claims response message that this shim wraps.
+ /// </summary>
+ private readonly ClaimsResponse response;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ClaimsResponseShim"/> class.
+ /// </summary>
+ /// <param name="response">The Simple Registration response to wrap.</param>
+ internal ClaimsResponseShim(ClaimsResponse response)
+ {
+ Contract.Requires<ArgumentNullException>(response != null);
+
+ this.response = response;
+ }
+
+ /// <summary>
+ /// Gets the nickname the user goes by.
+ /// </summary>
+ public string Nickname {
+ get { return this.response.Nickname; }
+ }
+
+ /// <summary>
+ /// Gets the user's email address.
+ /// </summary>
+ public string Email {
+ get { return this.response.Email; }
+ }
+
+ /// <summary>
+ /// Gets the full name of a user as a single string.
+ /// </summary>
+ public string FullName {
+ get { return this.response.FullName; }
+ }
+
+ /// <summary>
+ /// Gets the raw birth date string given by the extension.
+ /// </summary>
+ /// <value>A string in the format yyyy-MM-dd.</value>
+ public string BirthDate {
+ get { return this.response.BirthDateRaw; }
+ }
+
+ /// <summary>
+ /// Gets the gender of the user.
+ /// </summary>
+ public string Gender {
+ get {
+ if (this.response.Gender.HasValue) {
+ return this.response.Gender.Value == Extensions.SimpleRegistration.Gender.Male ? Constants.Genders.Male : Constants.Genders.Female;
+ }
+ return null;
+ }
+ }
+
+ /// <summary>
+ /// Gets the zip code / postal code of the user.
+ /// </summary>
+ public string PostalCode {
+ get { return this.response.PostalCode; }
+ }
+
+ /// <summary>
+ /// Gets the country of the user.
+ /// </summary>
+ public string Country {
+ get { return this.response.Country; }
+ }
+
+ /// <summary>
+ /// Gets the primary/preferred language of the user.
+ /// </summary>
+ public string Language {
+ get { return this.response.Language; }
+ }
+
+ /// <summary>
+ /// Gets the user's timezone.
+ /// </summary>
+ public string TimeZone {
+ get { return this.response.TimeZone; }
+ }
+ }
+} \ No newline at end of file
diff --git a/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/Interop/OpenIdRelyingPartyShim.cs b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/Interop/OpenIdRelyingPartyShim.cs
new file mode 100644
index 0000000..fc0f32e
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/Interop/OpenIdRelyingPartyShim.cs
@@ -0,0 +1,190 @@
+//-----------------------------------------------------------------------
+// <copyright file="OpenIdRelyingPartyShim.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OpenId.Interop {
+ using System;
+ using System.Diagnostics.CodeAnalysis;
+ using System.IO;
+ using System.Runtime.InteropServices;
+ using System.Text;
+ using System.Web;
+ using DotNetOpenAuth.Messaging;
+ using DotNetOpenAuth.OpenId.Extensions.SimpleRegistration;
+ using DotNetOpenAuth.OpenId.RelyingParty;
+
+ /// <summary>
+ /// The COM interface describing the DotNetOpenAuth functionality available to
+ /// COM client OpenID relying parties.
+ /// </summary>
+ [Guid("56BD3DB0-EE0D-4191-ADFC-1F3705CD2636")]
+ [InterfaceType(ComInterfaceType.InterfaceIsDual)]
+ public interface IOpenIdRelyingParty {
+ /// <summary>
+ /// Creates an authentication request to verify that a user controls
+ /// some given Identifier.
+ /// </summary>
+ /// <param name="userSuppliedIdentifier">
+ /// The Identifier supplied by the user. This may be a URL, an XRI or i-name.
+ /// </param>
+ /// <param name="realm">
+ /// The shorest URL that describes this relying party web site's address.
+ /// For example, if your login page is found at https://www.example.com/login.aspx,
+ /// your realm would typically be https://www.example.com/.
+ /// </param>
+ /// <param name="returnToUrl">
+ /// The URL of the login page, or the page prepared to receive authentication
+ /// responses from the OpenID Provider.
+ /// </param>
+ /// <returns>
+ /// An authentication request object that describes the HTTP response to
+ /// send to the user agent to initiate the authentication.
+ /// </returns>
+ /// <exception cref="ProtocolException">Thrown if no OpenID endpoint could be found.</exception>
+ string CreateRequest(string userSuppliedIdentifier, string realm, string returnToUrl);
+
+ /// <summary>
+ /// Creates an authentication request to verify that a user controls
+ /// some given Identifier.
+ /// </summary>
+ /// <param name="userSuppliedIdentifier">The Identifier supplied by the user. This may be a URL, an XRI or i-name.</param>
+ /// <param name="realm">The shorest URL that describes this relying party web site's address.
+ /// For example, if your login page is found at https://www.example.com/login.aspx,
+ /// your realm would typically be https://www.example.com/.</param>
+ /// <param name="returnToUrl">The URL of the login page, or the page prepared to receive authentication
+ /// responses from the OpenID Provider.</param>
+ /// <param name="optionalSreg">A comma-delimited list of simple registration fields to request as optional.</param>
+ /// <param name="requiredSreg">A comma-delimited list of simple registration fields to request as required.</param>
+ /// <returns>
+ /// An authentication request object that describes the HTTP response to
+ /// send to the user agent to initiate the authentication.
+ /// </returns>
+ /// <exception cref="ProtocolException">Thrown if no OpenID endpoint could be found.</exception>
+ [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Sreg", Justification = "Accepted acronym")]
+ string CreateRequestWithSimpleRegistration(string userSuppliedIdentifier, string realm, string returnToUrl, string optionalSreg, string requiredSreg);
+
+ /// <summary>
+ /// Gets the result of a user agent's visit to his OpenId provider in an
+ /// authentication attempt. Null if no response is available.
+ /// </summary>
+ /// <param name="url">The incoming request URL .</param>
+ /// <param name="form">The form data that may have been included in the case of a POST request.</param>
+ /// <returns>The Provider's response to a previous authentication request, or null if no response is present.</returns>
+#pragma warning disable 0618 // we're using the COM type properly
+ AuthenticationResponseShim ProcessAuthentication(string url, string form);
+#pragma warning restore 0618
+ }
+
+ /// <summary>
+ /// Implementation of <see cref="IOpenIdRelyingParty"/>, providing a subset of the
+ /// functionality available to .NET clients.
+ /// </summary>
+ [Guid("8F97A798-B4C5-4da5-9727-EE7DD96A8CD9")]
+ [ProgId("DotNetOpenAuth.OpenId.RelyingParty.OpenIdRelyingParty")]
+ [ComVisible(true), Obsolete("This class acts as a COM Server and should not be called directly from .NET code.", true)]
+ [ClassInterface(ClassInterfaceType.None)]
+ public sealed class OpenIdRelyingPartyShim : IOpenIdRelyingParty {
+ /// <summary>
+ /// The OpenIdRelyingParty instance to use for requests.
+ /// </summary>
+ private static OpenIdRelyingParty relyingParty;
+
+ /// <summary>
+ /// Initializes static members of the <see cref="OpenIdRelyingPartyShim"/> class.
+ /// </summary>
+ static OpenIdRelyingPartyShim() {
+ relyingParty = new OpenIdRelyingParty(null);
+ relyingParty.Behaviors.Add(new Behaviors.AXFetchAsSregTransform());
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="OpenIdRelyingPartyShim"/> class.
+ /// </summary>
+ public OpenIdRelyingPartyShim() {
+ Reporting.RecordFeatureUse(this);
+ }
+
+ /// <summary>
+ /// Creates an authentication request to verify that a user controls
+ /// some given Identifier.
+ /// </summary>
+ /// <param name="userSuppliedIdentifier">
+ /// The Identifier supplied by the user. This may be a URL, an XRI or i-name.
+ /// </param>
+ /// <param name="realm">
+ /// The shorest URL that describes this relying party web site's address.
+ /// For example, if your login page is found at https://www.example.com/login.aspx,
+ /// your realm would typically be https://www.example.com/.
+ /// </param>
+ /// <param name="returnToUrl">
+ /// The URL of the login page, or the page prepared to receive authentication
+ /// responses from the OpenID Provider.
+ /// </param>
+ /// <returns>
+ /// An authentication request object that describes the HTTP response to
+ /// send to the user agent to initiate the authentication.
+ /// </returns>
+ /// <exception cref="ProtocolException">Thrown if no OpenID endpoint could be found.</exception>
+ [SuppressMessage("Microsoft.Usage", "CA2234:PassSystemUriObjectsInsteadOfStrings", Justification = "COM requires primitive types")]
+ public string CreateRequest(string userSuppliedIdentifier, string realm, string returnToUrl) {
+ var request = relyingParty.CreateRequest(userSuppliedIdentifier, realm, new Uri(returnToUrl));
+ return request.RedirectingResponse.GetDirectUriRequest(relyingParty.Channel).AbsoluteUri;
+ }
+
+ /// <summary>
+ /// Creates an authentication request to verify that a user controls
+ /// some given Identifier.
+ /// </summary>
+ /// <param name="userSuppliedIdentifier">The Identifier supplied by the user. This may be a URL, an XRI or i-name.</param>
+ /// <param name="realm">The shorest URL that describes this relying party web site's address.
+ /// For example, if your login page is found at https://www.example.com/login.aspx,
+ /// your realm would typically be https://www.example.com/.</param>
+ /// <param name="returnToUrl">The URL of the login page, or the page prepared to receive authentication
+ /// responses from the OpenID Provider.</param>
+ /// <param name="optionalSreg">A comma-delimited list of simple registration fields to request as optional.</param>
+ /// <param name="requiredSreg">A comma-delimited list of simple registration fields to request as required.</param>
+ /// <returns>
+ /// An authentication request object that describes the HTTP response to
+ /// send to the user agent to initiate the authentication.
+ /// </returns>
+ /// <exception cref="ProtocolException">Thrown if no OpenID endpoint could be found.</exception>
+ [SuppressMessage("Microsoft.Usage", "CA2234:PassSystemUriObjectsInsteadOfStrings", Justification = "COM requires primitive types")]
+ public string CreateRequestWithSimpleRegistration(string userSuppliedIdentifier, string realm, string returnToUrl, string optionalSreg, string requiredSreg) {
+ var request = relyingParty.CreateRequest(userSuppliedIdentifier, realm, new Uri(returnToUrl));
+
+ ClaimsRequest sreg = new ClaimsRequest();
+ if (!string.IsNullOrEmpty(optionalSreg)) {
+ sreg.SetProfileRequestFromList(optionalSreg.Split(','), DemandLevel.Request);
+ }
+ if (!string.IsNullOrEmpty(requiredSreg)) {
+ sreg.SetProfileRequestFromList(requiredSreg.Split(','), DemandLevel.Require);
+ }
+ request.AddExtension(sreg);
+ return request.RedirectingResponse.GetDirectUriRequest(relyingParty.Channel).AbsoluteUri;
+ }
+
+ /// <summary>
+ /// Gets the result of a user agent's visit to his OpenId provider in an
+ /// authentication attempt. Null if no response is available.
+ /// </summary>
+ /// <param name="url">The incoming request URL.</param>
+ /// <param name="form">The form data that may have been included in the case of a POST request.</param>
+ /// <returns>The Provider's response to a previous authentication request, or null if no response is present.</returns>
+ public AuthenticationResponseShim ProcessAuthentication(string url, string form) {
+ HttpRequestInfo requestInfo = new HttpRequestInfo { UrlBeforeRewriting = new Uri(url) };
+ if (!string.IsNullOrEmpty(form)) {
+ requestInfo.HttpMethod = "POST";
+ requestInfo.InputStream = new MemoryStream(Encoding.Unicode.GetBytes(form));
+ }
+
+ var response = relyingParty.GetResponse(requestInfo);
+ if (response != null) {
+ return new AuthenticationResponseShim(response);
+ }
+
+ return null;
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/Messages/AssociateDiffieHellmanResponse.cs b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/Messages/AssociateDiffieHellmanResponse.cs
new file mode 100644
index 0000000..de3dad8
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/Messages/AssociateDiffieHellmanResponse.cs
@@ -0,0 +1,51 @@
+//-----------------------------------------------------------------------
+// <copyright file="AssociateDiffieHellmanRelyingPartyResponse.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OpenId.Messages {
+ using System;
+ using System.Diagnostics.Contracts;
+ using System.Security.Cryptography;
+ using DotNetOpenAuth.Messaging;
+ using DotNetOpenAuth.Messaging.Reflection;
+ using DotNetOpenAuth.OpenId.Provider;
+ using Org.Mentalis.Security.Cryptography;
+
+ /// <summary>
+ /// The successful Diffie-Hellman association response message.
+ /// </summary>
+ /// <remarks>
+ /// Association response messages are described in OpenID 2.0 section 8.2. This type covers section 8.2.3.
+ /// </remarks>
+ internal class AssociateDiffieHellmanRelyingPartyResponse : AssociateDiffieHellmanResponse {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="AssociateDiffieHellmanRelyingPartyResponse"/> class.
+ /// </summary>
+ /// <param name="responseVersion">The OpenID version of the response message.</param>
+ /// <param name="originatingRequest">The originating request.</param>
+ internal AssociateDiffieHellmanRelyingPartyResponse(Version responseVersion, AssociateDiffieHellmanRequest originatingRequest)
+ : base(responseVersion, originatingRequest) {
+ }
+
+ /// <summary>
+ /// Creates the association at relying party side after the association response has been received.
+ /// </summary>
+ /// <param name="request">The original association request that was already sent and responded to.</param>
+ /// <returns>The newly created association.</returns>
+ /// <remarks>
+ /// The resulting association is <i>not</i> added to the association store and must be done by the caller.
+ /// </remarks>
+ protected override Association CreateAssociationAtRelyingParty(AssociateRequest request) {
+ var diffieHellmanRequest = request as AssociateDiffieHellmanRequest;
+ ErrorUtilities.VerifyArgument(diffieHellmanRequest != null, OpenIdStrings.DiffieHellmanAssociationRequired);
+
+ HashAlgorithm hasher = DiffieHellmanUtilities.Lookup(Protocol, this.SessionType);
+ byte[] associationSecret = DiffieHellmanUtilities.SHAHashXorSecret(hasher, diffieHellmanRequest.Algorithm, this.DiffieHellmanServerPublic, this.EncodedMacKey);
+
+ Association association = HmacShaAssociation.Create(Protocol, this.AssociationType, this.AssociationHandle, associationSecret, TimeSpan.FromSeconds(this.ExpiresIn));
+ return association;
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/Messages/AssociateRequestRelyingParty.cs b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/Messages/AssociateRequestRelyingParty.cs
new file mode 100644
index 0000000..19d3a94
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/Messages/AssociateRequestRelyingParty.cs
@@ -0,0 +1,70 @@
+namespace DotNetOpenAuth.OpenId.Messages {
+ using System;
+ using System.Collections.Generic;
+ using System.Linq;
+ using System.Text;
+ using System.Diagnostics.Contracts;
+ using DotNetOpenAuth.OpenId.RelyingParty;
+
+ internal abstract class AssociateRequestRelyingParty : AssociateRequest {
+ /// <summary>
+ /// Creates an association request message that is appropriate for a given Provider.
+ /// </summary>
+ /// <param name="securityRequirements">The set of requirements the selected association type must comply to.</param>
+ /// <param name="provider">The provider to create an association with.</param>
+ /// <returns>
+ /// The message to send to the Provider to request an association.
+ /// Null if no association could be created that meet the security requirements
+ /// and the provider OpenID version.
+ /// </returns>
+ internal static AssociateRequest Create(SecuritySettings securityRequirements, IProviderEndpoint provider) {
+ Contract.Requires<ArgumentNullException>(securityRequirements != null);
+ Contract.Requires<ArgumentNullException>(provider != null);
+
+ // Apply our knowledge of the endpoint's transport, OpenID version, and
+ // security requirements to decide the best association.
+ bool unencryptedAllowed = provider.Uri.IsTransportSecure();
+ bool useDiffieHellman = !unencryptedAllowed;
+ string associationType, sessionType;
+ if (!HmacShaAssociation.TryFindBestAssociation(Protocol.Lookup(provider.Version), true, securityRequirements, useDiffieHellman, out associationType, out sessionType)) {
+ // There are no associations that meet all requirements.
+ Logger.OpenId.Warn("Security requirements and protocol combination knock out all possible association types. Dumb mode forced.");
+ return null;
+ }
+
+ return Create(securityRequirements, provider, associationType, sessionType);
+ }
+
+ /// <summary>
+ /// Creates an association request message that is appropriate for a given Provider.
+ /// </summary>
+ /// <param name="securityRequirements">The set of requirements the selected association type must comply to.</param>
+ /// <param name="provider">The provider to create an association with.</param>
+ /// <param name="associationType">Type of the association.</param>
+ /// <param name="sessionType">Type of the session.</param>
+ /// <returns>
+ /// The message to send to the Provider to request an association.
+ /// Null if no association could be created that meet the security requirements
+ /// and the provider OpenID version.
+ /// </returns>
+ internal static AssociateRequest Create(SecuritySettings securityRequirements, IProviderEndpoint provider, string associationType, string sessionType) {
+ Contract.Requires<ArgumentNullException>(securityRequirements != null);
+ Contract.Requires<ArgumentNullException>(provider != null);
+ Contract.Requires<ArgumentException>(!String.IsNullOrEmpty(associationType));
+ Contract.Requires<ArgumentNullException>(sessionType != null);
+
+ bool unencryptedAllowed = provider.Uri.IsTransportSecure();
+ if (unencryptedAllowed) {
+ var associateRequest = new AssociateUnencryptedRequest(provider.Version, provider.Uri);
+ associateRequest.AssociationType = associationType;
+ return associateRequest;
+ } else {
+ var associateRequest = new AssociateDiffieHellmanRequest(provider.Version, provider.Uri);
+ associateRequest.AssociationType = associationType;
+ associateRequest.SessionType = sessionType;
+ associateRequest.InitializeRequest();
+ return associateRequest;
+ }
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/Messages/AssociateSuccessfulResponseContract.cs b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/Messages/AssociateSuccessfulResponseContract.cs
new file mode 100644
index 0000000..de28a64
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/Messages/AssociateSuccessfulResponseContract.cs
@@ -0,0 +1,17 @@
+namespace DotNetOpenAuth {
+ using System;
+ using System.Collections.Generic;
+ using System.Linq;
+ using System.Text;
+ using DotNetOpenAuth.OpenId.Messages;
+ using System.Diagnostics.Contracts;
+ using DotNetOpenAuth.OpenId;
+
+ [ContractClassFor(typeof(AssociateSuccessfulResponseRelyingParty))]
+ internal abstract class AssociateSuccessfulResponseRelyingPartyContract : AssociateSuccessfulResponseRelyingParty {
+ protected override Association CreateAssociationAtRelyingParty(AssociateRequest request) {
+ Contract.Requires<ArgumentNullException>(request != null);
+ throw new NotImplementedException();
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/Messages/AssociateSuccessfulResponseRelyingParty.cs b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/Messages/AssociateSuccessfulResponseRelyingParty.cs
new file mode 100644
index 0000000..7ee3988
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/Messages/AssociateSuccessfulResponseRelyingParty.cs
@@ -0,0 +1,15 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace DotNetOpenAuth.OpenId.Messages {
+ internal abstract class AssociateSuccessfulResponseRelyingParty : AssociateSuccessfulResponse {
+ /// <summary>
+ /// Called to create the Association based on a request previously given by the Relying Party.
+ /// </summary>
+ /// <param name="request">The prior request for an association.</param>
+ /// <returns>The created association.</returns>
+ protected abstract Association CreateAssociationAtRelyingParty(AssociateRequest request);
+ }
+}
diff --git a/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/Messages/AssociateUnencryptedResponseRelyingParty.cs b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/Messages/AssociateUnencryptedResponseRelyingParty.cs
new file mode 100644
index 0000000..23cbd9b
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/Messages/AssociateUnencryptedResponseRelyingParty.cs
@@ -0,0 +1,20 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace DotNetOpenAuth.OpenId.Messages {
+ internal class AssociateUnencryptedResponseRelyingParty : AssociateUnencryptedResponse {
+
+ /// <summary>
+ /// Called to create the Association based on a request previously given by the Relying Party.
+ /// </summary>
+ /// <param name="request">The prior request for an association.</param>
+ /// <returns>The created association.</returns>
+ protected override Association CreateAssociationAtRelyingParty(AssociateRequest request) {
+ Association association = HmacShaAssociation.Create(Protocol, this.AssociationType, this.AssociationHandle, this.MacKey, TimeSpan.FromSeconds(this.ExpiresIn));
+ return association;
+ }
+
+ }
+}
diff --git a/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/Mvc/OpenIdAjaxOptions.cs b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/Mvc/OpenIdAjaxOptions.cs
new file mode 100644
index 0000000..4b88d04
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/Mvc/OpenIdAjaxOptions.cs
@@ -0,0 +1,76 @@
+//-----------------------------------------------------------------------
+// <copyright file="OpenIdAjaxOptions.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.Mvc {
+ using System;
+ using System.Collections.Generic;
+ using System.Globalization;
+ using System.Linq;
+ using System.Text;
+ using DotNetOpenAuth.Messaging;
+
+ /// <summary>
+ /// A set of customizations available for the scripts sent to the browser in AJAX OpenID scenarios.
+ /// </summary>
+ public class OpenIdAjaxOptions {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="OpenIdAjaxOptions"/> class.
+ /// </summary>
+ public OpenIdAjaxOptions() {
+ this.AssertionHiddenFieldId = "openid_openidAuthData";
+ this.ReturnUrlHiddenFieldId = "ReturnUrl";
+ }
+
+ /// <summary>
+ /// Gets or sets the ID of the hidden field that should carry the positive assertion
+ /// until it is posted to the RP.
+ /// </summary>
+ public string AssertionHiddenFieldId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the ID of the hidden field that should be set with the parent window/frame's URL
+ /// prior to posting the form with the positive assertion. Useful for jQuery popup dialogs.
+ /// </summary>
+ public string ReturnUrlHiddenFieldId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the index of the form in the document.forms array on the browser that should
+ /// be submitted when the user is ready to send the positive assertion to the RP.
+ /// </summary>
+ public int FormIndex { get; set; }
+
+ /// <summary>
+ /// Gets or sets the id of the form in the document.forms array on the browser that should
+ /// be submitted when the user is ready to send the positive assertion to the RP. A value
+ /// in this property takes precedence over any value in the <see cref="FormIndex"/> property.
+ /// </summary>
+ /// <value>The form id.</value>
+ public string FormId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the preloaded discovery results.
+ /// </summary>
+ public string PreloadedDiscoveryResults { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether to print diagnostic trace messages in the browser.
+ /// </summary>
+ public bool ShowDiagnosticTrace { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether to show all the "hidden" iframes that facilitate
+ /// asynchronous authentication of the user for diagnostic purposes.
+ /// </summary>
+ public bool ShowDiagnosticIFrame { get; set; }
+
+ /// <summary>
+ /// Gets the form key to use when accessing the relevant form.
+ /// </summary>
+ internal string FormKey {
+ get { return string.IsNullOrEmpty(this.FormId) ? this.FormIndex.ToString(CultureInfo.InvariantCulture) : MessagingUtilities.GetSafeJavascriptValue(this.FormId); }
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/Mvc/OpenIdHelper.cs b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/Mvc/OpenIdHelper.cs
new file mode 100644
index 0000000..b98e0d6
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/Mvc/OpenIdHelper.cs
@@ -0,0 +1,431 @@
+//-----------------------------------------------------------------------
+// <copyright file="OpenIdHelper.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.Mvc {
+ using System;
+ using System.Collections.Generic;
+ using System.Diagnostics.CodeAnalysis;
+ using System.Diagnostics.Contracts;
+ using System.Globalization;
+ using System.IO;
+ using System.Linq;
+ using System.Text;
+ using System.Web;
+ using System.Web.Mvc;
+ using System.Web.Routing;
+ using System.Web.UI;
+ using DotNetOpenAuth.Configuration;
+ using DotNetOpenAuth.Messaging;
+ using DotNetOpenAuth.OpenId;
+ using DotNetOpenAuth.OpenId.RelyingParty;
+
+ /// <summary>
+ /// Methods that generate HTML or Javascript for hosting AJAX OpenID "controls" on
+ /// ASP.NET MVC web sites.
+ /// </summary>
+ public static class OpenIdHelper {
+ /// <summary>
+ /// Emits a series of stylesheet import tags to support the AJAX OpenID Selector.
+ /// </summary>
+ /// <param name="html">The <see cref="HtmlHelper"/> on the view.</param>
+ /// <returns>HTML that should be sent directly to the browser.</returns>
+ public static string OpenIdSelectorStyles(this HtmlHelper html) {
+ Contract.Requires<ArgumentNullException>(html != null);
+ Contract.Ensures(Contract.Result<string>() != null);
+
+ using (var result = new StringWriter(CultureInfo.CurrentCulture)) {
+ result.WriteStylesheetLink(OpenId.RelyingParty.OpenIdSelector.EmbeddedStylesheetResourceName);
+ result.WriteStylesheetLink(OpenId.RelyingParty.OpenIdAjaxTextBox.EmbeddedStylesheetResourceName);
+ return result.ToString();
+ }
+ }
+
+ /// <summary>
+ /// Emits a series of script import tags and some inline script to support the AJAX OpenID Selector.
+ /// </summary>
+ /// <param name="html">The <see cref="HtmlHelper"/> on the view.</param>
+ /// <returns>HTML that should be sent directly to the browser.</returns>
+ public static string OpenIdSelectorScripts(this HtmlHelper html) {
+ return OpenIdSelectorScripts(html, null, null);
+ }
+
+ /// <summary>
+ /// Emits a series of script import tags and some inline script to support the AJAX OpenID Selector.
+ /// </summary>
+ /// <param name="html">The <see cref="HtmlHelper"/> on the view.</param>
+ /// <param name="selectorOptions">An optional instance of an <see cref="OpenIdSelector"/> control, whose properties have been customized to express how this MVC control should be rendered.</param>
+ /// <param name="additionalOptions">An optional set of additional script customizations.</param>
+ /// <returns>
+ /// HTML that should be sent directly to the browser.
+ /// </returns>
+ [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "False positive")]
+ public static string OpenIdSelectorScripts(this HtmlHelper html, OpenIdSelector selectorOptions, OpenIdAjaxOptions additionalOptions) {
+ Contract.Requires<ArgumentNullException>(html != null);
+ Contract.Ensures(Contract.Result<string>() != null);
+
+ bool selectorOptionsOwned = false;
+ if (selectorOptions == null) {
+ selectorOptionsOwned = true;
+ selectorOptions = new OpenId.RelyingParty.OpenIdSelector();
+ }
+ try {
+ if (additionalOptions == null) {
+ additionalOptions = new OpenIdAjaxOptions();
+ }
+
+ using (StringWriter result = new StringWriter(CultureInfo.CurrentCulture)) {
+ if (additionalOptions.ShowDiagnosticIFrame || additionalOptions.ShowDiagnosticTrace) {
+ string scriptFormat = @"window.openid_visible_iframe = {0}; // causes the hidden iframe to show up
+window.openid_trace = {1}; // causes lots of messages";
+ result.WriteScriptBlock(string.Format(
+ CultureInfo.InvariantCulture,
+ scriptFormat,
+ additionalOptions.ShowDiagnosticIFrame ? "true" : "false",
+ additionalOptions.ShowDiagnosticTrace ? "true" : "false"));
+ }
+ var scriptResources = new[] {
+ OpenIdRelyingPartyControlBase.EmbeddedJavascriptResource,
+ OpenIdRelyingPartyAjaxControlBase.EmbeddedAjaxJavascriptResource,
+ OpenId.RelyingParty.OpenIdAjaxTextBox.EmbeddedScriptResourceName,
+ };
+ result.WriteScriptTags(scriptResources);
+
+ if (selectorOptions.DownloadYahooUILibrary) {
+ result.WriteScriptTagsUrls(new[] { "https://ajax.googleapis.com/ajax/libs/yui/2.8.0r4/build/yuiloader/yuiloader-min.js" });
+ }
+
+ using (var blockBuilder = new StringWriter(CultureInfo.CurrentCulture)) {
+ if (selectorOptions.DownloadYahooUILibrary) {
+ blockBuilder.WriteLine(@" try {
+ if (YAHOO) {
+ var loader = new YAHOO.util.YUILoader({
+ require: ['button', 'menu'],
+ loadOptional: false,
+ combine: true
+ });
+
+ loader.insert();
+ }
+ } catch (e) { }");
+ }
+
+ blockBuilder.WriteLine("window.aspnetapppath = '{0}';", VirtualPathUtility.AppendTrailingSlash(HttpContext.Current.Request.ApplicationPath));
+
+ // Positive assertions can last no longer than this library is willing to consider them valid,
+ // and when they come with OP private associations they last no longer than the OP is willing
+ // to consider them valid. We assume the OP will hold them valid for at least five minutes.
+ double assertionLifetimeInMilliseconds = Math.Min(TimeSpan.FromMinutes(5).TotalMilliseconds, Math.Min(OpenIdElement.Configuration.MaxAuthenticationTime.TotalMilliseconds, DotNetOpenAuthSection.Messaging.MaximumMessageLifetime.TotalMilliseconds));
+ blockBuilder.WriteLine(
+ "{0} = {1};",
+ OpenIdRelyingPartyAjaxControlBase.MaxPositiveAssertionLifetimeJsName,
+ assertionLifetimeInMilliseconds.ToString(CultureInfo.InvariantCulture));
+
+ if (additionalOptions.PreloadedDiscoveryResults != null) {
+ blockBuilder.WriteLine(additionalOptions.PreloadedDiscoveryResults);
+ }
+
+ string discoverUrl = VirtualPathUtility.AppendTrailingSlash(HttpContext.Current.Request.ApplicationPath) + html.RouteCollection["OpenIdDiscover"].GetVirtualPath(html.ViewContext.RequestContext, new RouteValueDictionary(new { identifier = "xxx" })).VirtualPath;
+ string blockFormat = @" {0} = function (argument, resultFunction, errorCallback) {{
+ jQuery.ajax({{
+ async: true,
+ dataType: 'text',
+ error: function (request, status, error) {{ errorCallback(status, argument); }},
+ success: function (result) {{ resultFunction(result, argument); }},
+ url: '{1}'.replace('xxx', encodeURIComponent(argument))
+ }});
+ }};";
+ blockBuilder.WriteLine(blockFormat, OpenIdRelyingPartyAjaxControlBase.CallbackJSFunctionAsync, discoverUrl);
+
+ blockFormat = @" window.postLoginAssertion = function (positiveAssertion) {{
+ $('#{0}')[0].setAttribute('value', positiveAssertion);
+ if ($('#{1}')[0] && !$('#{1}')[0].value) {{ // popups have no ReturnUrl predefined, but full page LogOn does.
+ $('#{1}')[0].setAttribute('value', window.parent.location.href);
+ }}
+ document.forms[{2}].submit();
+ }};";
+ blockBuilder.WriteLine(
+ blockFormat,
+ additionalOptions.AssertionHiddenFieldId,
+ additionalOptions.ReturnUrlHiddenFieldId,
+ additionalOptions.FormKey);
+
+ blockFormat = @" $(function () {{
+ var box = document.getElementsByName('openid_identifier')[0];
+ initAjaxOpenId(box, {0}, {1}, {2}, {3}, {4}, {5},
+ null, // js function to invoke on receiving a positive assertion
+ {6}, {7}, {8}, {9}, {10}, {11}, {12}, {13}, {14}, {15}, {16}, {17},
+ false, // auto postback
+ null); // PostBackEventReference (unused in MVC)
+ }});";
+ blockBuilder.WriteLine(
+ blockFormat,
+ MessagingUtilities.GetSafeJavascriptValue(Util.GetWebResourceUrl(typeof(OpenIdRelyingPartyControlBase), OpenIdTextBox.EmbeddedLogoResourceName)),
+ MessagingUtilities.GetSafeJavascriptValue(Util.GetWebResourceUrl(typeof(OpenIdRelyingPartyControlBase), OpenId.RelyingParty.OpenIdAjaxTextBox.EmbeddedSpinnerResourceName)),
+ MessagingUtilities.GetSafeJavascriptValue(Util.GetWebResourceUrl(typeof(OpenIdRelyingPartyControlBase), OpenId.RelyingParty.OpenIdAjaxTextBox.EmbeddedLoginSuccessResourceName)),
+ MessagingUtilities.GetSafeJavascriptValue(Util.GetWebResourceUrl(typeof(OpenIdRelyingPartyControlBase), OpenId.RelyingParty.OpenIdAjaxTextBox.EmbeddedLoginFailureResourceName)),
+ selectorOptions.Throttle,
+ selectorOptions.Timeout.TotalMilliseconds,
+ MessagingUtilities.GetSafeJavascriptValue(selectorOptions.TextBox.LogOnText),
+ MessagingUtilities.GetSafeJavascriptValue(selectorOptions.TextBox.LogOnToolTip),
+ selectorOptions.TextBox.ShowLogOnPostBackButton ? "true" : "false",
+ MessagingUtilities.GetSafeJavascriptValue(selectorOptions.TextBox.LogOnPostBackToolTip),
+ MessagingUtilities.GetSafeJavascriptValue(selectorOptions.TextBox.RetryText),
+ MessagingUtilities.GetSafeJavascriptValue(selectorOptions.TextBox.RetryToolTip),
+ MessagingUtilities.GetSafeJavascriptValue(selectorOptions.TextBox.BusyToolTip),
+ MessagingUtilities.GetSafeJavascriptValue(selectorOptions.TextBox.IdentifierRequiredMessage),
+ MessagingUtilities.GetSafeJavascriptValue(selectorOptions.TextBox.LogOnInProgressMessage),
+ MessagingUtilities.GetSafeJavascriptValue(selectorOptions.TextBox.AuthenticationSucceededToolTip),
+ MessagingUtilities.GetSafeJavascriptValue(selectorOptions.TextBox.AuthenticatedAsToolTip),
+ MessagingUtilities.GetSafeJavascriptValue(selectorOptions.TextBox.AuthenticationFailedToolTip));
+
+ result.WriteScriptBlock(blockBuilder.ToString());
+ result.WriteScriptTags(OpenId.RelyingParty.OpenIdSelector.EmbeddedScriptResourceName);
+
+ Reporting.RecordFeatureUse("MVC " + typeof(OpenIdSelector).Name);
+ return result.ToString();
+ }
+ }
+ } catch {
+ if (selectorOptionsOwned) {
+ selectorOptions.Dispose();
+ }
+
+ throw;
+ }
+ }
+
+ /// <summary>
+ /// Emits the HTML to render an OpenID Provider button as a part of the overall OpenID Selector UI.
+ /// </summary>
+ /// <param name="html">The <see cref="HtmlHelper"/> on the view.</param>
+ /// <param name="providerIdentifier">The OP Identifier.</param>
+ /// <param name="imageUrl">The URL of the image to display on the button.</param>
+ /// <returns>
+ /// HTML that should be sent directly to the browser.
+ /// </returns>
+ public static string OpenIdSelectorOPButton(this HtmlHelper html, Identifier providerIdentifier, string imageUrl) {
+ Contract.Requires<ArgumentNullException>(html != null);
+ Contract.Requires<ArgumentNullException>(providerIdentifier != null);
+ Contract.Requires<ArgumentException>(!string.IsNullOrEmpty(imageUrl));
+ Contract.Ensures(Contract.Result<string>() != null);
+
+ return OpenIdSelectorButton(html, providerIdentifier, "OPButton", imageUrl);
+ }
+
+ /// <summary>
+ /// Emits the HTML to render a generic OpenID button as a part of the overall OpenID Selector UI,
+ /// allowing the user to enter their own OpenID.
+ /// </summary>
+ /// <param name="html">The <see cref="HtmlHelper"/> on the view.</param>
+ /// <param name="imageUrl">The URL of the image to display on the button.</param>
+ /// <returns>
+ /// HTML that should be sent directly to the browser.
+ /// </returns>
+ public static string OpenIdSelectorOpenIdButton(this HtmlHelper html, string imageUrl) {
+ Contract.Requires<ArgumentNullException>(html != null);
+ Contract.Requires<ArgumentException>(!string.IsNullOrEmpty(imageUrl));
+ Contract.Ensures(Contract.Result<string>() != null);
+
+ return OpenIdSelectorButton(html, "OpenIDButton", "OpenIDButton", imageUrl);
+ }
+
+ /// <summary>
+ /// Emits the HTML to render the entire OpenID Selector UI.
+ /// </summary>
+ /// <param name="html">The <see cref="HtmlHelper"/> on the view.</param>
+ /// <param name="buttons">The buttons to include on the selector.</param>
+ /// <returns>
+ /// HTML that should be sent directly to the browser.
+ /// </returns>
+ [SuppressMessage("Microsoft.Usage", "CA2202:Do not dispose objects multiple times", Justification = "Not a problem for this type.")]
+ public static string OpenIdSelector(this HtmlHelper html, params SelectorButton[] buttons) {
+ Contract.Requires<ArgumentNullException>(html != null);
+ Contract.Requires<ArgumentNullException>(buttons != null);
+ Contract.Ensures(Contract.Result<string>() != null);
+
+ using (var writer = new StringWriter(CultureInfo.CurrentCulture)) {
+ using (var h = new HtmlTextWriter(writer)) {
+ h.AddAttribute(HtmlTextWriterAttribute.Class, "OpenIdProviders");
+ h.RenderBeginTag(HtmlTextWriterTag.Ul);
+
+ foreach (SelectorButton button in buttons) {
+ var op = button as SelectorProviderButton;
+ if (op != null) {
+ h.Write(OpenIdSelectorOPButton(html, op.OPIdentifier, op.Image));
+ continue;
+ }
+
+ var openid = button as SelectorOpenIdButton;
+ if (openid != null) {
+ h.Write(OpenIdSelectorOpenIdButton(html, openid.Image));
+ continue;
+ }
+
+ ErrorUtilities.VerifySupported(false, "The {0} button is not yet supported for MVC.", button.GetType().Name);
+ }
+
+ h.RenderEndTag(); // ul
+
+ if (buttons.OfType<SelectorOpenIdButton>().Any()) {
+ h.Write(OpenIdAjaxTextBox(html));
+ }
+ }
+
+ return writer.ToString();
+ }
+ }
+
+ /// <summary>
+ /// Emits the HTML to render the <see cref="OpenIdAjaxTextBox"/> control as a part of the overall
+ /// OpenID Selector UI.
+ /// </summary>
+ /// <param name="html">The <see cref="HtmlHelper"/> on the view.</param>
+ /// <returns>
+ /// HTML that should be sent directly to the browser.
+ /// </returns>
+ [SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", MessageId = "html", Justification = "Breaking change, and it's an extension method so it's useful.")]
+ public static string OpenIdAjaxTextBox(this HtmlHelper html) {
+ return @"<div style='display: none' id='OpenIDForm'>
+ <span class='OpenIdAjaxTextBox' style='display: inline-block; position: relative; font-size: 16px'>
+ <input name='openid_identifier' id='openid_identifier' size='40' style='padding-left: 18px; border-style: solid; border-width: 1px; border-color: lightgray' />
+ </span>
+ </div>";
+ }
+
+ /// <summary>
+ /// Emits the HTML to render a button as a part of the overall OpenID Selector UI.
+ /// </summary>
+ /// <param name="html">The <see cref="HtmlHelper"/> on the view.</param>
+ /// <param name="id">The value to assign to the HTML id attribute.</param>
+ /// <param name="cssClass">The value to assign to the HTML class attribute.</param>
+ /// <param name="imageUrl">The URL of the image to draw on the button.</param>
+ /// <returns>
+ /// HTML that should be sent directly to the browser.
+ /// </returns>
+ [SuppressMessage("Microsoft.Usage", "CA2202:Do not dispose objects multiple times", Justification = "Not a problem for this type.")]
+ private static string OpenIdSelectorButton(this HtmlHelper html, string id, string cssClass, string imageUrl) {
+ Contract.Requires<ArgumentNullException>(html != null);
+ Contract.Requires<ArgumentNullException>(id != null);
+ Contract.Requires<ArgumentException>(!string.IsNullOrEmpty(imageUrl));
+ Contract.Ensures(Contract.Result<string>() != null);
+
+ using (var writer = new StringWriter(CultureInfo.CurrentCulture)) {
+ using (var h = new HtmlTextWriter(writer)) {
+ h.AddAttribute(HtmlTextWriterAttribute.Id, id);
+ if (!string.IsNullOrEmpty(cssClass)) {
+ h.AddAttribute(HtmlTextWriterAttribute.Class, cssClass);
+ }
+ h.RenderBeginTag(HtmlTextWriterTag.Li);
+
+ h.AddAttribute(HtmlTextWriterAttribute.Href, "#");
+ h.RenderBeginTag(HtmlTextWriterTag.A);
+
+ h.RenderBeginTag(HtmlTextWriterTag.Div);
+ h.RenderBeginTag(HtmlTextWriterTag.Div);
+
+ h.AddAttribute(HtmlTextWriterAttribute.Src, imageUrl);
+ h.RenderBeginTag(HtmlTextWriterTag.Img);
+ h.RenderEndTag();
+
+ h.AddAttribute(HtmlTextWriterAttribute.Src, Util.GetWebResourceUrl(typeof(OpenIdSelector), OpenId.RelyingParty.OpenIdAjaxTextBox.EmbeddedLoginSuccessResourceName));
+ h.AddAttribute(HtmlTextWriterAttribute.Class, "loginSuccess");
+ h.AddAttribute(HtmlTextWriterAttribute.Title, "Authenticated as {0}");
+ h.RenderBeginTag(HtmlTextWriterTag.Img);
+ h.RenderEndTag();
+
+ h.RenderEndTag(); // div
+
+ h.AddAttribute(HtmlTextWriterAttribute.Class, "ui-widget-overlay");
+ h.RenderBeginTag(HtmlTextWriterTag.Div);
+ h.RenderEndTag(); // div
+
+ h.RenderEndTag(); // div
+ h.RenderEndTag(); // a
+ h.RenderEndTag(); // li
+ }
+
+ return writer.ToString();
+ }
+ }
+
+ /// <summary>
+ /// Emits &lt;script&gt; tags that import a given set of scripts given their URLs.
+ /// </summary>
+ /// <param name="writer">The writer to emit the tags to.</param>
+ /// <param name="scriptUrls">The locations of the scripts to import.</param>
+ private static void WriteScriptTagsUrls(this TextWriter writer, IEnumerable<string> scriptUrls) {
+ Contract.Requires<ArgumentNullException>(writer != null);
+ Contract.Requires<ArgumentNullException>(scriptUrls != null);
+
+ foreach (string script in scriptUrls) {
+ writer.WriteLine("<script type='text/javascript' src='{0}'></script>", script);
+ }
+ }
+
+ /// <summary>
+ /// Writes out script tags that import a script from resources embedded in this assembly.
+ /// </summary>
+ /// <param name="writer">The writer to emit the tags to.</param>
+ /// <param name="resourceName">Name of the resource.</param>
+ private static void WriteScriptTags(this TextWriter writer, string resourceName) {
+ Contract.Requires<ArgumentNullException>(writer != null);
+ Contract.Requires<ArgumentException>(!string.IsNullOrEmpty(resourceName));
+
+ WriteScriptTags(writer, new[] { resourceName });
+ }
+
+ /// <summary>
+ /// Writes out script tags that import scripts from resources embedded in this assembly.
+ /// </summary>
+ /// <param name="writer">The writer to emit the tags to.</param>
+ /// <param name="resourceNames">The resource names.</param>
+ private static void WriteScriptTags(this TextWriter writer, IEnumerable<string> resourceNames) {
+ Contract.Requires<ArgumentNullException>(writer != null);
+ Contract.Requires<ArgumentNullException>(resourceNames != null);
+
+ writer.WriteScriptTagsUrls(resourceNames.Select(r => Util.GetWebResourceUrl(typeof(OpenIdRelyingPartyControlBase), r)));
+ }
+
+ /// <summary>
+ /// Writes a given script block, surrounding it with &lt;script&gt; and CDATA tags.
+ /// </summary>
+ /// <param name="writer">The writer to emit the tags to.</param>
+ /// <param name="script">The script to inline on the page.</param>
+ private static void WriteScriptBlock(this TextWriter writer, string script) {
+ Contract.Requires<ArgumentNullException>(writer != null);
+ Contract.Requires<ArgumentException>(!string.IsNullOrEmpty(script));
+
+ writer.WriteLine("<script type='text/javascript' language='javascript'><!--");
+ writer.WriteLine("//<![CDATA[");
+ writer.WriteLine(script);
+ writer.WriteLine("//]]>--></script>");
+ }
+
+ /// <summary>
+ /// Writes a given CSS link.
+ /// </summary>
+ /// <param name="writer">The writer to emit the tags to.</param>
+ /// <param name="resourceName">Name of the resource containing the CSS content.</param>
+ private static void WriteStylesheetLink(this TextWriter writer, string resourceName) {
+ Contract.Requires<ArgumentNullException>(writer != null);
+ Contract.Requires<ArgumentException>(!string.IsNullOrEmpty(resourceName));
+
+ WriteStylesheetLinkUrl(writer, Util.GetWebResourceUrl(typeof(OpenIdRelyingPartyAjaxControlBase), resourceName));
+ }
+
+ /// <summary>
+ /// Writes a given CSS link.
+ /// </summary>
+ /// <param name="writer">The writer to emit the tags to.</param>
+ /// <param name="stylesheet">The stylesheet to link in.</param>
+ private static void WriteStylesheetLinkUrl(this TextWriter writer, string stylesheet) {
+ Contract.Requires<ArgumentNullException>(writer != null);
+ Contract.Requires<ArgumentException>(!string.IsNullOrEmpty(stylesheet));
+
+ writer.WriteLine("<link rel='stylesheet' type='text/css' href='{0}' />", stylesheet);
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/OpenIdXrdsHelper.cs b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/OpenIdXrdsHelper.cs
new file mode 100644
index 0000000..6b2fb54
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/OpenIdXrdsHelper.cs
@@ -0,0 +1,163 @@
+//-----------------------------------------------------------------------
+// <copyright file="OpenIdXrdsHelper.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OpenId {
+ using System;
+ using System.Collections.Generic;
+ using System.Diagnostics.Contracts;
+ using System.Linq;
+ using DotNetOpenAuth.Messaging;
+ using DotNetOpenAuth.OpenId.RelyingParty;
+ using DotNetOpenAuth.Xrds;
+
+ /// <summary>
+ /// Adds OpenID-specific extension methods to the XrdsDocument class.
+ /// </summary>
+ internal static class OpenIdXrdsHelperRelyingParty {
+ /// <summary>
+ /// Creates the service endpoints described in this document, useful for requesting
+ /// authentication of one of the OpenID Providers that result from it.
+ /// </summary>
+ /// <param name="xrds">The XrdsDocument instance to use in this process.</param>
+ /// <param name="claimedIdentifier">The claimed identifier that was used to discover this XRDS document.</param>
+ /// <param name="userSuppliedIdentifier">The user supplied identifier.</param>
+ /// <returns>
+ /// A sequence of OpenID Providers that can assert ownership of the <paramref name="claimedIdentifier"/>.
+ /// </returns>
+ internal static IEnumerable<IdentifierDiscoveryResult> CreateServiceEndpoints(this IEnumerable<XrdElement> xrds, UriIdentifier claimedIdentifier, UriIdentifier userSuppliedIdentifier) {
+ Contract.Requires<ArgumentNullException>(xrds != null);
+ Contract.Requires<ArgumentNullException>(claimedIdentifier != null);
+ Contract.Requires<ArgumentNullException>(userSuppliedIdentifier != null);
+ Contract.Ensures(Contract.Result<IEnumerable<IdentifierDiscoveryResult>>() != null);
+
+ var endpoints = new List<IdentifierDiscoveryResult>();
+ endpoints.AddRange(xrds.GenerateOPIdentifierServiceEndpoints(userSuppliedIdentifier));
+ endpoints.AddRange(xrds.GenerateClaimedIdentifierServiceEndpoints(claimedIdentifier, userSuppliedIdentifier));
+
+ Logger.Yadis.DebugFormat("Total services discovered in XRDS: {0}", endpoints.Count);
+ Logger.Yadis.Debug(endpoints.ToStringDeferred(true));
+ return endpoints;
+ }
+
+ /// <summary>
+ /// Creates the service endpoints described in this document, useful for requesting
+ /// authentication of one of the OpenID Providers that result from it.
+ /// </summary>
+ /// <param name="xrds">The XrdsDocument instance to use in this process.</param>
+ /// <param name="userSuppliedIdentifier">The user-supplied i-name that was used to discover this XRDS document.</param>
+ /// <returns>A sequence of OpenID Providers that can assert ownership of the canonical ID given in this document.</returns>
+ internal static IEnumerable<IdentifierDiscoveryResult> CreateServiceEndpoints(this IEnumerable<XrdElement> xrds, XriIdentifier userSuppliedIdentifier) {
+ Contract.Requires<ArgumentNullException>(xrds != null);
+ Contract.Requires<ArgumentNullException>(userSuppliedIdentifier != null);
+ Contract.Ensures(Contract.Result<IEnumerable<IdentifierDiscoveryResult>>() != null);
+
+ var endpoints = new List<IdentifierDiscoveryResult>();
+ endpoints.AddRange(xrds.GenerateOPIdentifierServiceEndpoints(userSuppliedIdentifier));
+ endpoints.AddRange(xrds.GenerateClaimedIdentifierServiceEndpoints(userSuppliedIdentifier));
+ Logger.Yadis.DebugFormat("Total services discovered in XRDS: {0}", endpoints.Count);
+ Logger.Yadis.Debug(endpoints.ToStringDeferred(true));
+ return endpoints;
+ }
+
+ /// <summary>
+ /// Generates OpenID Providers that can authenticate using directed identity.
+ /// </summary>
+ /// <param name="xrds">The XrdsDocument instance to use in this process.</param>
+ /// <param name="opIdentifier">The OP Identifier entered (and resolved) by the user. Essentially the user-supplied identifier.</param>
+ /// <returns>A sequence of the providers that can offer directed identity services.</returns>
+ private static IEnumerable<IdentifierDiscoveryResult> GenerateOPIdentifierServiceEndpoints(this IEnumerable<XrdElement> xrds, Identifier opIdentifier) {
+ Contract.Requires<ArgumentNullException>(xrds != null);
+ Contract.Requires<ArgumentNullException>(opIdentifier != null);
+ Contract.Ensures(Contract.Result<IEnumerable<IdentifierDiscoveryResult>>() != null);
+ return from service in xrds.FindOPIdentifierServices()
+ from uri in service.UriElements
+ let protocol = Protocol.FindBestVersion(p => p.OPIdentifierServiceTypeURI, service.TypeElementUris)
+ let providerDescription = new ProviderEndpointDescription(uri.Uri, service.TypeElementUris)
+ select IdentifierDiscoveryResult.CreateForProviderIdentifier(opIdentifier, providerDescription, service.Priority, uri.Priority);
+ }
+
+ /// <summary>
+ /// Generates the OpenID Providers that are capable of asserting ownership
+ /// of a particular URI claimed identifier.
+ /// </summary>
+ /// <param name="xrds">The XrdsDocument instance to use in this process.</param>
+ /// <param name="claimedIdentifier">The claimed identifier.</param>
+ /// <param name="userSuppliedIdentifier">The user supplied identifier.</param>
+ /// <returns>
+ /// A sequence of the providers that can assert ownership of the given identifier.
+ /// </returns>
+ private static IEnumerable<IdentifierDiscoveryResult> GenerateClaimedIdentifierServiceEndpoints(this IEnumerable<XrdElement> xrds, UriIdentifier claimedIdentifier, UriIdentifier userSuppliedIdentifier) {
+ Contract.Requires<ArgumentNullException>(xrds != null);
+ Contract.Requires<ArgumentNullException>(claimedIdentifier != null);
+ Contract.Ensures(Contract.Result<IEnumerable<IdentifierDiscoveryResult>>() != null);
+
+ return from service in xrds.FindClaimedIdentifierServices()
+ from uri in service.UriElements
+ where uri.Uri != null
+ let providerEndpoint = new ProviderEndpointDescription(uri.Uri, service.TypeElementUris)
+ select IdentifierDiscoveryResult.CreateForClaimedIdentifier(claimedIdentifier, userSuppliedIdentifier, service.ProviderLocalIdentifier, providerEndpoint, service.Priority, uri.Priority);
+ }
+
+ /// <summary>
+ /// Generates the OpenID Providers that are capable of asserting ownership
+ /// of a particular XRI claimed identifier.
+ /// </summary>
+ /// <param name="xrds">The XrdsDocument instance to use in this process.</param>
+ /// <param name="userSuppliedIdentifier">The i-name supplied by the user.</param>
+ /// <returns>A sequence of the providers that can assert ownership of the given identifier.</returns>
+ private static IEnumerable<IdentifierDiscoveryResult> GenerateClaimedIdentifierServiceEndpoints(this IEnumerable<XrdElement> xrds, XriIdentifier userSuppliedIdentifier) {
+ // Cannot use code contracts because this method uses yield return.
+ ////Contract.Requires<ArgumentNullException>(xrds != null);
+ ////Contract.Ensures(Contract.Result<IEnumerable<IdentifierDiscoveryResult>>() != null);
+ ErrorUtilities.VerifyArgumentNotNull(xrds, "xrds");
+
+ foreach (var service in xrds.FindClaimedIdentifierServices()) {
+ foreach (var uri in service.UriElements) {
+ // spec section 7.3.2.3 on Claimed Id -> CanonicalID substitution
+ if (service.Xrd.CanonicalID == null) {
+ Logger.Yadis.WarnFormat(XrdsStrings.MissingCanonicalIDElement, userSuppliedIdentifier);
+ break; // skip on to next service
+ }
+ ErrorUtilities.VerifyProtocol(service.Xrd.IsCanonicalIdVerified, XrdsStrings.CIDVerificationFailed, userSuppliedIdentifier);
+
+ // In the case of XRI names, the ClaimedId is actually the CanonicalID.
+ var claimedIdentifier = new XriIdentifier(service.Xrd.CanonicalID);
+ var providerEndpoint = new ProviderEndpointDescription(uri.Uri, service.TypeElementUris);
+ yield return IdentifierDiscoveryResult.CreateForClaimedIdentifier(claimedIdentifier, userSuppliedIdentifier, service.ProviderLocalIdentifier, providerEndpoint, service.Priority, uri.Priority);
+ }
+ }
+ }
+
+ /// <summary>
+ /// Enumerates the XRDS service elements that describe OpenID Providers offering directed identity assertions.
+ /// </summary>
+ /// <param name="xrds">The XrdsDocument instance to use in this process.</param>
+ /// <returns>A sequence of service elements.</returns>
+ private static IEnumerable<ServiceElement> FindOPIdentifierServices(this IEnumerable<XrdElement> xrds) {
+ Contract.Requires<ArgumentNullException>(xrds != null);
+ Contract.Ensures(Contract.Result<IEnumerable<ServiceElement>>() != null);
+
+ return from xrd in xrds
+ from service in xrd.OpenIdProviderIdentifierServices
+ select service;
+ }
+
+ /// <summary>
+ /// Returns the OpenID-compatible services described by a given XRDS document,
+ /// in priority order.
+ /// </summary>
+ /// <param name="xrds">The XrdsDocument instance to use in this process.</param>
+ /// <returns>A sequence of the services offered.</returns>
+ private static IEnumerable<ServiceElement> FindClaimedIdentifierServices(this IEnumerable<XrdElement> xrds) {
+ Contract.Requires<ArgumentNullException>(xrds != null);
+ Contract.Ensures(Contract.Result<IEnumerable<ServiceElement>>() != null);
+
+ return from xrd in xrds
+ from service in xrd.OpenIdClaimedIdentifierServices
+ select service;
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/ProviderEndpointDescription.cs b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/ProviderEndpointDescription.cs
new file mode 100644
index 0000000..6514ffd
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/ProviderEndpointDescription.cs
@@ -0,0 +1,134 @@
+//-----------------------------------------------------------------------
+// <copyright file="ProviderEndpointDescription.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OpenId {
+ using System;
+ using System.Collections.Generic;
+ using System.Collections.ObjectModel;
+ using System.Diagnostics.CodeAnalysis;
+ using System.Diagnostics.Contracts;
+ using System.Linq;
+ using DotNetOpenAuth.Messaging;
+ using DotNetOpenAuth.OpenId.Messages;
+ using DotNetOpenAuth.OpenId.RelyingParty;
+
+ /// <summary>
+ /// Describes some OpenID Provider endpoint and its capabilities.
+ /// </summary>
+ /// <remarks>
+ /// This is an immutable type.
+ /// </remarks>
+ [Serializable]
+ internal sealed class ProviderEndpointDescription : IProviderEndpoint {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ProviderEndpointDescription"/> class.
+ /// </summary>
+ /// <param name="providerEndpoint">The OpenID Provider endpoint URL.</param>
+ /// <param name="openIdVersion">The OpenID version supported by this particular endpoint.</param>
+ internal ProviderEndpointDescription(Uri providerEndpoint, Version openIdVersion) {
+ Contract.Requires<ArgumentNullException>(providerEndpoint != null);
+ Contract.Requires<ArgumentNullException>(openIdVersion != null);
+
+ this.Uri = providerEndpoint;
+ this.Version = openIdVersion;
+ this.Capabilities = new ReadOnlyCollection<string>(EmptyList<string>.Instance);
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ProviderEndpointDescription"/> class.
+ /// </summary>
+ /// <param name="providerEndpoint">The URI the provider listens on for OpenID requests.</param>
+ /// <param name="serviceTypeURIs">The set of services offered by this endpoint.</param>
+ internal ProviderEndpointDescription(Uri providerEndpoint, IEnumerable<string> serviceTypeURIs) {
+ Contract.Requires<ArgumentNullException>(providerEndpoint != null);
+ Contract.Requires<ArgumentNullException>(serviceTypeURIs != null);
+
+ this.Uri = providerEndpoint;
+ this.Capabilities = new ReadOnlyCollection<string>(serviceTypeURIs.ToList());
+
+ Protocol opIdentifierProtocol = Protocol.FindBestVersion(p => p.OPIdentifierServiceTypeURI, serviceTypeURIs);
+ Protocol claimedIdentifierProviderVersion = Protocol.FindBestVersion(p => p.ClaimedIdentifierServiceTypeURI, serviceTypeURIs);
+ if (opIdentifierProtocol != null) {
+ this.Version = opIdentifierProtocol.Version;
+ } else if (claimedIdentifierProviderVersion != null) {
+ this.Version = claimedIdentifierProviderVersion.Version;
+ } else {
+ ErrorUtilities.ThrowProtocol(OpenIdStrings.ProviderVersionUnrecognized, this.Uri);
+ }
+ }
+
+ /// <summary>
+ /// Gets the URL that the OpenID Provider listens for incoming OpenID messages on.
+ /// </summary>
+ public Uri Uri { get; private set; }
+
+ /// <summary>
+ /// Gets the OpenID protocol version this endpoint supports.
+ /// </summary>
+ /// <remarks>
+ /// If an endpoint supports multiple versions, each version must be represented
+ /// by its own <see cref="ProviderEndpointDescription"/> object.
+ /// </remarks>
+ public Version Version { get; private set; }
+
+ /// <summary>
+ /// Gets the collection of service type URIs found in the XRDS document describing this Provider.
+ /// </summary>
+ internal ReadOnlyCollection<string> Capabilities { get; private set; }
+
+ #region IProviderEndpoint Members
+
+ /// <summary>
+ /// Checks whether the OpenId Identifier claims support for a given extension.
+ /// </summary>
+ /// <typeparam name="T">The extension whose support is being queried.</typeparam>
+ /// <returns>
+ /// True if support for the extension is advertised. False otherwise.
+ /// </returns>
+ /// <remarks>
+ /// Note that a true or false return value is no guarantee of a Provider's
+ /// support for or lack of support for an extension. The return value is
+ /// determined by how the authenticating user filled out his/her XRDS document only.
+ /// The only way to be sure of support for a given extension is to include
+ /// the extension in the request and see if a response comes back for that extension.
+ /// </remarks>
+ bool IProviderEndpoint.IsExtensionSupported<T>() {
+ throw new NotImplementedException();
+ }
+
+ /// <summary>
+ /// Checks whether the OpenId Identifier claims support for a given extension.
+ /// </summary>
+ /// <param name="extensionType">The extension whose support is being queried.</param>
+ /// <returns>
+ /// True if support for the extension is advertised. False otherwise.
+ /// </returns>
+ /// <remarks>
+ /// Note that a true or false return value is no guarantee of a Provider's
+ /// support for or lack of support for an extension. The return value is
+ /// determined by how the authenticating user filled out his/her XRDS document only.
+ /// The only way to be sure of support for a given extension is to include
+ /// the extension in the request and see if a response comes back for that extension.
+ /// </remarks>
+ bool IProviderEndpoint.IsExtensionSupported(Type extensionType) {
+ throw new NotImplementedException();
+ }
+
+ #endregion
+
+#if CONTRACTS_FULL
+ /// <summary>
+ /// Verifies conditions that should be true for any valid state of this object.
+ /// </summary>
+ [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "Called by code contracts.")]
+ [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "Called by code contracts.")]
+ [ContractInvariantMethod]
+ private void ObjectInvariant() {
+ Contract.Invariant(this.Capabilities != null);
+ }
+#endif
+ }
+}
diff --git a/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/AssociationManager.cs b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/AssociationManager.cs
new file mode 100644
index 0000000..9a43506
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/AssociationManager.cs
@@ -0,0 +1,246 @@
+//-----------------------------------------------------------------------
+// <copyright file="AssociationManager.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OpenId.RelyingParty {
+ using System;
+ using System.Collections.Generic;
+ using System.Diagnostics.Contracts;
+ using System.Linq;
+ using System.Net;
+ using System.Security;
+ using System.Text;
+ using DotNetOpenAuth.Messaging;
+ using DotNetOpenAuth.OpenId.ChannelElements;
+ using DotNetOpenAuth.OpenId.Messages;
+
+ /// <summary>
+ /// Manages the establishment, storage and retrieval of associations at the relying party.
+ /// </summary>
+ internal class AssociationManager {
+ /// <summary>
+ /// The storage to use for saving and retrieving associations. May be null.
+ /// </summary>
+ private readonly IRelyingPartyAssociationStore associationStore;
+
+ /// <summary>
+ /// Backing field for the <see cref="Channel"/> property.
+ /// </summary>
+ private Channel channel;
+
+ /// <summary>
+ /// Backing field for the <see cref="SecuritySettings"/> property.
+ /// </summary>
+ private RelyingPartySecuritySettings securitySettings;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="AssociationManager"/> class.
+ /// </summary>
+ /// <param name="channel">The channel the relying party is using.</param>
+ /// <param name="associationStore">The association store. May be null for dumb mode relying parties.</param>
+ /// <param name="securitySettings">The security settings.</param>
+ internal AssociationManager(Channel channel, IRelyingPartyAssociationStore associationStore, RelyingPartySecuritySettings securitySettings) {
+ Contract.Requires<ArgumentNullException>(channel != null);
+ Contract.Requires<ArgumentNullException>(securitySettings != null);
+
+ this.channel = channel;
+ this.associationStore = associationStore;
+ this.securitySettings = securitySettings;
+ }
+
+ /// <summary>
+ /// Gets or sets the channel to use for establishing associations.
+ /// </summary>
+ /// <value>The channel.</value>
+ internal Channel Channel {
+ get {
+ return this.channel;
+ }
+
+ set {
+ Contract.Requires<ArgumentNullException>(value != null);
+ this.channel = value;
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets the security settings to apply in choosing association types to support.
+ /// </summary>
+ internal RelyingPartySecuritySettings SecuritySettings {
+ get {
+ return this.securitySettings;
+ }
+
+ set {
+ Contract.Requires<ArgumentNullException>(value != null);
+ this.securitySettings = value;
+ }
+ }
+
+ /// <summary>
+ /// Gets a value indicating whether this instance has an association store.
+ /// </summary>
+ /// <value>
+ /// <c>true</c> if the relying party can act in 'smart' mode;
+ /// <c>false</c> if the relying party must always act in 'dumb' mode.
+ /// </value>
+ internal bool HasAssociationStore {
+ get { return this.associationStore != null; }
+ }
+
+ /// <summary>
+ /// Gets the storage to use for saving and retrieving associations. May be null.
+ /// </summary>
+ internal IRelyingPartyAssociationStore AssociationStoreTestHook {
+ get { return this.associationStore; }
+ }
+
+ /// <summary>
+ /// Gets an association between this Relying Party and a given Provider
+ /// if it already exists in the association store.
+ /// </summary>
+ /// <param name="provider">The provider to create an association with.</param>
+ /// <returns>The association if one exists and has useful life remaining. Otherwise <c>null</c>.</returns>
+ internal Association GetExistingAssociation(IProviderEndpoint provider) {
+ Contract.Requires<ArgumentNullException>(provider != null);
+
+ // If the RP has no application store for associations, there's no point in creating one.
+ if (this.associationStore == null) {
+ return null;
+ }
+
+ Association association = this.associationStore.GetAssociation(provider.Uri, this.SecuritySettings);
+
+ // If the returned association does not fulfill security requirements, ignore it.
+ if (association != null && !this.SecuritySettings.IsAssociationInPermittedRange(association)) {
+ association = null;
+ }
+
+ if (association != null && !association.HasUsefulLifeRemaining) {
+ association = null;
+ }
+
+ return association;
+ }
+
+ /// <summary>
+ /// Gets an existing association with the specified Provider, or attempts to create
+ /// a new association of one does not already exist.
+ /// </summary>
+ /// <param name="provider">The provider to get an association for.</param>
+ /// <returns>The existing or new association; <c>null</c> if none existed and one could not be created.</returns>
+ internal Association GetOrCreateAssociation(IProviderEndpoint provider) {
+ return this.GetExistingAssociation(provider) ?? this.CreateNewAssociation(provider);
+ }
+
+ /// <summary>
+ /// Creates a new association with a given Provider.
+ /// </summary>
+ /// <param name="provider">The provider to create an association with.</param>
+ /// <returns>
+ /// The newly created association, or null if no association can be created with
+ /// the given Provider given the current security settings.
+ /// </returns>
+ /// <remarks>
+ /// A new association is created and returned even if one already exists in the
+ /// association store.
+ /// Any new association is automatically added to the <see cref="associationStore"/>.
+ /// </remarks>
+ private Association CreateNewAssociation(IProviderEndpoint provider) {
+ Contract.Requires<ArgumentNullException>(provider != null);
+
+ // If there is no association store, there is no point in creating an association.
+ if (this.associationStore == null) {
+ return null;
+ }
+
+ try {
+ var associateRequest = AssociateRequest.Create(this.securitySettings, provider);
+
+ const int RenegotiateRetries = 1;
+ return this.CreateNewAssociation(provider, associateRequest, RenegotiateRetries);
+ } catch (VerificationException ex) {
+ // See Trac ticket #163. In partial trust host environments, the
+ // Diffie-Hellman implementation we're using for HTTP OP endpoints
+ // sometimes causes the CLR to throw:
+ // "VerificationException: Operation could destabilize the runtime."
+ // Just give up and use dumb mode in this case.
+ Logger.OpenId.ErrorFormat("VerificationException occurred while trying to create an association with {0}. {1}", provider.Uri, ex);
+ return null;
+ }
+ }
+
+ /// <summary>
+ /// Creates a new association with a given Provider.
+ /// </summary>
+ /// <param name="provider">The provider to create an association with.</param>
+ /// <param name="associateRequest">The associate request. May be <c>null</c>, which will always result in a <c>null</c> return value..</param>
+ /// <param name="retriesRemaining">The number of times to try the associate request again if the Provider suggests it.</param>
+ /// <returns>
+ /// The newly created association, or null if no association can be created with
+ /// the given Provider given the current security settings.
+ /// </returns>
+ private Association CreateNewAssociation(IProviderEndpoint provider, AssociateRequest associateRequest, int retriesRemaining) {
+ Contract.Requires<ArgumentNullException>(provider != null);
+
+ if (associateRequest == null || retriesRemaining < 0) {
+ // this can happen if security requirements and protocol conflict
+ // to where there are no association types to choose from.
+ return null;
+ }
+
+ try {
+ var associateResponse = this.channel.Request(associateRequest);
+ var associateSuccessfulResponse = associateResponse as AssociateSuccessfulResponse;
+ var associateUnsuccessfulResponse = associateResponse as AssociateUnsuccessfulResponse;
+ if (associateSuccessfulResponse != null) {
+ Association association = associateSuccessfulResponse.CreateAssociation(associateRequest, null, null);
+ this.associationStore.StoreAssociation(provider.Uri, association);
+ return association;
+ } else if (associateUnsuccessfulResponse != null) {
+ if (string.IsNullOrEmpty(associateUnsuccessfulResponse.AssociationType)) {
+ Logger.OpenId.Debug("Provider rejected an association request and gave no suggestion as to an alternative association type. Giving up.");
+ return null;
+ }
+
+ if (!this.securitySettings.IsAssociationInPermittedRange(Protocol.Lookup(provider.Version), associateUnsuccessfulResponse.AssociationType)) {
+ Logger.OpenId.DebugFormat("Provider rejected an association request and suggested '{0}' as an association to try, which this Relying Party does not support. Giving up.", associateUnsuccessfulResponse.AssociationType);
+ return null;
+ }
+
+ if (retriesRemaining <= 0) {
+ Logger.OpenId.Debug("Unable to agree on an association type with the Provider in the allowed number of retries. Giving up.");
+ return null;
+ }
+
+ // Make sure the Provider isn't suggesting an incompatible pair of association/session types.
+ Protocol protocol = Protocol.Lookup(provider.Version);
+ ErrorUtilities.VerifyProtocol(
+ HmacShaAssociation.IsDHSessionCompatible(protocol, associateUnsuccessfulResponse.AssociationType, associateUnsuccessfulResponse.SessionType),
+ OpenIdStrings.IncompatibleAssociationAndSessionTypes,
+ associateUnsuccessfulResponse.AssociationType,
+ associateUnsuccessfulResponse.SessionType);
+
+ associateRequest = AssociateRequest.Create(this.securitySettings, provider, associateUnsuccessfulResponse.AssociationType, associateUnsuccessfulResponse.SessionType);
+ return this.CreateNewAssociation(provider, associateRequest, retriesRemaining - 1);
+ } else {
+ throw new ProtocolException(MessagingStrings.UnexpectedMessageReceivedOfMany);
+ }
+ } catch (ProtocolException ex) {
+ // If the association failed because the remote server can't handle Expect: 100 Continue headers,
+ // then our web request handler should have already accomodated for future calls. Go ahead and
+ // immediately make one of those future calls now to try to get the association to succeed.
+ if (StandardWebRequestHandler.IsExceptionFrom417ExpectationFailed(ex)) {
+ return this.CreateNewAssociation(provider, associateRequest, retriesRemaining - 1);
+ }
+
+ // Since having associations with OPs is not totally critical, we'll log and eat
+ // the exception so that auth may continue in dumb mode.
+ Logger.OpenId.ErrorFormat("An error occurred while trying to create an association with {0}. {1}", provider.Uri, ex);
+ return null;
+ }
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/AssociationPreference.cs b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/AssociationPreference.cs
new file mode 100644
index 0000000..9f4a21f
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/AssociationPreference.cs
@@ -0,0 +1,42 @@
+//-----------------------------------------------------------------------
+// <copyright file="AssociationPreference.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OpenId.RelyingParty {
+ using System;
+ using System.Collections.Generic;
+ using System.Text;
+
+ /// <summary>
+ /// Preferences regarding creation and use of an association between a relying party
+ /// and provider for authentication.
+ /// </summary>
+ internal enum AssociationPreference {
+ /// <summary>
+ /// Indicates that an association should be created for use in authentication
+ /// if one has not already been established between the relying party and the
+ /// selected provider.
+ /// </summary>
+ /// <remarks>
+ /// Even with this value, if an association attempt fails or the relying party
+ /// has no application store to recall associations, the authentication may
+ /// proceed without an association.
+ /// </remarks>
+ IfPossible,
+
+ /// <summary>
+ /// Indicates that an association should be used for authentication only if
+ /// it happens to already exist.
+ /// </summary>
+ IfAlreadyEstablished,
+
+ /// <summary>
+ /// Indicates that an authentication attempt should NOT use an OpenID association
+ /// between the relying party and the provider, even if an association was previously
+ /// created.
+ /// </summary>
+ Never,
+ }
+}
diff --git a/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/Associations.cs b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/Associations.cs
new file mode 100644
index 0000000..b171bec
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/Associations.cs
@@ -0,0 +1,127 @@
+//-----------------------------------------------------------------------
+// <copyright file="Associations.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OpenId.RelyingParty {
+ using System;
+ using System.Collections.Generic;
+ using System.Collections.ObjectModel;
+ using System.Diagnostics;
+ using System.Diagnostics.CodeAnalysis;
+ using System.Diagnostics.Contracts;
+ using System.Linq;
+ using DotNetOpenAuth.Messaging;
+
+ /// <summary>
+ /// A dictionary of handle/Association pairs.
+ /// </summary>
+ /// <remarks>
+ /// Each method is locked, even if it is only one line, so that they are thread safe
+ /// against each other, particularly the ones that enumerate over the list, since they
+ /// can break if the collection is changed by another thread during enumeration.
+ /// </remarks>
+ [DebuggerDisplay("Count = {assocs.Count}")]
+ [ContractVerification(true)]
+ internal class Associations {
+ /// <summary>
+ /// The lookup table where keys are the association handles and values are the associations themselves.
+ /// </summary>
+ [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
+ private readonly KeyedCollection<string, Association> associations = new KeyedCollectionDelegate<string, Association>(assoc => assoc.Handle);
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="Associations"/> class.
+ /// </summary>
+ public Associations() {
+ }
+
+ /// <summary>
+ /// Gets the <see cref="Association"/>s ordered in order of descending issue date
+ /// (most recently issued comes first). An empty sequence if no valid associations exist.
+ /// </summary>
+ /// <remarks>
+ /// This property is used by relying parties that are initiating authentication requests.
+ /// It does not apply to Providers, which always need a specific association by handle.
+ /// </remarks>
+ public IEnumerable<Association> Best {
+ get {
+ Contract.Ensures(Contract.Result<IEnumerable<Association>>() != null);
+
+ lock (this.associations) {
+ return this.associations.OrderByDescending(assoc => assoc.Issued);
+ }
+ }
+ }
+
+ /// <summary>
+ /// Stores an <see cref="Association"/> in the collection.
+ /// </summary>
+ /// <param name="association">The association to add to the collection.</param>
+ public void Set(Association association) {
+ Contract.Requires<ArgumentNullException>(association != null);
+ Contract.Ensures(this.Get(association.Handle) == association);
+ lock (this.associations) {
+ this.associations.Remove(association.Handle); // just in case one already exists.
+ this.associations.Add(association);
+ }
+
+ Contract.Assume(this.Get(association.Handle) == association);
+ }
+
+ /// <summary>
+ /// Returns the <see cref="Association"/> with the given handle. Null if not found.
+ /// </summary>
+ /// <param name="handle">The handle to the required association.</param>
+ /// <returns>The desired association, or null if none with the given handle could be found.</returns>
+ [Pure]
+ public Association Get(string handle) {
+ Contract.Requires<ArgumentException>(!string.IsNullOrEmpty(handle));
+
+ lock (this.associations) {
+ if (this.associations.Contains(handle)) {
+ return this.associations[handle];
+ } else {
+ return null;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Removes the <see cref="Association"/> with the given handle.
+ /// </summary>
+ /// <param name="handle">The handle to the required association.</param>
+ /// <returns>Whether an <see cref="Association"/> with the given handle was in the collection for removal.</returns>
+ public bool Remove(string handle) {
+ Contract.Requires<ArgumentException>(!string.IsNullOrEmpty(handle));
+ lock (this.associations) {
+ return this.associations.Remove(handle);
+ }
+ }
+
+ /// <summary>
+ /// Removes all expired associations from the collection.
+ /// </summary>
+ public void ClearExpired() {
+ lock (this.associations) {
+ var expireds = this.associations.Where(assoc => assoc.IsExpired).ToList();
+ foreach (Association assoc in expireds) {
+ this.associations.Remove(assoc.Handle);
+ }
+ }
+ }
+
+#if CONTRACTS_FULL
+ /// <summary>
+ /// Verifies conditions that should be true for any valid state of this object.
+ /// </summary>
+ [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "Called by code contracts.")]
+ [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "Called by code contracts.")]
+ [ContractInvariantMethod]
+ private void ObjectInvariant() {
+ Contract.Invariant(this.associations != null);
+ }
+#endif
+ }
+}
diff --git a/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/AuthenticationRequest.cs b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/AuthenticationRequest.cs
new file mode 100644
index 0000000..d79038c
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/AuthenticationRequest.cs
@@ -0,0 +1,594 @@
+//-----------------------------------------------------------------------
+// <copyright file="AuthenticationRequest.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OpenId.RelyingParty {
+ using System;
+ using System.Collections.Generic;
+ using System.Collections.Specialized;
+ using System.Diagnostics.Contracts;
+ using System.Linq;
+ using System.Text;
+ using System.Threading;
+ using System.Web;
+
+ using DotNetOpenAuth.Configuration;
+ using DotNetOpenAuth.Messaging;
+ using DotNetOpenAuth.OpenId.ChannelElements;
+ using DotNetOpenAuth.OpenId.Messages;
+
+ /// <summary>
+ /// Facilitates customization and creation and an authentication request
+ /// that a Relying Party is preparing to send.
+ /// </summary>
+ internal class AuthenticationRequest : IAuthenticationRequest {
+ /// <summary>
+ /// The name of the internal callback parameter to use to store the user-supplied identifier.
+ /// </summary>
+ internal const string UserSuppliedIdentifierParameterName = OpenIdUtilities.CustomParameterPrefix + "userSuppliedIdentifier";
+
+ /// <summary>
+ /// The relying party that created this request object.
+ /// </summary>
+ private readonly OpenIdRelyingParty RelyingParty;
+
+ /// <summary>
+ /// How an association may or should be created or used in the formulation of the
+ /// authentication request.
+ /// </summary>
+ private AssociationPreference associationPreference = AssociationPreference.IfPossible;
+
+ /// <summary>
+ /// The extensions that have been added to this authentication request.
+ /// </summary>
+ private List<IOpenIdMessageExtension> extensions = new List<IOpenIdMessageExtension>();
+
+ /// <summary>
+ /// Arguments to add to the return_to part of the query string, so that
+ /// these values come back to the consumer when the user agent returns.
+ /// </summary>
+ private Dictionary<string, string> returnToArgs = new Dictionary<string, string>();
+
+ /// <summary>
+ /// A value indicating whether the return_to callback arguments must be signed.
+ /// </summary>
+ /// <remarks>
+ /// This field defaults to false, but is set to true as soon as the first callback argument
+ /// is added that indicates it must be signed. At which point, all arguments are signed
+ /// even if individual ones did not need to be.
+ /// </remarks>
+ private bool returnToArgsMustBeSigned;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="AuthenticationRequest"/> class.
+ /// </summary>
+ /// <param name="discoveryResult">The endpoint that describes the OpenID Identifier and Provider that will complete the authentication.</param>
+ /// <param name="realm">The realm, or root URL, of the host web site.</param>
+ /// <param name="returnToUrl">The base return_to URL that the Provider should return the user to to complete authentication. This should not include callback parameters as these should be added using the <see cref="AddCallbackArguments(string, string)"/> method.</param>
+ /// <param name="relyingParty">The relying party that created this instance.</param>
+ private AuthenticationRequest(IdentifierDiscoveryResult discoveryResult, Realm realm, Uri returnToUrl, OpenIdRelyingParty relyingParty) {
+ Contract.Requires<ArgumentNullException>(discoveryResult != null);
+ Contract.Requires<ArgumentNullException>(realm != null);
+ Contract.Requires<ArgumentNullException>(returnToUrl != null);
+ Contract.Requires<ArgumentNullException>(relyingParty != null);
+
+ this.DiscoveryResult = discoveryResult;
+ this.RelyingParty = relyingParty;
+ this.Realm = realm;
+ this.ReturnToUrl = returnToUrl;
+
+ this.Mode = AuthenticationRequestMode.Setup;
+ }
+
+ #region IAuthenticationRequest Members
+
+ /// <summary>
+ /// Gets or sets the mode the Provider should use during authentication.
+ /// </summary>
+ /// <value></value>
+ public AuthenticationRequestMode Mode { get; set; }
+
+ /// <summary>
+ /// Gets the HTTP response the relying party should send to the user agent
+ /// to redirect it to the OpenID Provider to start the OpenID authentication process.
+ /// </summary>
+ /// <value></value>
+ public OutgoingWebResponse RedirectingResponse {
+ get {
+ foreach (var behavior in this.RelyingParty.Behaviors) {
+ behavior.OnOutgoingAuthenticationRequest(this);
+ }
+
+ return this.RelyingParty.Channel.PrepareResponse(this.CreateRequestMessage());
+ }
+ }
+
+ /// <summary>
+ /// Gets the URL that the user agent will return to after authentication
+ /// completes or fails at the Provider.
+ /// </summary>
+ /// <value></value>
+ public Uri ReturnToUrl { get; private set; }
+
+ /// <summary>
+ /// Gets the URL that identifies this consumer web application that
+ /// the Provider will display to the end user.
+ /// </summary>
+ public Realm Realm { get; private set; }
+
+ /// <summary>
+ /// Gets the Claimed Identifier that the User Supplied Identifier
+ /// resolved to. Null if the user provided an OP Identifier
+ /// (directed identity).
+ /// </summary>
+ /// <value></value>
+ /// <remarks>
+ /// Null is returned if the user is using the directed identity feature
+ /// of OpenID 2.0 to make it nearly impossible for a relying party site
+ /// to improperly store the reserved OpenID URL used for directed identity
+ /// as a user's own Identifier.
+ /// However, to test for the Directed Identity feature, please test the
+ /// <see cref="IsDirectedIdentity"/> property rather than testing this
+ /// property for a null value.
+ /// </remarks>
+ public Identifier ClaimedIdentifier {
+ get { return this.IsDirectedIdentity ? null : this.DiscoveryResult.ClaimedIdentifier; }
+ }
+
+ /// <summary>
+ /// Gets a value indicating whether the authenticating user has chosen to let the Provider
+ /// determine and send the ClaimedIdentifier after authentication.
+ /// </summary>
+ public bool IsDirectedIdentity {
+ get { return this.DiscoveryResult.ClaimedIdentifier == this.DiscoveryResult.Protocol.ClaimedIdentifierForOPIdentifier; }
+ }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this request only carries extensions
+ /// and is not a request to verify that the user controls some identifier.
+ /// </summary>
+ /// <value>
+ /// <c>true</c> if this request is merely a carrier of extensions and is not
+ /// about an OpenID identifier; otherwise, <c>false</c>.
+ /// </value>
+ public bool IsExtensionOnly { get; set; }
+
+ /// <summary>
+ /// Gets information about the OpenId Provider, as advertised by the
+ /// OpenId discovery documents found at the <see cref="ClaimedIdentifier"/>
+ /// location.
+ /// </summary>
+ public IProviderEndpoint Provider {
+ get { return this.DiscoveryResult; }
+ }
+
+ /// <summary>
+ /// Gets the discovery result leading to the formulation of this request.
+ /// </summary>
+ /// <value>The discovery result.</value>
+ public IdentifierDiscoveryResult DiscoveryResult { get; private set; }
+
+ #endregion
+
+ /// <summary>
+ /// Gets or sets how an association may or should be created or used
+ /// in the formulation of the authentication request.
+ /// </summary>
+ internal AssociationPreference AssociationPreference {
+ get { return this.associationPreference; }
+ set { this.associationPreference = value; }
+ }
+
+ /// <summary>
+ /// Gets the extensions that have been added to the request.
+ /// </summary>
+ internal IEnumerable<IOpenIdMessageExtension> AppliedExtensions {
+ get { return this.extensions; }
+ }
+
+ /// <summary>
+ /// Gets the list of extensions for this request.
+ /// </summary>
+ internal IList<IOpenIdMessageExtension> Extensions {
+ get { return this.extensions; }
+ }
+
+ #region IAuthenticationRequest methods
+
+ /// <summary>
+ /// Makes a dictionary of key/value pairs available when the authentication is completed.
+ /// </summary>
+ /// <param name="arguments">The arguments to add to the request's return_to URI.</param>
+ /// <remarks>
+ /// <para>Note that these values are NOT protected against eavesdropping in transit. No
+ /// privacy-sensitive data should be stored using this method.</para>
+ /// <para>The values stored here can be retrieved using
+ /// <see cref="IAuthenticationResponse.GetCallbackArguments"/>, which will only return the value
+ /// if it hasn't been tampered with in transit.</para>
+ /// <para>Since the data set here is sent in the querystring of the request and some
+ /// servers place limits on the size of a request URL, this data should be kept relatively
+ /// small to ensure successful authentication. About 1.5KB is about all that should be stored.</para>
+ /// </remarks>
+ public void AddCallbackArguments(IDictionary<string, string> arguments) {
+ ErrorUtilities.VerifyOperation(this.RelyingParty.CanSignCallbackArguments, OpenIdStrings.CallbackArgumentsRequireSecretStore, typeof(IRelyingPartyAssociationStore).Name, typeof(OpenIdRelyingParty).Name);
+
+ this.returnToArgsMustBeSigned = true;
+ foreach (var pair in arguments) {
+ ErrorUtilities.VerifyArgument(!string.IsNullOrEmpty(pair.Key), MessagingStrings.UnexpectedNullOrEmptyKey);
+ ErrorUtilities.VerifyArgument(pair.Value != null, MessagingStrings.UnexpectedNullValue, pair.Key);
+
+ this.returnToArgs.Add(pair.Key, pair.Value);
+ }
+ }
+
+ /// <summary>
+ /// Makes a key/value pair available when the authentication is completed.
+ /// </summary>
+ /// <param name="key">The parameter name.</param>
+ /// <param name="value">The value of the argument.</param>
+ /// <remarks>
+ /// <para>Note that these values are NOT protected against eavesdropping in transit. No
+ /// privacy-sensitive data should be stored using this method.</para>
+ /// <para>The value stored here can be retrieved using
+ /// <see cref="IAuthenticationResponse.GetCallbackArgument"/>, which will only return the value
+ /// if it hasn't been tampered with in transit.</para>
+ /// <para>Since the data set here is sent in the querystring of the request and some
+ /// servers place limits on the size of a request URL, this data should be kept relatively
+ /// small to ensure successful authentication. About 1.5KB is about all that should be stored.</para>
+ /// </remarks>
+ public void AddCallbackArguments(string key, string value) {
+ ErrorUtilities.VerifyOperation(this.RelyingParty.CanSignCallbackArguments, OpenIdStrings.CallbackArgumentsRequireSecretStore, typeof(IRelyingPartyAssociationStore).Name, typeof(OpenIdRelyingParty).Name);
+
+ this.returnToArgsMustBeSigned = true;
+ this.returnToArgs.Add(key, value);
+ }
+
+ /// <summary>
+ /// Makes a key/value pair available when the authentication is completed.
+ /// </summary>
+ /// <param name="key">The parameter name.</param>
+ /// <param name="value">The value of the argument. Must not be null.</param>
+ /// <remarks>
+ /// <para>Note that these values are NOT protected against tampering in transit. No
+ /// security-sensitive data should be stored using this method.</para>
+ /// <para>The value stored here can be retrieved using
+ /// <see cref="IAuthenticationResponse.GetCallbackArgument"/>.</para>
+ /// <para>Since the data set here is sent in the querystring of the request and some
+ /// servers place limits on the size of a request URL, this data should be kept relatively
+ /// small to ensure successful authentication. About 1.5KB is about all that should be stored.</para>
+ /// </remarks>
+ public void SetCallbackArgument(string key, string value) {
+ ErrorUtilities.VerifyOperation(this.RelyingParty.CanSignCallbackArguments, OpenIdStrings.CallbackArgumentsRequireSecretStore, typeof(IRelyingPartyAssociationStore).Name, typeof(OpenIdRelyingParty).Name);
+
+ this.returnToArgsMustBeSigned = true;
+ this.returnToArgs[key] = value;
+ }
+
+ /// <summary>
+ /// Makes a key/value pair available when the authentication is completed without
+ /// requiring a return_to signature to protect against tampering of the callback argument.
+ /// </summary>
+ /// <param name="key">The parameter name.</param>
+ /// <param name="value">The value of the argument. Must not be null.</param>
+ /// <remarks>
+ /// <para>Note that these values are NOT protected against eavesdropping or tampering in transit. No
+ /// security-sensitive data should be stored using this method. </para>
+ /// <para>The value stored here can be retrieved using
+ /// <see cref="IAuthenticationResponse.GetCallbackArgument"/>.</para>
+ /// <para>Since the data set here is sent in the querystring of the request and some
+ /// servers place limits on the size of a request URL, this data should be kept relatively
+ /// small to ensure successful authentication. About 1.5KB is about all that should be stored.</para>
+ /// </remarks>
+ public void SetUntrustedCallbackArgument(string key, string value) {
+ this.returnToArgs[key] = value;
+ }
+
+ /// <summary>
+ /// Adds an OpenID extension to the request directed at the OpenID provider.
+ /// </summary>
+ /// <param name="extension">The initialized extension to add to the request.</param>
+ public void AddExtension(IOpenIdMessageExtension extension) {
+ this.extensions.Add(extension);
+ }
+
+ /// <summary>
+ /// Redirects the user agent to the provider for authentication.
+ /// </summary>
+ /// <remarks>
+ /// This method requires an ASP.NET HttpContext.
+ /// </remarks>
+ public void RedirectToProvider() {
+ this.RedirectingResponse.Respond();
+ }
+
+ #endregion
+
+ /// <summary>
+ /// Performs identifier discovery, creates associations and generates authentication requests
+ /// on-demand for as long as new ones can be generated based on the results of Identifier discovery.
+ /// </summary>
+ /// <param name="userSuppliedIdentifier">The user supplied identifier.</param>
+ /// <param name="relyingParty">The relying party.</param>
+ /// <param name="realm">The realm.</param>
+ /// <param name="returnToUrl">The return_to base URL.</param>
+ /// <param name="createNewAssociationsAsNeeded">if set to <c>true</c>, associations that do not exist between this Relying Party and the asserting Providers are created before the authentication request is created.</param>
+ /// <returns>
+ /// A sequence of authentication requests, any of which constitutes a valid identity assertion on the Claimed Identifier.
+ /// Never null, but may be empty.
+ /// </returns>
+ internal static IEnumerable<AuthenticationRequest> Create(Identifier userSuppliedIdentifier, OpenIdRelyingParty relyingParty, Realm realm, Uri returnToUrl, bool createNewAssociationsAsNeeded) {
+ Contract.Requires<ArgumentNullException>(userSuppliedIdentifier != null);
+ Contract.Requires<ArgumentNullException>(relyingParty != null);
+ Contract.Requires<ArgumentNullException>(realm != null);
+ Contract.Ensures(Contract.Result<IEnumerable<AuthenticationRequest>>() != null);
+
+ // Normalize the portion of the return_to path that correlates to the realm for capitalization.
+ // (so that if a web app base path is /MyApp/, but the URL of this request happens to be
+ // /myapp/login.aspx, we bump up the return_to Url to use /MyApp/ so it matches the realm.
+ UriBuilder returnTo = new UriBuilder(returnToUrl);
+ if (returnTo.Path.StartsWith(realm.AbsolutePath, StringComparison.OrdinalIgnoreCase) &&
+ !returnTo.Path.StartsWith(realm.AbsolutePath, StringComparison.Ordinal)) {
+ returnTo.Path = realm.AbsolutePath + returnTo.Path.Substring(realm.AbsolutePath.Length);
+ returnToUrl = returnTo.Uri;
+ }
+
+ userSuppliedIdentifier = userSuppliedIdentifier.TrimFragment();
+ if (relyingParty.SecuritySettings.RequireSsl) {
+ // Rather than check for successful SSL conversion at this stage,
+ // We'll wait for secure discovery to fail on the new identifier.
+ if (!userSuppliedIdentifier.TryRequireSsl(out userSuppliedIdentifier)) {
+ // But at least log the failure.
+ Logger.OpenId.WarnFormat("RequireSsl mode is on, so discovery on insecure identifier {0} will yield no results.", userSuppliedIdentifier);
+ }
+ }
+
+ if (Logger.OpenId.IsWarnEnabled && returnToUrl.Query != null) {
+ NameValueCollection returnToArgs = HttpUtility.ParseQueryString(returnToUrl.Query);
+ foreach (string key in returnToArgs) {
+ if (OpenIdRelyingParty.IsOpenIdSupportingParameter(key)) {
+ Logger.OpenId.WarnFormat("OpenID argument \"{0}\" found in return_to URL. This can corrupt an OpenID response.", key);
+ }
+ }
+ }
+
+ // Throw an exception now if the realm and the return_to URLs don't match
+ // as required by the provider. We could wait for the provider to test this and
+ // fail, but this will be faster and give us a better error message.
+ ErrorUtilities.VerifyProtocol(realm.Contains(returnToUrl), OpenIdStrings.ReturnToNotUnderRealm, returnToUrl, realm);
+
+ // Perform discovery right now (not deferred).
+ IEnumerable<IdentifierDiscoveryResult> serviceEndpoints;
+ try {
+ var results = relyingParty.Discover(userSuppliedIdentifier).CacheGeneratedResults();
+
+ // If any OP Identifier service elements were found, we must not proceed
+ // to use any Claimed Identifier services, per OpenID 2.0 sections 7.3.2.2 and 11.2.
+ // For a discussion on this topic, see
+ // http://groups.google.com/group/dotnetopenid/browse_thread/thread/4b5a8c6b2210f387/5e25910e4d2252c8
+ // Usually the Discover method we called will automatically filter this for us, but
+ // just to be sure, we'll do it here as well since the RP may be configured to allow
+ // these dual identifiers for assertion verification purposes.
+ var opIdentifiers = results.Where(result => result.ClaimedIdentifier == result.Protocol.ClaimedIdentifierForOPIdentifier).CacheGeneratedResults();
+ var claimedIdentifiers = results.Where(result => result.ClaimedIdentifier != result.Protocol.ClaimedIdentifierForOPIdentifier);
+ serviceEndpoints = opIdentifiers.Any() ? opIdentifiers : claimedIdentifiers;
+ } catch (ProtocolException ex) {
+ Logger.Yadis.ErrorFormat("Error while performing discovery on: \"{0}\": {1}", userSuppliedIdentifier, ex);
+ serviceEndpoints = Enumerable.Empty<IdentifierDiscoveryResult>();
+ }
+
+ // Filter disallowed endpoints.
+ serviceEndpoints = relyingParty.SecuritySettings.FilterEndpoints(serviceEndpoints);
+
+ // Call another method that defers request generation.
+ return CreateInternal(userSuppliedIdentifier, relyingParty, realm, returnToUrl, serviceEndpoints, createNewAssociationsAsNeeded);
+ }
+
+ /// <summary>
+ /// Creates an instance of <see cref="AuthenticationRequest"/> FOR TESTING PURPOSES ONLY.
+ /// </summary>
+ /// <param name="discoveryResult">The discovery result.</param>
+ /// <param name="realm">The realm.</param>
+ /// <param name="returnTo">The return to.</param>
+ /// <param name="rp">The relying party.</param>
+ /// <returns>The instantiated <see cref="AuthenticationRequest"/>.</returns>
+ internal static AuthenticationRequest CreateForTest(IdentifierDiscoveryResult discoveryResult, Realm realm, Uri returnTo, OpenIdRelyingParty rp) {
+ return new AuthenticationRequest(discoveryResult, realm, returnTo, rp);
+ }
+
+ /// <summary>
+ /// Creates the request message to send to the Provider,
+ /// based on the properties in this instance.
+ /// </summary>
+ /// <returns>The message to send to the Provider.</returns>
+ internal SignedResponseRequest CreateRequestMessageTestHook()
+ {
+ return this.CreateRequestMessage();
+ }
+
+ /// <summary>
+ /// Performs deferred request generation for the <see cref="Create"/> method.
+ /// </summary>
+ /// <param name="userSuppliedIdentifier">The user supplied identifier.</param>
+ /// <param name="relyingParty">The relying party.</param>
+ /// <param name="realm">The realm.</param>
+ /// <param name="returnToUrl">The return_to base URL.</param>
+ /// <param name="serviceEndpoints">The discovered service endpoints on the Claimed Identifier.</param>
+ /// <param name="createNewAssociationsAsNeeded">if set to <c>true</c>, associations that do not exist between this Relying Party and the asserting Providers are created before the authentication request is created.</param>
+ /// <returns>
+ /// A sequence of authentication requests, any of which constitutes a valid identity assertion on the Claimed Identifier.
+ /// Never null, but may be empty.
+ /// </returns>
+ /// <remarks>
+ /// All data validation and cleansing steps must have ALREADY taken place
+ /// before calling this method.
+ /// </remarks>
+ private static IEnumerable<AuthenticationRequest> CreateInternal(Identifier userSuppliedIdentifier, OpenIdRelyingParty relyingParty, Realm realm, Uri returnToUrl, IEnumerable<IdentifierDiscoveryResult> serviceEndpoints, bool createNewAssociationsAsNeeded) {
+ // DO NOT USE CODE CONTRACTS IN THIS METHOD, since it uses yield return
+ ErrorUtilities.VerifyArgumentNotNull(userSuppliedIdentifier, "userSuppliedIdentifier");
+ ErrorUtilities.VerifyArgumentNotNull(relyingParty, "relyingParty");
+ ErrorUtilities.VerifyArgumentNotNull(realm, "realm");
+ ErrorUtilities.VerifyArgumentNotNull(serviceEndpoints, "serviceEndpoints");
+ ////Contract.Ensures(Contract.Result<IEnumerable<AuthenticationRequest>>() != null);
+
+ // If shared associations are required, then we had better have an association store.
+ ErrorUtilities.VerifyOperation(!relyingParty.SecuritySettings.RequireAssociation || relyingParty.AssociationManager.HasAssociationStore, OpenIdStrings.AssociationStoreRequired);
+
+ Logger.Yadis.InfoFormat("Performing discovery on user-supplied identifier: {0}", userSuppliedIdentifier);
+ IEnumerable<IdentifierDiscoveryResult> endpoints = FilterAndSortEndpoints(serviceEndpoints, relyingParty);
+
+ // Maintain a list of endpoints that we could not form an association with.
+ // We'll fallback to generating requests to these if the ones we CAN create
+ // an association with run out.
+ var failedAssociationEndpoints = new List<IdentifierDiscoveryResult>(0);
+
+ foreach (var endpoint in endpoints) {
+ Logger.OpenId.DebugFormat("Creating authentication request for user supplied Identifier: {0}", userSuppliedIdentifier);
+
+ // The strategy here is to prefer endpoints with whom we can create associations.
+ Association association = null;
+ if (relyingParty.AssociationManager.HasAssociationStore) {
+ // In some scenarios (like the AJAX control wanting ALL auth requests possible),
+ // we don't want to create associations with every Provider. But we'll use
+ // associations where they are already formed from previous authentications.
+ association = createNewAssociationsAsNeeded ? relyingParty.AssociationManager.GetOrCreateAssociation(endpoint) : relyingParty.AssociationManager.GetExistingAssociation(endpoint);
+ if (association == null && createNewAssociationsAsNeeded) {
+ Logger.OpenId.WarnFormat("Failed to create association with {0}. Skipping to next endpoint.", endpoint.ProviderEndpoint);
+
+ // No association could be created. Add it to the list of failed association
+ // endpoints and skip to the next available endpoint.
+ failedAssociationEndpoints.Add(endpoint);
+ continue;
+ }
+ }
+
+ yield return new AuthenticationRequest(endpoint, realm, returnToUrl, relyingParty);
+ }
+
+ // Now that we've run out of endpoints that respond to association requests,
+ // since we apparently are still running, the caller must want another request.
+ // We'll go ahead and generate the requests to OPs that may be down --
+ // unless associations are set as required in our security settings.
+ if (failedAssociationEndpoints.Count > 0) {
+ if (relyingParty.SecuritySettings.RequireAssociation) {
+ Logger.OpenId.Warn("Associations could not be formed with some Providers. Security settings require shared associations for authentication requests so these will be skipped.");
+ } else {
+ Logger.OpenId.Debug("Now generating requests for Provider endpoints that failed initial association attempts.");
+
+ foreach (var endpoint in failedAssociationEndpoints) {
+ Logger.OpenId.DebugFormat("Creating authentication request for user supplied Identifier: {0} at endpoint: {1}", userSuppliedIdentifier, endpoint.ProviderEndpoint.AbsoluteUri);
+
+ // Create the auth request, but prevent it from attempting to create an association
+ // because we've already tried. Let's not have it waste time trying again.
+ var authRequest = new AuthenticationRequest(endpoint, realm, returnToUrl, relyingParty);
+ authRequest.associationPreference = AssociationPreference.IfAlreadyEstablished;
+ yield return authRequest;
+ }
+ }
+ }
+ }
+
+ /// <summary>
+ /// Returns a filtered and sorted list of the available OP endpoints for a discovered Identifier.
+ /// </summary>
+ /// <param name="endpoints">The endpoints.</param>
+ /// <param name="relyingParty">The relying party.</param>
+ /// <returns>A filtered and sorted list of endpoints; may be empty if the input was empty or the filter removed all endpoints.</returns>
+ private static List<IdentifierDiscoveryResult> FilterAndSortEndpoints(IEnumerable<IdentifierDiscoveryResult> endpoints, OpenIdRelyingParty relyingParty) {
+ Contract.Requires<ArgumentNullException>(endpoints != null);
+ Contract.Requires<ArgumentNullException>(relyingParty != null);
+
+ bool anyFilteredOut = false;
+ var filteredEndpoints = new List<IdentifierDiscoveryResult>();
+ foreach (var endpoint in endpoints) {
+ if (relyingParty.FilterEndpoint(endpoint)) {
+ filteredEndpoints.Add(endpoint);
+ } else {
+ anyFilteredOut = true;
+ }
+ }
+
+ // Sort endpoints so that the first one in the list is the most preferred one.
+ filteredEndpoints.OrderBy(ep => ep, relyingParty.EndpointOrder);
+
+ var endpointList = new List<IdentifierDiscoveryResult>(filteredEndpoints.Count);
+ foreach (var endpoint in filteredEndpoints) {
+ endpointList.Add(endpoint);
+ }
+
+ if (anyFilteredOut) {
+ Logger.Yadis.DebugFormat("Some endpoints were filtered out. Total endpoints remaining: {0}", filteredEndpoints.Count);
+ }
+ if (Logger.Yadis.IsDebugEnabled) {
+ if (MessagingUtilities.AreEquivalent(endpoints, endpointList)) {
+ Logger.Yadis.Debug("Filtering and sorting of endpoints did not affect the list.");
+ } else {
+ Logger.Yadis.Debug("After filtering and sorting service endpoints, this is the new prioritized list:");
+ Logger.Yadis.Debug(Util.ToStringDeferred(filteredEndpoints, true));
+ }
+ }
+
+ return endpointList;
+ }
+
+ /// <summary>
+ /// Creates the request message to send to the Provider,
+ /// based on the properties in this instance.
+ /// </summary>
+ /// <returns>The message to send to the Provider.</returns>
+ private SignedResponseRequest CreateRequestMessage() {
+ Association association = this.GetAssociation();
+
+ SignedResponseRequest request;
+ if (!this.IsExtensionOnly) {
+ CheckIdRequest authRequest = new CheckIdRequest(this.DiscoveryResult.Version, this.DiscoveryResult.ProviderEndpoint, this.Mode);
+ authRequest.ClaimedIdentifier = this.DiscoveryResult.ClaimedIdentifier;
+ authRequest.LocalIdentifier = this.DiscoveryResult.ProviderLocalIdentifier;
+ request = authRequest;
+ } else {
+ request = new SignedResponseRequest(this.DiscoveryResult.Version, this.DiscoveryResult.ProviderEndpoint, this.Mode);
+ }
+ request.Realm = this.Realm;
+ request.ReturnTo = this.ReturnToUrl;
+ request.AssociationHandle = association != null ? association.Handle : null;
+ request.SignReturnTo = this.returnToArgsMustBeSigned;
+ request.AddReturnToArguments(this.returnToArgs);
+ if (this.DiscoveryResult.UserSuppliedIdentifier != null && OpenIdElement.Configuration.RelyingParty.PreserveUserSuppliedIdentifier) {
+ request.AddReturnToArguments(UserSuppliedIdentifierParameterName, this.DiscoveryResult.UserSuppliedIdentifier.OriginalString);
+ }
+ foreach (IOpenIdMessageExtension extension in this.extensions) {
+ request.Extensions.Add(extension);
+ }
+
+ return request;
+ }
+
+ /// <summary>
+ /// Gets the association to use for this authentication request.
+ /// </summary>
+ /// <returns>The association to use; <c>null</c> to use 'dumb mode'.</returns>
+ private Association GetAssociation() {
+ Association association = null;
+ switch (this.associationPreference) {
+ case AssociationPreference.IfPossible:
+ association = this.RelyingParty.AssociationManager.GetOrCreateAssociation(this.DiscoveryResult);
+ if (association == null) {
+ // Avoid trying to create the association again if the redirecting response
+ // is generated again.
+ this.associationPreference = AssociationPreference.IfAlreadyEstablished;
+ }
+ break;
+ case AssociationPreference.IfAlreadyEstablished:
+ association = this.RelyingParty.AssociationManager.GetExistingAssociation(this.DiscoveryResult);
+ break;
+ case AssociationPreference.Never:
+ break;
+ default:
+ throw new InternalErrorException();
+ }
+
+ return association;
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/AuthenticationStatus.cs b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/AuthenticationStatus.cs
new file mode 100644
index 0000000..d9e5d0a
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/AuthenticationStatus.cs
@@ -0,0 +1,43 @@
+//-----------------------------------------------------------------------
+// <copyright file="AuthenticationStatus.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OpenId.RelyingParty {
+ /// <summary>
+ /// An enumeration of the possible results of an authentication attempt.
+ /// </summary>
+ public enum AuthenticationStatus {
+ /// <summary>
+ /// The authentication was canceled by the user agent while at the provider.
+ /// </summary>
+ Canceled,
+
+ /// <summary>
+ /// The authentication failed because an error was detected in the OpenId communication.
+ /// </summary>
+ Failed,
+
+ /// <summary>
+ /// <para>The Provider responded to a request for immediate authentication approval
+ /// with a message stating that additional user agent interaction is required
+ /// before authentication can be completed.</para>
+ /// <para>Casting the <see cref="IAuthenticationResponse"/> to a
+ /// <see cref="ISetupRequiredAuthenticationResponse"/> in this case can help
+ /// you retry the authentication using setup (non-immediate) mode.</para>
+ /// </summary>
+ SetupRequired,
+
+ /// <summary>
+ /// Authentication is completed successfully.
+ /// </summary>
+ Authenticated,
+
+ /// <summary>
+ /// The Provider sent a message that did not contain an identity assertion,
+ /// but may carry OpenID extensions.
+ /// </summary>
+ ExtensionsOnly,
+ }
+}
diff --git a/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/Controls.cd b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/Controls.cd
new file mode 100644
index 0000000..f96db36
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/Controls.cd
@@ -0,0 +1,112 @@
+<?xml version="1.0" encoding="utf-8"?>
+<ClassDiagram MajorVersion="1" MinorVersion="1">
+ <Class Name="DotNetOpenAuth.OpenId.RelyingParty.OpenIdRelyingPartyAjaxControlBase">
+ <Position X="0.5" Y="9.75" Width="3" />
+ <Compartments>
+ <Compartment Name="Fields" Collapsed="true" />
+ </Compartments>
+ <TypeIdentifier>
+ <HashCode>BARAAAAAAAAAAACQAAAAAAAEAAAgAAAAAQAFAAAAAFk=</HashCode>
+ <FileName>OpenId\RelyingParty\OpenIdRelyingPartyAjaxControlBase.cs</FileName>
+ </TypeIdentifier>
+ <Lollipop Position="0.2" />
+ </Class>
+ <Class Name="DotNetOpenAuth.OpenId.RelyingParty.OpenIdMobileTextBox">
+ <Position X="8.5" Y="5.25" Width="2.5" />
+ <Compartments>
+ <Compartment Name="Fields" Collapsed="true" />
+ </Compartments>
+ <TypeIdentifier>
+ <HashCode>AI0JADgFQRQQDAIw4lAYSEIWCAMZhMVlELAASQIAgSI=</HashCode>
+ <FileName>OpenId\RelyingParty\OpenIdMobileTextBox.cs</FileName>
+ </TypeIdentifier>
+ </Class>
+ <Class Name="DotNetOpenAuth.OpenId.RelyingParty.OpenIdLogin">
+ <Position X="6.25" Y="1.25" Width="1.75" />
+ <Compartments>
+ <Compartment Name="Fields" Collapsed="true" />
+ </Compartments>
+ <NestedTypes>
+ <Class Name="DotNetOpenAuth.OpenId.RelyingParty.OpenIdLogin.InPlaceControl" Collapsed="true">
+ <TypeIdentifier>
+ <NewMemberFileName>OpenId\RelyingParty\OpenIdLogin.cs</NewMemberFileName>
+ </TypeIdentifier>
+ </Class>
+ </NestedTypes>
+ <TypeIdentifier>
+ <HashCode>gIMgADAIAQEQIJAYOQBSADiQBgiIECk0jQCggdAp4BQ=</HashCode>
+ <FileName>OpenId\RelyingParty\OpenIdLogin.cs</FileName>
+ </TypeIdentifier>
+ </Class>
+ <Class Name="DotNetOpenAuth.OpenId.RelyingParty.OpenIdAjaxTextBox">
+ <Position X="3.75" Y="10" Width="2.25" />
+ <Compartments>
+ <Compartment Name="Fields" Collapsed="true" />
+ </Compartments>
+ <TypeIdentifier>
+ <HashCode>ACBEEbABZzOzAKCYJNOEwM3uSIR5AAOkUFANCQ7DsVs=</HashCode>
+ <FileName>OpenId\RelyingParty\OpenIdAjaxTextBox.cs</FileName>
+ </TypeIdentifier>
+ <Lollipop Position="0.2" />
+ </Class>
+ <Class Name="DotNetOpenAuth.OpenId.RelyingParty.OpenIdButton">
+ <Position X="8.75" Y="1" Width="1.75" />
+ <Compartments>
+ <Compartment Name="Fields" Collapsed="true" />
+ </Compartments>
+ <InheritanceLine Type="DotNetOpenAuth.OpenId.RelyingParty.OpenIdRelyingPartyControlBase" ManuallyRouted="true" FixedFromPoint="true" FixedToPoint="true">
+ <Path>
+ <Point X="2.875" Y="0.5" />
+ <Point X="7.194" Y="0.5" />
+ <Point X="7.194" Y="1" />
+ <Point X="8.75" Y="1" />
+ </Path>
+ </InheritanceLine>
+ <TypeIdentifier>
+ <HashCode>BAAEQAAAAAAAAAACAAAgAAAAAIAAAACQABAECABAAAA=</HashCode>
+ <FileName>OpenId\RelyingParty\OpenIdButton.cs</FileName>
+ </TypeIdentifier>
+ <Lollipop Position="0.2" />
+ </Class>
+ <Class Name="DotNetOpenAuth.OpenId.RelyingParty.OpenIdTextBox">
+ <Position X="3.5" Y="1.25" Width="2.25" />
+ <Compartments>
+ <Compartment Name="Fields" Collapsed="true" />
+ </Compartments>
+ <TypeIdentifier>
+ <HashCode>AIEVQjgBIxYITIARcAAACEc2CIAIlER1CBAQSQoEpCg=</HashCode>
+ <FileName>OpenId\RelyingParty\OpenIdTextBox.cs</FileName>
+ </TypeIdentifier>
+ <Lollipop Position="0.2" />
+ </Class>
+ <Class Name="DotNetOpenAuth.OpenId.RelyingParty.OpenIdRelyingPartyControlBase">
+ <Position X="0.5" Y="0.5" Width="2.5" />
+ <Compartments>
+ <Compartment Name="Fields" Collapsed="true" />
+ </Compartments>
+ <NestedTypes>
+ <Enum Name="DotNetOpenAuth.OpenId.RelyingParty.OpenIdRelyingPartyControlBase.LoginSiteNotification" Collapsed="true">
+ <TypeIdentifier>
+ <NewMemberFileName>OpenId\RelyingParty\OpenIdRelyingPartyControlBase.cs</NewMemberFileName>
+ </TypeIdentifier>
+ </Enum>
+ <Enum Name="DotNetOpenAuth.OpenId.RelyingParty.OpenIdRelyingPartyControlBase.LoginPersistence" Collapsed="true">
+ <TypeIdentifier>
+ <NewMemberFileName>OpenId\RelyingParty\OpenIdRelyingPartyControlBase.cs</NewMemberFileName>
+ </TypeIdentifier>
+ </Enum>
+ <Class Name="DotNetOpenAuth.OpenId.RelyingParty.OpenIdRelyingPartyControlBase.DuplicateRequestedHostsComparer" Collapsed="true">
+ <TypeIdentifier>
+ <NewMemberFileName>OpenId\RelyingParty\OpenIdRelyingPartyControlBase.cs</NewMemberFileName>
+ </TypeIdentifier>
+ <Lollipop Position="0.2" />
+ </Class>
+ </NestedTypes>
+ <TypeIdentifier>
+ <HashCode>BA0AAsAAQCAwQAJAoFAWwADSAgE5EIEEEbAGSAwAgfI=</HashCode>
+ <FileName>OpenId\RelyingParty\OpenIdRelyingPartyControlBase.cs</FileName>
+ </TypeIdentifier>
+ <Lollipop Position="0.2" />
+ </Class>
+ <Font Name="Segoe UI" Size="9" />
+</ClassDiagram> \ No newline at end of file
diff --git a/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/CryptoKeyStoreAsRelyingPartyAssociationStore.cs b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/CryptoKeyStoreAsRelyingPartyAssociationStore.cs
new file mode 100644
index 0000000..02ed3b0
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/CryptoKeyStoreAsRelyingPartyAssociationStore.cs
@@ -0,0 +1,87 @@
+//-----------------------------------------------------------------------
+// <copyright file="CryptoKeyStoreAsRelyingPartyAssociationStore.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OpenId.RelyingParty {
+ using System;
+ using System.Diagnostics.Contracts;
+ using System.Linq;
+ using DotNetOpenAuth.Messaging.Bindings;
+
+ /// <summary>
+ /// Wraps a standard <see cref="ICryptoKeyStore"/> so that it behaves as an association store.
+ /// </summary>
+ internal class CryptoKeyStoreAsRelyingPartyAssociationStore : IRelyingPartyAssociationStore {
+ /// <summary>
+ /// The underlying key store.
+ /// </summary>
+ private readonly ICryptoKeyStore keyStore;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="CryptoKeyStoreAsRelyingPartyAssociationStore"/> class.
+ /// </summary>
+ /// <param name="keyStore">The key store.</param>
+ internal CryptoKeyStoreAsRelyingPartyAssociationStore(ICryptoKeyStore keyStore) {
+ Contract.Requires<ArgumentNullException>(keyStore != null);
+ Contract.Ensures(this.keyStore == keyStore);
+ this.keyStore = keyStore;
+ }
+
+ /// <summary>
+ /// Saves an <see cref="Association"/> for later recall.
+ /// </summary>
+ /// <param name="providerEndpoint">The OP Endpoint with which the association is established.</param>
+ /// <param name="association">The association to store.</param>
+ public void StoreAssociation(Uri providerEndpoint, Association association) {
+ var cryptoKey = new CryptoKey(association.SerializePrivateData(), association.Expires);
+ this.keyStore.StoreKey(providerEndpoint.AbsoluteUri, association.Handle, cryptoKey);
+ }
+
+ /// <summary>
+ /// Gets the best association (the one with the longest remaining life) for a given key.
+ /// </summary>
+ /// <param name="providerEndpoint">The OP Endpoint with which the association is established.</param>
+ /// <param name="securityRequirements">The security requirements that the returned association must meet.</param>
+ /// <returns>
+ /// The requested association, or null if no unexpired <see cref="Association"/>s exist for the given key.
+ /// </returns>
+ public Association GetAssociation(Uri providerEndpoint, SecuritySettings securityRequirements) {
+ var matches = from cryptoKey in this.keyStore.GetKeys(providerEndpoint.AbsoluteUri)
+ where cryptoKey.Value.ExpiresUtc > DateTime.UtcNow
+ orderby cryptoKey.Value.ExpiresUtc descending
+ let assoc = Association.Deserialize(cryptoKey.Key, cryptoKey.Value.ExpiresUtc, cryptoKey.Value.Key)
+ where assoc.HashBitLength >= securityRequirements.MinimumHashBitLength
+ where assoc.HashBitLength <= securityRequirements.MaximumHashBitLength
+ select assoc;
+ return matches.FirstOrDefault();
+ }
+
+ /// <summary>
+ /// Gets the association for a given key and handle.
+ /// </summary>
+ /// <param name="providerEndpoint">The OP Endpoint with which the association is established.</param>
+ /// <param name="handle">The handle of the specific association that must be recalled.</param>
+ /// <returns>
+ /// The requested association, or null if no unexpired <see cref="Association"/>s exist for the given key and handle.
+ /// </returns>
+ public Association GetAssociation(Uri providerEndpoint, string handle) {
+ var cryptoKey = this.keyStore.GetKey(providerEndpoint.AbsoluteUri, handle);
+ return cryptoKey != null ? Association.Deserialize(handle, cryptoKey.ExpiresUtc, cryptoKey.Key) : null;
+ }
+
+ /// <summary>
+ /// Removes a specified handle that may exist in the store.
+ /// </summary>
+ /// <param name="providerEndpoint">The OP Endpoint with which the association is established.</param>
+ /// <param name="handle">The handle of the specific association that must be deleted.</param>
+ /// <returns>
+ /// True if the association existed in this store previous to this call.
+ /// </returns>
+ public bool RemoveAssociation(Uri providerEndpoint, string handle) {
+ this.keyStore.RemoveKey(providerEndpoint.AbsoluteUri, handle);
+ return true; // return value isn't used by DNOA.
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/DuplicateRequestedHostsComparer.cs b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/DuplicateRequestedHostsComparer.cs
new file mode 100644
index 0000000..94eb5ba
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/DuplicateRequestedHostsComparer.cs
@@ -0,0 +1,74 @@
+//-----------------------------------------------------------------------
+// <copyright file="DuplicateRequestedHostsComparer.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OpenId.RelyingParty {
+ using System;
+ using System.Collections.Generic;
+ using System.Linq;
+ using System.Text;
+
+ /// <summary>
+ /// An authentication request comparer that judges equality solely on the OP endpoint hostname.
+ /// </summary>
+ internal 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 to compare.</param>
+ /// <param name="y">The second object 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/OpenId/RelyingParty/FailedAuthenticationResponse.cs b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/FailedAuthenticationResponse.cs
new file mode 100644
index 0000000..682e3ff
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/FailedAuthenticationResponse.cs
@@ -0,0 +1,299 @@
+//-----------------------------------------------------------------------
+// <copyright file="FailedAuthenticationResponse.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OpenId.RelyingParty {
+ using System;
+ using System.Collections.Generic;
+ using System.Diagnostics;
+ using System.Diagnostics.Contracts;
+ using System.Globalization;
+ using System.Text;
+ using System.Web;
+ using DotNetOpenAuth.Messaging;
+ using DotNetOpenAuth.OpenId;
+ using DotNetOpenAuth.OpenId.Messages;
+ using DotNetOpenAuth.OpenId.RelyingParty;
+
+ /// <summary>
+ /// Wraps a failed authentication response in an <see cref="IAuthenticationResponse"/> instance
+ /// for public consumption by the host web site.
+ /// </summary>
+ [DebuggerDisplay("{Exception.Message}")]
+ internal class FailedAuthenticationResponse : IAuthenticationResponse {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="FailedAuthenticationResponse"/> class.
+ /// </summary>
+ /// <param name="exception">The exception that resulted in the failed authentication.</param>
+ internal FailedAuthenticationResponse(Exception exception) {
+ Contract.Requires<ArgumentNullException>(exception != null);
+
+ this.Exception = exception;
+
+ string category = string.Empty;
+ if (Reporting.Enabled) {
+ var pe = exception as ProtocolException;
+ if (pe != null) {
+ var responseMessage = pe.FaultedMessage as IndirectSignedResponse;
+ if (responseMessage != null && responseMessage.ProviderEndpoint != null) { // check "required" parts because this is a failure after all
+ category = responseMessage.ProviderEndpoint.AbsoluteUri;
+ }
+ }
+
+ Reporting.RecordEventOccurrence(this, category);
+ }
+ }
+
+ #region IAuthenticationResponse Members
+
+ /// <summary>
+ /// Gets the Identifier that the end user claims to own. For use with user database storage and lookup.
+ /// May be null for some failed authentications (i.e. failed directed identity authentications).
+ /// </summary>
+ /// <value></value>
+ /// <remarks>
+ /// <para>
+ /// This is the secure identifier that should be used for database storage and lookup.
+ /// It is not always friendly (i.e. =Arnott becomes =!9B72.7DD1.50A9.5CCD), but it protects
+ /// user identities against spoofing and other attacks.
+ /// </para>
+ /// <para>
+ /// For user-friendly identifiers to display, use the
+ /// <see cref="FriendlyIdentifierForDisplay"/> property.
+ /// </para>
+ /// </remarks>
+ public Identifier ClaimedIdentifier {
+ get { return null; }
+ }
+
+ /// <summary>
+ /// Gets a user-friendly OpenID Identifier for display purposes ONLY.
+ /// </summary>
+ /// <value></value>
+ /// <remarks>
+ /// <para>
+ /// This <i>should</i> be put through <see cref="HttpUtility.HtmlEncode(string)"/> before
+ /// sending to a browser to secure against javascript injection attacks.
+ /// </para>
+ /// <para>
+ /// This property retains some aspects of the user-supplied identifier that get lost
+ /// in the <see cref="ClaimedIdentifier"/>. For example, XRIs used as user-supplied
+ /// identifiers (i.e. =Arnott) become unfriendly unique strings (i.e. =!9B72.7DD1.50A9.5CCD).
+ /// For display purposes, such as text on a web page that says "You're logged in as ...",
+ /// this property serves to provide the =Arnott string, or whatever else is the most friendly
+ /// string close to what the user originally typed in.
+ /// </para>
+ /// <para>
+ /// If the user-supplied identifier is a URI, this property will be the URI after all
+ /// redirects, and with the protocol and fragment trimmed off.
+ /// If the user-supplied identifier is an XRI, this property will be the original XRI.
+ /// If the user-supplied identifier is an OpenID Provider identifier (i.e. yahoo.com),
+ /// this property will be the Claimed Identifier, with the protocol stripped if it is a URI.
+ /// </para>
+ /// <para>
+ /// It is <b>very</b> important that this property <i>never</i> be used for database storage
+ /// or lookup to avoid identity spoofing and other security risks. For database storage
+ /// and lookup please use the <see cref="ClaimedIdentifier"/> property.
+ /// </para>
+ /// </remarks>
+ public string FriendlyIdentifierForDisplay {
+ get { return null; }
+ }
+
+ /// <summary>
+ /// Gets the detailed success or failure status of the authentication attempt.
+ /// </summary>
+ /// <value></value>
+ public AuthenticationStatus Status {
+ get { return AuthenticationStatus.Failed; }
+ }
+
+ /// <summary>
+ /// Gets information about the OpenId Provider, as advertised by the
+ /// OpenID discovery documents found at the <see cref="ClaimedIdentifier"/>
+ /// location.
+ /// </summary>
+ /// <value>
+ /// The Provider endpoint that issued the positive assertion;
+ /// or <c>null</c> if information about the Provider is unavailable.
+ /// </value>
+ public IProviderEndpoint Provider {
+ get { return null; }
+ }
+
+ /// <summary>
+ /// Gets the details regarding a failed authentication attempt, if available.
+ /// This will be set if and only if <see cref="Status"/> is <see cref="AuthenticationStatus.Failed"/>.
+ /// </summary>
+ public Exception Exception { get; private set; }
+
+ /// <summary>
+ /// Gets all the callback arguments that were previously added using
+ /// <see cref="IAuthenticationRequest.AddCallbackArguments(string, string)"/> or as a natural part
+ /// of the return_to URL.
+ /// </summary>
+ /// <returns>A name-value dictionary. Never null.</returns>
+ /// <remarks>
+ /// <para>This MAY return any argument on the querystring that came with the authentication response,
+ /// which may include parameters not explicitly added using
+ /// <see cref="IAuthenticationRequest.AddCallbackArguments(string, string)"/>.</para>
+ /// <para>Note that these values are NOT protected against tampering in transit.</para>
+ /// </remarks>
+ public IDictionary<string, string> GetCallbackArguments() {
+ return EmptyDictionary<string, string>.Instance;
+ }
+
+ /// <summary>
+ /// Gets all the callback arguments that were previously added using
+ /// <see cref="IAuthenticationRequest.AddCallbackArguments(string, string)"/> or as a natural part
+ /// of the return_to URL.
+ /// </summary>
+ /// <returns>A name-value dictionary. Never null.</returns>
+ /// <remarks>
+ /// Callback parameters are only available even if the RP is in stateless mode,
+ /// or the callback parameters are otherwise unverifiable as untampered with.
+ /// Therefore, use this method only when the callback argument is not to be
+ /// used to make a security-sensitive decision.
+ /// </remarks>
+ public IDictionary<string, string> GetUntrustedCallbackArguments() {
+ return EmptyDictionary<string, string>.Instance;
+ }
+
+ /// <summary>
+ /// Gets a callback argument's value that was previously added using
+ /// <see cref="IAuthenticationRequest.AddCallbackArguments(string, string)"/>.
+ /// </summary>
+ /// <param name="key">The name of the parameter whose value is sought.</param>
+ /// <returns>
+ /// The value of the argument, or null if the named parameter could not be found.
+ /// </returns>
+ /// <remarks>
+ /// <para>This may return any argument on the querystring that came with the authentication response,
+ /// which may include parameters not explicitly added using
+ /// <see cref="IAuthenticationRequest.AddCallbackArguments(string, string)"/>.</para>
+ /// <para>Note that these values are NOT protected against tampering in transit.</para>
+ /// </remarks>
+ public string GetCallbackArgument(string key) {
+ return null;
+ }
+
+ /// <summary>
+ /// Gets a callback argument's value that was previously added using
+ /// <see cref="IAuthenticationRequest.AddCallbackArguments(string, string)"/>.
+ /// </summary>
+ /// <param name="key">The name of the parameter whose value is sought.</param>
+ /// <returns>
+ /// The value of the argument, or null if the named parameter could not be found.
+ /// </returns>
+ /// <remarks>
+ /// Callback parameters are only available even if the RP is in stateless mode,
+ /// or the callback parameters are otherwise unverifiable as untampered with.
+ /// Therefore, use this method only when the callback argument is not to be
+ /// used to make a security-sensitive decision.
+ /// </remarks>
+ public string GetUntrustedCallbackArgument(string key) {
+ return null;
+ }
+
+ /// <summary>
+ /// Tries to get an OpenID extension that may be present in the response.
+ /// </summary>
+ /// <typeparam name="T">The type of extension to look for in the response message.</typeparam>
+ /// <returns>
+ /// The extension, if it is found. Null otherwise.
+ /// </returns>
+ /// <remarks>
+ /// <para>Extensions are returned only if the Provider signed them.
+ /// Relying parties that do not care if the values were modified in
+ /// transit should use the <see cref="GetUntrustedExtension&lt;T&gt;"/> method
+ /// in order to allow the Provider to not sign the extension. </para>
+ /// <para>Unsigned extensions are completely unreliable and should be
+ /// used only to prefill user forms since the user or any other third
+ /// party may have tampered with the data carried by the extension.</para>
+ /// <para>Signed extensions are only reliable if the relying party
+ /// trusts the OpenID Provider that signed them. Signing does not mean
+ /// the relying party can trust the values -- it only means that the values
+ /// have not been tampered with since the Provider sent the message.</para>
+ /// </remarks>
+ public T GetExtension<T>() where T : IOpenIdMessageExtension {
+ return default(T);
+ }
+
+ /// <summary>
+ /// Tries to get an OpenID extension that may be present in the response.
+ /// </summary>
+ /// <param name="extensionType">Type of the extension to look for in the response.</param>
+ /// <returns>
+ /// The extension, if it is found. Null otherwise.
+ /// </returns>
+ /// <remarks>
+ /// <para>Extensions are returned only if the Provider signed them.
+ /// Relying parties that do not care if the values were modified in
+ /// transit should use the <see cref="GetUntrustedExtension"/> method
+ /// in order to allow the Provider to not sign the extension. </para>
+ /// <para>Unsigned extensions are completely unreliable and should be
+ /// used only to prefill user forms since the user or any other third
+ /// party may have tampered with the data carried by the extension.</para>
+ /// <para>Signed extensions are only reliable if the relying party
+ /// trusts the OpenID Provider that signed them. Signing does not mean
+ /// the relying party can trust the values -- it only means that the values
+ /// have not been tampered with since the Provider sent the message.</para>
+ /// </remarks>
+ public IOpenIdMessageExtension GetExtension(Type extensionType) {
+ return null;
+ }
+
+ /// <summary>
+ /// Tries to get an OpenID extension that may be present in the response, without
+ /// requiring it to be signed by the Provider.
+ /// </summary>
+ /// <typeparam name="T">The type of extension to look for in the response message.</typeparam>
+ /// <returns>
+ /// The extension, if it is found. Null otherwise.
+ /// </returns>
+ /// <remarks>
+ /// <para>Extensions are returned whether they are signed or not.
+ /// Use the <see cref="GetExtension&lt;T&gt;"/> method to retrieve
+ /// extension responses only if they are signed by the Provider to
+ /// protect against tampering. </para>
+ /// <para>Unsigned extensions are completely unreliable and should be
+ /// used only to prefill user forms since the user or any other third
+ /// party may have tampered with the data carried by the extension.</para>
+ /// <para>Signed extensions are only reliable if the relying party
+ /// trusts the OpenID Provider that signed them. Signing does not mean
+ /// the relying party can trust the values -- it only means that the values
+ /// have not been tampered with since the Provider sent the message.</para>
+ /// </remarks>
+ public T GetUntrustedExtension<T>() where T : IOpenIdMessageExtension {
+ return default(T);
+ }
+
+ /// <summary>
+ /// Tries to get an OpenID extension that may be present in the response.
+ /// </summary>
+ /// <param name="extensionType">Type of the extension to look for in the response.</param>
+ /// <returns>
+ /// The extension, if it is found. Null otherwise.
+ /// </returns>
+ /// <remarks>
+ /// <para>Extensions are returned whether they are signed or not.
+ /// Use the <see cref="GetExtension"/> method to retrieve
+ /// extension responses only if they are signed by the Provider to
+ /// protect against tampering. </para>
+ /// <para>Unsigned extensions are completely unreliable and should be
+ /// used only to prefill user forms since the user or any other third
+ /// party may have tampered with the data carried by the extension.</para>
+ /// <para>Signed extensions are only reliable if the relying party
+ /// trusts the OpenID Provider that signed them. Signing does not mean
+ /// the relying party can trust the values -- it only means that the values
+ /// have not been tampered with since the Provider sent the message.</para>
+ /// </remarks>
+ public IOpenIdMessageExtension GetUntrustedExtension(Type extensionType) {
+ return null;
+ }
+
+ #endregion
+ }
+}
diff --git a/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/IAuthenticationRequest.cs b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/IAuthenticationRequest.cs
new file mode 100644
index 0000000..65db0bd
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/IAuthenticationRequest.cs
@@ -0,0 +1,186 @@
+//-----------------------------------------------------------------------
+// <copyright file="IAuthenticationRequest.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OpenId.RelyingParty {
+ using System;
+ using System.Collections.Generic;
+ using System.Diagnostics.Contracts;
+ using System.Linq;
+ using System.Text;
+ using DotNetOpenAuth.Messaging;
+ using DotNetOpenAuth.OpenId.Messages;
+
+ /// <summary>
+ /// Instances of this interface represent relying party authentication
+ /// requests that may be queried/modified in specific ways before being
+ /// routed to the OpenID Provider.
+ /// </summary>
+ [ContractClass(typeof(IAuthenticationRequestContract))]
+ public interface IAuthenticationRequest {
+ /// <summary>
+ /// Gets or sets the mode the Provider should use during authentication.
+ /// </summary>
+ AuthenticationRequestMode Mode { get; set; }
+
+ /// <summary>
+ /// Gets the HTTP response the relying party should send to the user agent
+ /// to redirect it to the OpenID Provider to start the OpenID authentication process.
+ /// </summary>
+ OutgoingWebResponse RedirectingResponse { get; }
+
+ /// <summary>
+ /// Gets the URL that the user agent will return to after authentication
+ /// completes or fails at the Provider.
+ /// </summary>
+ Uri ReturnToUrl { get; }
+
+ /// <summary>
+ /// Gets the URL that identifies this consumer web application that
+ /// the Provider will display to the end user.
+ /// </summary>
+ Realm Realm { get; }
+
+ /// <summary>
+ /// Gets the Claimed Identifier that the User Supplied Identifier
+ /// resolved to. Null if the user provided an OP Identifier
+ /// (directed identity).
+ /// </summary>
+ /// <remarks>
+ /// Null is returned if the user is using the directed identity feature
+ /// of OpenID 2.0 to make it nearly impossible for a relying party site
+ /// to improperly store the reserved OpenID URL used for directed identity
+ /// as a user's own Identifier.
+ /// However, to test for the Directed Identity feature, please test the
+ /// <see cref="IsDirectedIdentity"/> property rather than testing this
+ /// property for a null value.
+ /// </remarks>
+ Identifier ClaimedIdentifier { get; }
+
+ /// <summary>
+ /// Gets a value indicating whether the authenticating user has chosen to let the Provider
+ /// determine and send the ClaimedIdentifier after authentication.
+ /// </summary>
+ bool IsDirectedIdentity { get; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this request only carries extensions
+ /// and is not a request to verify that the user controls some identifier.
+ /// </summary>
+ /// <value>
+ /// <c>true</c> if this request is merely a carrier of extensions and is not
+ /// about an OpenID identifier; otherwise, <c>false</c>.
+ /// </value>
+ /// <remarks>
+ /// <para>Although OpenID is first and primarily an authentication protocol, its extensions
+ /// can be interesting all by themselves. For instance, a relying party might want
+ /// to know that its user is over 21 years old, or perhaps a member of some organization.
+ /// OpenID extensions can provide this, without any need for asserting the identity of the user.</para>
+ /// <para>Constructing an OpenID request for only extensions can be done by calling
+ /// <see cref="OpenIdRelyingParty.CreateRequest(Identifier)"/> with any valid OpenID identifier
+ /// (claimed identifier or OP identifier). But once this property is set to <c>true</c>,
+ /// the claimed identifier value in the request is not included in the transmitted message.</para>
+ /// <para>It is anticipated that an RP would only issue these types of requests to OPs that
+ /// trusts to make assertions regarding the individual holding an account at that OP, so it
+ /// is not likely that the RP would allow the user to type in an arbitrary claimed identifier
+ /// without checking that it resolved to an OP endpoint the RP has on a trust whitelist.</para>
+ /// </remarks>
+ bool IsExtensionOnly { get; set; }
+
+ /// <summary>
+ /// Gets information about the OpenId Provider, as advertised by the
+ /// OpenID discovery documents found at the <see cref="ClaimedIdentifier"/>
+ /// location.
+ /// </summary>
+ IProviderEndpoint Provider { get; }
+
+ /// <summary>
+ /// Gets the discovery result leading to the formulation of this request.
+ /// </summary>
+ /// <value>The discovery result.</value>
+ IdentifierDiscoveryResult DiscoveryResult { get; }
+
+ /// <summary>
+ /// Makes a dictionary of key/value pairs available when the authentication is completed.
+ /// </summary>
+ /// <param name="arguments">The arguments to add to the request's return_to URI. Values must not be null.</param>
+ /// <remarks>
+ /// <para>Note that these values are NOT protected against eavesdropping in transit. No
+ /// privacy-sensitive data should be stored using this method.</para>
+ /// <para>The values stored here can be retrieved using
+ /// <see cref="IAuthenticationResponse.GetCallbackArgument"/>, which will only return the value
+ /// if it can be verified as untampered with in transit.</para>
+ /// <para>Since the data set here is sent in the querystring of the request and some
+ /// servers place limits on the size of a request URL, this data should be kept relatively
+ /// small to ensure successful authentication. About 1.5KB is about all that should be stored.</para>
+ /// </remarks>
+ void AddCallbackArguments(IDictionary<string, string> arguments);
+
+ /// <summary>
+ /// Makes a key/value pair available when the authentication is completed.
+ /// </summary>
+ /// <param name="key">The parameter name.</param>
+ /// <param name="value">The value of the argument. Must not be null.</param>
+ /// <remarks>
+ /// <para>Note that these values are NOT protected against eavesdropping in transit. No
+ /// privacy-sensitive data should be stored using this method.</para>
+ /// <para>The value stored here can be retrieved using
+ /// <see cref="IAuthenticationResponse.GetCallbackArgument"/>, which will only return the value
+ /// if it can be verified as untampered with in transit.</para>
+ /// <para>Since the data set here is sent in the querystring of the request and some
+ /// servers place limits on the size of a request URL, this data should be kept relatively
+ /// small to ensure successful authentication. About 1.5KB is about all that should be stored.</para>
+ /// </remarks>
+ void AddCallbackArguments(string key, string value);
+
+ /// <summary>
+ /// Makes a key/value pair available when the authentication is completed.
+ /// </summary>
+ /// <param name="key">The parameter name.</param>
+ /// <param name="value">The value of the argument. Must not be null.</param>
+ /// <remarks>
+ /// <para>Note that these values are NOT protected against eavesdropping in transit. No
+ /// security-sensitive data should be stored using this method.</para>
+ /// <para>The value stored here can be retrieved using
+ /// <see cref="IAuthenticationResponse.GetCallbackArgument"/>.</para>
+ /// <para>Since the data set here is sent in the querystring of the request and some
+ /// servers place limits on the size of a request URL, this data should be kept relatively
+ /// small to ensure successful authentication. About 1.5KB is about all that should be stored.</para>
+ /// </remarks>
+ void SetCallbackArgument(string key, string value);
+
+ /// <summary>
+ /// Makes a key/value pair available when the authentication is completed without
+ /// requiring a return_to signature to protect against tampering of the callback argument.
+ /// </summary>
+ /// <param name="key">The parameter name.</param>
+ /// <param name="value">The value of the argument. Must not be null.</param>
+ /// <remarks>
+ /// <para>Note that these values are NOT protected against eavesdropping or tampering in transit. No
+ /// security-sensitive data should be stored using this method. </para>
+ /// <para>The value stored here can be retrieved using
+ /// <see cref="IAuthenticationResponse.GetCallbackArgument"/>.</para>
+ /// <para>Since the data set here is sent in the querystring of the request and some
+ /// servers place limits on the size of a request URL, this data should be kept relatively
+ /// small to ensure successful authentication. About 1.5KB is about all that should be stored.</para>
+ /// </remarks>
+ void SetUntrustedCallbackArgument(string key, string value);
+
+ /// <summary>
+ /// Adds an OpenID extension to the request directed at the OpenID provider.
+ /// </summary>
+ /// <param name="extension">The initialized extension to add to the request.</param>
+ void AddExtension(IOpenIdMessageExtension extension);
+
+ /// <summary>
+ /// Redirects the user agent to the provider for authentication.
+ /// Execution of the current page terminates after this call.
+ /// </summary>
+ /// <remarks>
+ /// This method requires an ASP.NET HttpContext.
+ /// </remarks>
+ void RedirectToProvider();
+ }
+}
diff --git a/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/IAuthenticationRequestContract.cs b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/IAuthenticationRequestContract.cs
new file mode 100644
index 0000000..cd36cc7
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/IAuthenticationRequestContract.cs
@@ -0,0 +1,111 @@
+// <auto-generated />
+
+namespace DotNetOpenAuth.OpenId.RelyingParty {
+ using System;
+ using System.Collections.Generic;
+ using System.Diagnostics.Contracts;
+ using System.Linq;
+ using System.Text;
+ using DotNetOpenAuth.Messaging;
+ using DotNetOpenAuth.OpenId.Messages;
+
+ [ContractClassFor(typeof(IAuthenticationRequest))]
+ internal abstract class IAuthenticationRequestContract : IAuthenticationRequest {
+ #region IAuthenticationRequest Members
+
+ AuthenticationRequestMode IAuthenticationRequest.Mode {
+ get {
+ throw new NotImplementedException();
+ }
+
+ set {
+ throw new NotImplementedException();
+ }
+ }
+
+ OutgoingWebResponse IAuthenticationRequest.RedirectingResponse {
+ get { throw new NotImplementedException(); }
+ }
+
+ Uri IAuthenticationRequest.ReturnToUrl {
+ get { throw new NotImplementedException(); }
+ }
+
+ Realm IAuthenticationRequest.Realm {
+ get {
+ Contract.Ensures(Contract.Result<Realm>() != null);
+ throw new NotImplementedException();
+ }
+ }
+
+ Identifier IAuthenticationRequest.ClaimedIdentifier {
+ get {
+ throw new NotImplementedException();
+ }
+ }
+
+ bool IAuthenticationRequest.IsDirectedIdentity {
+ get { throw new NotImplementedException(); }
+ }
+
+ bool IAuthenticationRequest.IsExtensionOnly {
+ get {
+ throw new NotImplementedException();
+ }
+
+ set {
+ throw new NotImplementedException();
+ }
+ }
+
+ IProviderEndpoint IAuthenticationRequest.Provider {
+ get {
+ Contract.Ensures(Contract.Result<IProviderEndpoint>() != null);
+ throw new NotImplementedException();
+ }
+ }
+
+ IdentifierDiscoveryResult IAuthenticationRequest.DiscoveryResult {
+ get {
+ Contract.Ensures(Contract.Result<IdentifierDiscoveryResult>() != null);
+ throw new NotImplementedException();
+ }
+ }
+
+ void IAuthenticationRequest.AddCallbackArguments(IDictionary<string, string> arguments) {
+ Contract.Requires<ArgumentNullException>(arguments != null);
+ Contract.Requires<ArgumentException>(arguments.Keys.All(k => !String.IsNullOrEmpty(k)));
+ Contract.Requires<ArgumentException>(arguments.Values.All(v => v != null));
+ throw new NotImplementedException();
+ }
+
+ void IAuthenticationRequest.AddCallbackArguments(string key, string value) {
+ Contract.Requires<ArgumentException>(!String.IsNullOrEmpty(key));
+ Contract.Requires<ArgumentNullException>(value != null);
+ throw new NotImplementedException();
+ }
+
+ void IAuthenticationRequest.SetCallbackArgument(string key, string value) {
+ Contract.Requires<ArgumentException>(!String.IsNullOrEmpty(key));
+ Contract.Requires<ArgumentNullException>(value != null);
+ throw new NotImplementedException();
+ }
+
+ void IAuthenticationRequest.AddExtension(IOpenIdMessageExtension extension) {
+ Contract.Requires<ArgumentNullException>(extension != null);
+ throw new NotImplementedException();
+ }
+
+ void IAuthenticationRequest.RedirectToProvider() {
+ throw new NotImplementedException();
+ }
+
+ void IAuthenticationRequest.SetUntrustedCallbackArgument(string key, string value) {
+ Contract.Requires<ArgumentException>(!string.IsNullOrEmpty(key));
+ Contract.Requires<ArgumentNullException>(value != null);
+ throw new NotImplementedException();
+ }
+
+ #endregion
+ }
+}
diff --git a/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/IAuthenticationResponse.cs b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/IAuthenticationResponse.cs
new file mode 100644
index 0000000..a24220f
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/IAuthenticationResponse.cs
@@ -0,0 +1,532 @@
+//-----------------------------------------------------------------------
+// <copyright file="IAuthenticationResponse.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OpenId.RelyingParty {
+ using System;
+ using System.Collections.Generic;
+ using System.Diagnostics.CodeAnalysis;
+ using System.Diagnostics.Contracts;
+ using System.Globalization;
+ using System.Text;
+ using System.Web;
+ using DotNetOpenAuth.OpenId.Extensions;
+ using DotNetOpenAuth.OpenId.Messages;
+
+ /// <summary>
+ /// An instance of this interface represents an identity assertion
+ /// from an OpenID Provider. It may be in response to an authentication
+ /// request previously put to it by a Relying Party site or it may be an
+ /// unsolicited assertion.
+ /// </summary>
+ /// <remarks>
+ /// Relying party web sites should handle both solicited and unsolicited
+ /// assertions. This interface does not offer a way to discern between
+ /// solicited and unsolicited assertions as they should be treated equally.
+ /// </remarks>
+ [ContractClass(typeof(IAuthenticationResponseContract))]
+ public interface IAuthenticationResponse {
+ /// <summary>
+ /// Gets the Identifier that the end user claims to own. For use with user database storage and lookup.
+ /// May be null for some failed authentications (i.e. failed directed identity authentications).
+ /// </summary>
+ /// <remarks>
+ /// <para>
+ /// This is the secure identifier that should be used for database storage and lookup.
+ /// It is not always friendly (i.e. =Arnott becomes =!9B72.7DD1.50A9.5CCD), but it protects
+ /// user identities against spoofing and other attacks.
+ /// </para>
+ /// <para>
+ /// For user-friendly identifiers to display, use the
+ /// <see cref="FriendlyIdentifierForDisplay"/> property.
+ /// </para>
+ /// </remarks>
+ Identifier ClaimedIdentifier { get; }
+
+ /// <summary>
+ /// Gets a user-friendly OpenID Identifier for display purposes ONLY.
+ /// </summary>
+ /// <remarks>
+ /// <para>
+ /// This <i>should</i> be put through <see cref="HttpUtility.HtmlEncode(string)"/> before
+ /// sending to a browser to secure against javascript injection attacks.
+ /// </para>
+ /// <para>
+ /// This property retains some aspects of the user-supplied identifier that get lost
+ /// in the <see cref="ClaimedIdentifier"/>. For example, XRIs used as user-supplied
+ /// identifiers (i.e. =Arnott) become unfriendly unique strings (i.e. =!9B72.7DD1.50A9.5CCD).
+ /// For display purposes, such as text on a web page that says "You're logged in as ...",
+ /// this property serves to provide the =Arnott string, or whatever else is the most friendly
+ /// string close to what the user originally typed in.
+ /// </para>
+ /// <para>
+ /// If the user-supplied identifier is a URI, this property will be the URI after all
+ /// redirects, and with the protocol and fragment trimmed off.
+ /// If the user-supplied identifier is an XRI, this property will be the original XRI.
+ /// If the user-supplied identifier is an OpenID Provider identifier (i.e. yahoo.com),
+ /// this property will be the Claimed Identifier, with the protocol stripped if it is a URI.
+ /// </para>
+ /// <para>
+ /// It is <b>very</b> important that this property <i>never</i> be used for database storage
+ /// or lookup to avoid identity spoofing and other security risks. For database storage
+ /// and lookup please use the <see cref="ClaimedIdentifier"/> property.
+ /// </para>
+ /// </remarks>
+ string FriendlyIdentifierForDisplay { get; }
+
+ /// <summary>
+ /// Gets the detailed success or failure status of the authentication attempt.
+ /// </summary>
+ AuthenticationStatus Status { get; }
+
+ /// <summary>
+ /// Gets information about the OpenId Provider, as advertised by the
+ /// OpenID discovery documents found at the <see cref="ClaimedIdentifier"/>
+ /// location, if available.
+ /// </summary>
+ /// <value>
+ /// The Provider endpoint that issued the positive assertion;
+ /// or <c>null</c> if information about the Provider is unavailable.
+ /// </value>
+ IProviderEndpoint Provider { get; }
+
+ /// <summary>
+ /// Gets the details regarding a failed authentication attempt, if available.
+ /// This will be set if and only if <see cref="Status"/> is <see cref="AuthenticationStatus.Failed"/>.
+ /// </summary>
+ Exception Exception { get; }
+
+ /// <summary>
+ /// Gets a callback argument's value that was previously added using
+ /// <see cref="IAuthenticationRequest.AddCallbackArguments(string, string)"/>.
+ /// </summary>
+ /// <param name="key">The name of the parameter whose value is sought.</param>
+ /// <returns>
+ /// The value of the argument, or null if the named parameter could not be found.
+ /// </returns>
+ /// <remarks>
+ /// Callback parameters are only available if they are complete and untampered with
+ /// since the original request message (as proven by a signature).
+ /// If the relying party is operating in stateless mode <c>null</c> is always
+ /// returned since the callback arguments could not be signed to protect against
+ /// tampering.
+ /// </remarks>
+ string GetCallbackArgument(string key);
+
+ /// <summary>
+ /// Gets a callback argument's value that was previously added using
+ /// <see cref="IAuthenticationRequest.AddCallbackArguments(string, string)"/>.
+ /// </summary>
+ /// <param name="key">The name of the parameter whose value is sought.</param>
+ /// <returns>
+ /// The value of the argument, or null if the named parameter could not be found.
+ /// </returns>
+ /// <remarks>
+ /// Callback parameters are only available even if the RP is in stateless mode,
+ /// or the callback parameters are otherwise unverifiable as untampered with.
+ /// Therefore, use this method only when the callback argument is not to be
+ /// used to make a security-sensitive decision.
+ /// </remarks>
+ string GetUntrustedCallbackArgument(string key);
+
+ /// <summary>
+ /// Gets all the callback arguments that were previously added using
+ /// <see cref="IAuthenticationRequest.AddCallbackArguments(string, string)"/> or as a natural part
+ /// of the return_to URL.
+ /// </summary>
+ /// <returns>A name-value dictionary. Never null.</returns>
+ /// <remarks>
+ /// Callback parameters are only available if they are complete and untampered with
+ /// since the original request message (as proven by a signature).
+ /// If the relying party is operating in stateless mode an empty dictionary is always
+ /// returned since the callback arguments could not be signed to protect against
+ /// tampering.
+ /// </remarks>
+ [SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate", Justification = "Historically an expensive operation.")]
+ IDictionary<string, string> GetCallbackArguments();
+
+ /// <summary>
+ /// Gets all the callback arguments that were previously added using
+ /// <see cref="IAuthenticationRequest.AddCallbackArguments(string, string)"/> or as a natural part
+ /// of the return_to URL.
+ /// </summary>
+ /// <returns>A name-value dictionary. Never null.</returns>
+ /// <remarks>
+ /// Callback parameters are only available even if the RP is in stateless mode,
+ /// or the callback parameters are otherwise unverifiable as untampered with.
+ /// Therefore, use this method only when the callback argument is not to be
+ /// used to make a security-sensitive decision.
+ /// </remarks>
+ [SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate", Justification = "Historically an expensive operation.")]
+ IDictionary<string, string> GetUntrustedCallbackArguments();
+
+ /// <summary>
+ /// Tries to get an OpenID extension that may be present in the response.
+ /// </summary>
+ /// <typeparam name="T">The type of extension to look for in the response message.</typeparam>
+ /// <returns>
+ /// The extension, if it is found. Null otherwise.
+ /// </returns>
+ /// <remarks>
+ /// <para>Extensions are returned only if the Provider signed them.
+ /// Relying parties that do not care if the values were modified in
+ /// transit should use the <see cref="GetUntrustedExtension&lt;T&gt;"/> method
+ /// in order to allow the Provider to not sign the extension. </para>
+ /// <para>Unsigned extensions are completely unreliable and should be
+ /// used only to prefill user forms since the user or any other third
+ /// party may have tampered with the data carried by the extension.</para>
+ /// <para>Signed extensions are only reliable if the relying party
+ /// trusts the OpenID Provider that signed them. Signing does not mean
+ /// the relying party can trust the values -- it only means that the values
+ /// have not been tampered with since the Provider sent the message.</para>
+ /// </remarks>
+ [SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter", Justification = "No parameter at all is required. T is used for return type.")]
+ T GetExtension<T>() where T : IOpenIdMessageExtension;
+
+ /// <summary>
+ /// Tries to get an OpenID extension that may be present in the response.
+ /// </summary>
+ /// <param name="extensionType">Type of the extension to look for in the response.</param>
+ /// <returns>
+ /// The extension, if it is found. Null otherwise.
+ /// </returns>
+ /// <remarks>
+ /// <para>Extensions are returned only if the Provider signed them.
+ /// Relying parties that do not care if the values were modified in
+ /// transit should use the <see cref="GetUntrustedExtension"/> method
+ /// in order to allow the Provider to not sign the extension. </para>
+ /// <para>Unsigned extensions are completely unreliable and should be
+ /// used only to prefill user forms since the user or any other third
+ /// party may have tampered with the data carried by the extension.</para>
+ /// <para>Signed extensions are only reliable if the relying party
+ /// trusts the OpenID Provider that signed them. Signing does not mean
+ /// the relying party can trust the values -- it only means that the values
+ /// have not been tampered with since the Provider sent the message.</para>
+ /// </remarks>
+ IOpenIdMessageExtension GetExtension(Type extensionType);
+
+ /// <summary>
+ /// Tries to get an OpenID extension that may be present in the response, without
+ /// requiring it to be signed by the Provider.
+ /// </summary>
+ /// <typeparam name="T">The type of extension to look for in the response message.</typeparam>
+ /// <returns>
+ /// The extension, if it is found. Null otherwise.
+ /// </returns>
+ /// <remarks>
+ /// <para>Extensions are returned whether they are signed or not.
+ /// Use the <see cref="GetExtension&lt;T&gt;"/> method to retrieve
+ /// extension responses only if they are signed by the Provider to
+ /// protect against tampering. </para>
+ /// <para>Unsigned extensions are completely unreliable and should be
+ /// used only to prefill user forms since the user or any other third
+ /// party may have tampered with the data carried by the extension.</para>
+ /// <para>Signed extensions are only reliable if the relying party
+ /// trusts the OpenID Provider that signed them. Signing does not mean
+ /// the relying party can trust the values -- it only means that the values
+ /// have not been tampered with since the Provider sent the message.</para>
+ /// </remarks>
+ [SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter", Justification = "No parameter at all is required. T is used for return type.")]
+ T GetUntrustedExtension<T>() where T : IOpenIdMessageExtension;
+
+ /// <summary>
+ /// Tries to get an OpenID extension that may be present in the response, without
+ /// requiring it to be signed by the Provider.
+ /// </summary>
+ /// <param name="extensionType">Type of the extension to look for in the response.</param>
+ /// <returns>
+ /// The extension, if it is found. Null otherwise.
+ /// </returns>
+ /// <remarks>
+ /// <para>Extensions are returned whether they are signed or not.
+ /// Use the <see cref="GetExtension"/> method to retrieve
+ /// extension responses only if they are signed by the Provider to
+ /// protect against tampering. </para>
+ /// <para>Unsigned extensions are completely unreliable and should be
+ /// used only to prefill user forms since the user or any other third
+ /// party may have tampered with the data carried by the extension.</para>
+ /// <para>Signed extensions are only reliable if the relying party
+ /// trusts the OpenID Provider that signed them. Signing does not mean
+ /// the relying party can trust the values -- it only means that the values
+ /// have not been tampered with since the Provider sent the message.</para>
+ /// </remarks>
+ IOpenIdMessageExtension GetUntrustedExtension(Type extensionType);
+ }
+
+ /// <summary>
+ /// Code contract for the <see cref="IAuthenticationResponse"/> type.
+ /// </summary>
+ [ContractClassFor(typeof(IAuthenticationResponse))]
+ internal abstract class IAuthenticationResponseContract : IAuthenticationResponse {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="IAuthenticationResponseContract"/> class.
+ /// </summary>
+ protected IAuthenticationResponseContract() {
+ }
+
+ #region IAuthenticationResponse Members
+
+ /// <summary>
+ /// Gets the Identifier that the end user claims to own. For use with user database storage and lookup.
+ /// May be null for some failed authentications (i.e. failed directed identity authentications).
+ /// </summary>
+ /// <value></value>
+ /// <remarks>
+ /// <para>
+ /// This is the secure identifier that should be used for database storage and lookup.
+ /// It is not always friendly (i.e. =Arnott becomes =!9B72.7DD1.50A9.5CCD), but it protects
+ /// user identities against spoofing and other attacks.
+ /// </para>
+ /// <para>
+ /// For user-friendly identifiers to display, use the
+ /// <see cref="IAuthenticationResponse.FriendlyIdentifierForDisplay"/> property.
+ /// </para>
+ /// </remarks>
+ Identifier IAuthenticationResponse.ClaimedIdentifier {
+ get { throw new NotImplementedException(); }
+ }
+
+ /// <summary>
+ /// Gets a user-friendly OpenID Identifier for display purposes ONLY.
+ /// </summary>
+ /// <value></value>
+ /// <remarks>
+ /// <para>
+ /// This <i>should</i> be put through <see cref="HttpUtility.HtmlEncode(string)"/> before
+ /// sending to a browser to secure against javascript injection attacks.
+ /// </para>
+ /// <para>
+ /// This property retains some aspects of the user-supplied identifier that get lost
+ /// in the <see cref="IAuthenticationResponse.ClaimedIdentifier"/>. For example, XRIs used as user-supplied
+ /// identifiers (i.e. =Arnott) become unfriendly unique strings (i.e. =!9B72.7DD1.50A9.5CCD).
+ /// For display purposes, such as text on a web page that says "You're logged in as ...",
+ /// this property serves to provide the =Arnott string, or whatever else is the most friendly
+ /// string close to what the user originally typed in.
+ /// </para>
+ /// <para>
+ /// If the user-supplied identifier is a URI, this property will be the URI after all
+ /// redirects, and with the protocol and fragment trimmed off.
+ /// If the user-supplied identifier is an XRI, this property will be the original XRI.
+ /// If the user-supplied identifier is an OpenID Provider identifier (i.e. yahoo.com),
+ /// this property will be the Claimed Identifier, with the protocol stripped if it is a URI.
+ /// </para>
+ /// <para>
+ /// It is <b>very</b> important that this property <i>never</i> be used for database storage
+ /// or lookup to avoid identity spoofing and other security risks. For database storage
+ /// and lookup please use the <see cref="IAuthenticationResponse.ClaimedIdentifier"/> property.
+ /// </para>
+ /// </remarks>
+ string IAuthenticationResponse.FriendlyIdentifierForDisplay {
+ get { throw new NotImplementedException(); }
+ }
+
+ /// <summary>
+ /// Gets the detailed success or failure status of the authentication attempt.
+ /// </summary>
+ /// <value></value>
+ AuthenticationStatus IAuthenticationResponse.Status {
+ get { throw new NotImplementedException(); }
+ }
+
+ /// <summary>
+ /// Gets information about the OpenId Provider, as advertised by the
+ /// OpenID discovery documents found at the <see cref="IAuthenticationResponse.ClaimedIdentifier"/>
+ /// location, if available.
+ /// </summary>
+ /// <value>
+ /// The Provider endpoint that issued the positive assertion;
+ /// or <c>null</c> if information about the Provider is unavailable.
+ /// </value>
+ IProviderEndpoint IAuthenticationResponse.Provider {
+ get { throw new NotImplementedException(); }
+ }
+
+ /// <summary>
+ /// Gets the details regarding a failed authentication attempt, if available.
+ /// This will be set if and only if <see cref="IAuthenticationResponse.Status"/> is <see cref="AuthenticationStatus.Failed"/>.
+ /// </summary>
+ /// <value></value>
+ Exception IAuthenticationResponse.Exception {
+ get { throw new NotImplementedException(); }
+ }
+
+ /// <summary>
+ /// Gets a callback argument's value that was previously added using
+ /// <see cref="IAuthenticationRequest.AddCallbackArguments(string, string)"/>.
+ /// </summary>
+ /// <param name="key">The name of the parameter whose value is sought.</param>
+ /// <returns>
+ /// The value of the argument, or null if the named parameter could not be found.
+ /// </returns>
+ /// <remarks>
+ /// <para>This may return any argument on the querystring that came with the authentication response,
+ /// which may include parameters not explicitly added using
+ /// <see cref="IAuthenticationRequest.AddCallbackArguments(string, string)"/>.</para>
+ /// <para>Note that these values are NOT protected against tampering in transit.</para>
+ /// </remarks>
+ string IAuthenticationResponse.GetCallbackArgument(string key) {
+ Contract.Requires<ArgumentException>(!String.IsNullOrEmpty(key));
+ throw new NotImplementedException();
+ }
+
+ /// <summary>
+ /// Gets all the callback arguments that were previously added using
+ /// <see cref="IAuthenticationRequest.AddCallbackArguments(string, string)"/> or as a natural part
+ /// of the return_to URL.
+ /// </summary>
+ /// <returns>A name-value dictionary. Never null.</returns>
+ /// <remarks>
+ /// <para>This MAY return any argument on the querystring that came with the authentication response,
+ /// which may include parameters not explicitly added using
+ /// <see cref="IAuthenticationRequest.AddCallbackArguments(string, string)"/>.</para>
+ /// <para>Note that these values are NOT protected against tampering in transit.</para>
+ /// </remarks>
+ IDictionary<string, string> IAuthenticationResponse.GetCallbackArguments() {
+ throw new NotImplementedException();
+ }
+
+ /// <summary>
+ /// Tries to get an OpenID extension that may be present in the response.
+ /// </summary>
+ /// <typeparam name="T">The type of extension to look for in the response message.</typeparam>
+ /// <returns>
+ /// The extension, if it is found. Null otherwise.
+ /// </returns>
+ /// <remarks>
+ /// <para>Extensions are returned only if the Provider signed them.
+ /// Relying parties that do not care if the values were modified in
+ /// transit should use the <see cref="IAuthenticationResponse.GetUntrustedExtension&lt;T&gt;"/> method
+ /// in order to allow the Provider to not sign the extension. </para>
+ /// <para>Unsigned extensions are completely unreliable and should be
+ /// used only to prefill user forms since the user or any other third
+ /// party may have tampered with the data carried by the extension.</para>
+ /// <para>Signed extensions are only reliable if the relying party
+ /// trusts the OpenID Provider that signed them. Signing does not mean
+ /// the relying party can trust the values -- it only means that the values
+ /// have not been tampered with since the Provider sent the message.</para>
+ /// </remarks>
+ T IAuthenticationResponse.GetExtension<T>() {
+ throw new NotImplementedException();
+ }
+
+ /// <summary>
+ /// Tries to get an OpenID extension that may be present in the response.
+ /// </summary>
+ /// <param name="extensionType">Type of the extension to look for in the response.</param>
+ /// <returns>
+ /// The extension, if it is found. Null otherwise.
+ /// </returns>
+ /// <remarks>
+ /// <para>Extensions are returned only if the Provider signed them.
+ /// Relying parties that do not care if the values were modified in
+ /// transit should use the <see cref="IAuthenticationResponse.GetUntrustedExtension"/> method
+ /// in order to allow the Provider to not sign the extension. </para>
+ /// <para>Unsigned extensions are completely unreliable and should be
+ /// used only to prefill user forms since the user or any other third
+ /// party may have tampered with the data carried by the extension.</para>
+ /// <para>Signed extensions are only reliable if the relying party
+ /// trusts the OpenID Provider that signed them. Signing does not mean
+ /// the relying party can trust the values -- it only means that the values
+ /// have not been tampered with since the Provider sent the message.</para>
+ /// </remarks>
+ IOpenIdMessageExtension IAuthenticationResponse.GetExtension(Type extensionType) {
+ Contract.Requires<ArgumentNullException>(extensionType != null);
+ Contract.Requires<ArgumentException>(typeof(IOpenIdMessageExtension).IsAssignableFrom(extensionType));
+ ////ErrorUtilities.VerifyArgument(typeof(IOpenIdMessageExtension).IsAssignableFrom(extensionType), string.Format(CultureInfo.CurrentCulture, OpenIdStrings.TypeMustImplementX, typeof(IOpenIdMessageExtension).FullName));
+ throw new NotImplementedException();
+ }
+
+ /// <summary>
+ /// Tries to get an OpenID extension that may be present in the response, without
+ /// requiring it to be signed by the Provider.
+ /// </summary>
+ /// <typeparam name="T">The type of extension to look for in the response message.</typeparam>
+ /// <returns>
+ /// The extension, if it is found. Null otherwise.
+ /// </returns>
+ /// <remarks>
+ /// <para>Extensions are returned whether they are signed or not.
+ /// Use the <see cref="IAuthenticationResponse.GetExtension&lt;T&gt;"/> method to retrieve
+ /// extension responses only if they are signed by the Provider to
+ /// protect against tampering. </para>
+ /// <para>Unsigned extensions are completely unreliable and should be
+ /// used only to prefill user forms since the user or any other third
+ /// party may have tampered with the data carried by the extension.</para>
+ /// <para>Signed extensions are only reliable if the relying party
+ /// trusts the OpenID Provider that signed them. Signing does not mean
+ /// the relying party can trust the values -- it only means that the values
+ /// have not been tampered with since the Provider sent the message.</para>
+ /// </remarks>
+ T IAuthenticationResponse.GetUntrustedExtension<T>() {
+ throw new NotImplementedException();
+ }
+
+ /// <summary>
+ /// Tries to get an OpenID extension that may be present in the response, without
+ /// requiring it to be signed by the Provider.
+ /// </summary>
+ /// <param name="extensionType">Type of the extension to look for in the response.</param>
+ /// <returns>
+ /// The extension, if it is found. Null otherwise.
+ /// </returns>
+ /// <remarks>
+ /// <para>Extensions are returned whether they are signed or not.
+ /// Use the <see cref="IAuthenticationResponse.GetExtension"/> method to retrieve
+ /// extension responses only if they are signed by the Provider to
+ /// protect against tampering. </para>
+ /// <para>Unsigned extensions are completely unreliable and should be
+ /// used only to prefill user forms since the user or any other third
+ /// party may have tampered with the data carried by the extension.</para>
+ /// <para>Signed extensions are only reliable if the relying party
+ /// trusts the OpenID Provider that signed them. Signing does not mean
+ /// the relying party can trust the values -- it only means that the values
+ /// have not been tampered with since the Provider sent the message.</para>
+ /// </remarks>
+ IOpenIdMessageExtension IAuthenticationResponse.GetUntrustedExtension(Type extensionType) {
+ Contract.Requires<ArgumentNullException>(extensionType != null);
+ Contract.Requires<ArgumentException>(typeof(IOpenIdMessageExtension).IsAssignableFrom(extensionType));
+ ////ErrorUtilities.VerifyArgument(typeof(IOpenIdMessageExtension).IsAssignableFrom(extensionType), string.Format(CultureInfo.CurrentCulture, OpenIdStrings.TypeMustImplementX, typeof(IOpenIdMessageExtension).FullName));
+ throw new NotImplementedException();
+ }
+
+ /// <summary>
+ /// Gets a callback argument's value that was previously added using
+ /// <see cref="IAuthenticationRequest.AddCallbackArguments(string, string)"/>.
+ /// </summary>
+ /// <param name="key">The name of the parameter whose value is sought.</param>
+ /// <returns>
+ /// The value of the argument, or null if the named parameter could not be found.
+ /// </returns>
+ /// <remarks>
+ /// Callback parameters are only available even if the RP is in stateless mode,
+ /// or the callback parameters are otherwise unverifiable as untampered with.
+ /// Therefore, use this method only when the callback argument is not to be
+ /// used to make a security-sensitive decision.
+ /// </remarks>
+ string IAuthenticationResponse.GetUntrustedCallbackArgument(string key) {
+ Contract.Requires<ArgumentException>(!string.IsNullOrEmpty(key));
+ throw new NotImplementedException();
+ }
+
+ /// <summary>
+ /// Gets all the callback arguments that were previously added using
+ /// <see cref="IAuthenticationRequest.AddCallbackArguments(string, string)"/> or as a natural part
+ /// of the return_to URL.
+ /// </summary>
+ /// <returns>A name-value dictionary. Never null.</returns>
+ /// <remarks>
+ /// Callback parameters are only available even if the RP is in stateless mode,
+ /// or the callback parameters are otherwise unverifiable as untampered with.
+ /// Therefore, use this method only when the callback argument is not to be
+ /// used to make a security-sensitive decision.
+ /// </remarks>
+ IDictionary<string, string> IAuthenticationResponse.GetUntrustedCallbackArguments() {
+ Contract.Ensures(Contract.Result<IDictionary<string, string>>() != null);
+ throw new NotImplementedException();
+ }
+
+ #endregion
+ }
+}
diff --git a/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/IProviderEndpoint.cs b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/IProviderEndpoint.cs
new file mode 100644
index 0000000..5d8918d
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/IProviderEndpoint.cs
@@ -0,0 +1,144 @@
+//-----------------------------------------------------------------------
+// <copyright file="IProviderEndpoint.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OpenId.RelyingParty {
+ using System;
+ using System.Collections.ObjectModel;
+ using System.Diagnostics.CodeAnalysis;
+ using System.Diagnostics.Contracts;
+ using System.Globalization;
+ using DotNetOpenAuth.Messaging;
+ using DotNetOpenAuth.OpenId.Messages;
+
+ /// <summary>
+ /// Information published about an OpenId Provider by the
+ /// OpenId discovery documents found at a user's Claimed Identifier.
+ /// </summary>
+ /// <remarks>
+ /// Because information provided by this interface is suppplied by a
+ /// user's individually published documents, it may be incomplete or inaccurate.
+ /// </remarks>
+ [ContractClass(typeof(IProviderEndpointContract))]
+ public interface IProviderEndpoint {
+ /// <summary>
+ /// Gets the detected version of OpenID implemented by the Provider.
+ /// </summary>
+ Version Version { get; }
+
+ /// <summary>
+ /// Gets the URL that the OpenID Provider receives authentication requests at.
+ /// </summary>
+ /// <value>
+ /// This value MUST be an absolute HTTP or HTTPS URL.
+ /// </value>
+ Uri Uri { get; }
+
+ /// <summary>
+ /// Checks whether the OpenId Identifier claims support for a given extension.
+ /// </summary>
+ /// <typeparam name="T">The extension whose support is being queried.</typeparam>
+ /// <returns>True if support for the extension is advertised. False otherwise.</returns>
+ /// <remarks>
+ /// Note that a true or false return value is no guarantee of a Provider's
+ /// support for or lack of support for an extension. The return value is
+ /// determined by how the authenticating user filled out his/her XRDS document only.
+ /// The only way to be sure of support for a given extension is to include
+ /// the extension in the request and see if a response comes back for that extension.
+ /// </remarks>
+ [SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter", Justification = "No parameter at all.")]
+ [Obsolete("Use IAuthenticationRequest.DiscoveryResult.IsExtensionSupported instead.")]
+ bool IsExtensionSupported<T>() where T : IOpenIdMessageExtension, new();
+
+ /// <summary>
+ /// Checks whether the OpenId Identifier claims support for a given extension.
+ /// </summary>
+ /// <param name="extensionType">The extension whose support is being queried.</param>
+ /// <returns>True if support for the extension is advertised. False otherwise.</returns>
+ /// <remarks>
+ /// Note that a true or false return value is no guarantee of a Provider's
+ /// support for or lack of support for an extension. The return value is
+ /// determined by how the authenticating user filled out his/her XRDS document only.
+ /// The only way to be sure of support for a given extension is to include
+ /// the extension in the request and see if a response comes back for that extension.
+ /// </remarks>
+ [Obsolete("Use IAuthenticationRequest.DiscoveryResult.IsExtensionSupported instead.")]
+ bool IsExtensionSupported(Type extensionType);
+ }
+
+ /// <summary>
+ /// Code contract for the <see cref="IProviderEndpoint"/> type.
+ /// </summary>
+ [ContractClassFor(typeof(IProviderEndpoint))]
+ internal abstract class IProviderEndpointContract : IProviderEndpoint {
+ /// <summary>
+ /// Prevents a default instance of the <see cref="IProviderEndpointContract"/> class from being created.
+ /// </summary>
+ private IProviderEndpointContract() {
+ }
+
+ #region IProviderEndpoint Members
+
+ /// <summary>
+ /// Gets the detected version of OpenID implemented by the Provider.
+ /// </summary>
+ Version IProviderEndpoint.Version {
+ get {
+ Contract.Ensures(Contract.Result<Version>() != null);
+ throw new System.NotImplementedException();
+ }
+ }
+
+ /// <summary>
+ /// Gets the URL that the OpenID Provider receives authentication requests at.
+ /// </summary>
+ Uri IProviderEndpoint.Uri {
+ get {
+ Contract.Ensures(Contract.Result<Uri>() != null);
+ throw new System.NotImplementedException();
+ }
+ }
+
+ /// <summary>
+ /// Checks whether the OpenId Identifier claims support for a given extension.
+ /// </summary>
+ /// <typeparam name="T">The extension whose support is being queried.</typeparam>
+ /// <returns>
+ /// True if support for the extension is advertised. False otherwise.
+ /// </returns>
+ /// <remarks>
+ /// Note that a true or false return value is no guarantee of a Provider's
+ /// support for or lack of support for an extension. The return value is
+ /// determined by how the authenticating user filled out his/her XRDS document only.
+ /// The only way to be sure of support for a given extension is to include
+ /// the extension in the request and see if a response comes back for that extension.
+ /// </remarks>
+ bool IProviderEndpoint.IsExtensionSupported<T>() {
+ throw new NotImplementedException();
+ }
+
+ /// <summary>
+ /// Checks whether the OpenId Identifier claims support for a given extension.
+ /// </summary>
+ /// <param name="extensionType">The extension whose support is being queried.</param>
+ /// <returns>
+ /// True if support for the extension is advertised. False otherwise.
+ /// </returns>
+ /// <remarks>
+ /// Note that a true or false return value is no guarantee of a Provider's
+ /// support for or lack of support for an extension. The return value is
+ /// determined by how the authenticating user filled out his/her XRDS document only.
+ /// The only way to be sure of support for a given extension is to include
+ /// the extension in the request and see if a response comes back for that extension.
+ /// </remarks>
+ bool IProviderEndpoint.IsExtensionSupported(Type extensionType) {
+ Contract.Requires<ArgumentNullException>(extensionType != null);
+ Contract.Requires<ArgumentException>(typeof(IOpenIdMessageExtension).IsAssignableFrom(extensionType));
+ throw new NotImplementedException();
+ }
+
+ #endregion
+ }
+}
diff --git a/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/IRelyingPartyAssociationStore.cs b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/IRelyingPartyAssociationStore.cs
new file mode 100644
index 0000000..21a2c53
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/IRelyingPartyAssociationStore.cs
@@ -0,0 +1,153 @@
+//-----------------------------------------------------------------------
+// <copyright file="IRelyingPartyAssociationStore.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OpenId.RelyingParty {
+ using System;
+ using System.Diagnostics.Contracts;
+
+ /// <summary>
+ /// Stores <see cref="Association"/>s for lookup by their handle, keeping
+ /// associations separated by a given OP Endpoint.
+ /// </summary>
+ /// <remarks>
+ /// Expired associations should be periodically cleared out of an association store.
+ /// This should be done frequently enough to avoid a memory leak, but sparingly enough
+ /// to not be a performance drain. Because this balance can vary by host, it is the
+ /// responsibility of the host to initiate this cleaning.
+ /// </remarks>
+ [ContractClass(typeof(IRelyingPartyAssociationStoreContract))]
+ public interface IRelyingPartyAssociationStore {
+ /// <summary>
+ /// Saves an <see cref="Association"/> for later recall.
+ /// </summary>
+ /// <param name="providerEndpoint">The OP Endpoint with which the association is established.</param>
+ /// <param name="association">The association to store.</param>
+ /// <remarks>
+ /// If the new association conflicts (in OP endpoint and association handle) with an existing association,
+ /// (which should never happen by the way) implementations may overwrite the previously saved association.
+ /// </remarks>
+ void StoreAssociation(Uri providerEndpoint, Association association);
+
+ /// <summary>
+ /// Gets the best association (the one with the longest remaining life) for a given key.
+ /// </summary>
+ /// <param name="providerEndpoint">The OP Endpoint with which the association is established.</param>
+ /// <param name="securityRequirements">The security requirements that the returned association must meet.</param>
+ /// <returns>
+ /// The requested association, or null if no unexpired <see cref="Association"/>s exist for the given key.
+ /// </returns>
+ /// <remarks>
+ /// In the event that multiple associations exist for the given
+ /// <paramref name="providerEndpoint"/>, it is important for the
+ /// implementation for this method to use the <paramref name="securityRequirements"/>
+ /// to pick the best (highest grade or longest living as the host's policy may dictate)
+ /// association that fits the security requirements.
+ /// Associations that are returned that do not meet the security requirements will be
+ /// ignored and a new association created.
+ /// </remarks>
+ Association GetAssociation(Uri providerEndpoint, SecuritySettings securityRequirements);
+
+ /// <summary>
+ /// Gets the association for a given key and handle.
+ /// </summary>
+ /// <param name="providerEndpoint">The OP Endpoint with which the association is established.</param>
+ /// <param name="handle">The handle of the specific association that must be recalled.</param>
+ /// <returns>The requested association, or null if no unexpired <see cref="Association"/>s exist for the given key and handle.</returns>
+ Association GetAssociation(Uri providerEndpoint, string handle);
+
+ /// <summary>Removes a specified handle that may exist in the store.</summary>
+ /// <param name="providerEndpoint">The OP Endpoint with which the association is established.</param>
+ /// <param name="handle">The handle of the specific association that must be deleted.</param>
+ /// <returns>
+ /// Deprecated. The return value is insignificant.
+ /// Previously: True if the association existed in this store previous to this call.
+ /// </returns>
+ /// <remarks>
+ /// No exception should be thrown if the association does not exist in the store
+ /// before this call.
+ /// </remarks>
+ bool RemoveAssociation(Uri providerEndpoint, string handle);
+ }
+
+ /// <summary>
+ /// Code Contract for the <see cref="IRelyingPartyAssociationStore"/> class.
+ /// </summary>
+ [ContractClassFor(typeof(IRelyingPartyAssociationStore))]
+ internal abstract class IRelyingPartyAssociationStoreContract : IRelyingPartyAssociationStore {
+ #region IAssociationStore Members
+
+ /// <summary>
+ /// Saves an <see cref="Association"/> for later recall.
+ /// </summary>
+ /// <param name="providerEndpoint">The Uri (for relying parties) or Smart/Dumb (for providers).</param>
+ /// <param name="association">The association to store.</param>
+ /// <remarks>
+ /// TODO: what should implementations do on association handle conflict?
+ /// </remarks>
+ void IRelyingPartyAssociationStore.StoreAssociation(Uri providerEndpoint, Association association) {
+ Contract.Requires<ArgumentNullException>(providerEndpoint != null);
+ Contract.Requires<ArgumentNullException>(association != null);
+ throw new NotImplementedException();
+ }
+
+ /// <summary>
+ /// Gets the best association (the one with the longest remaining life) for a given key.
+ /// </summary>
+ /// <param name="providerEndpoint">The Uri (for relying parties) or Smart/Dumb (for Providers).</param>
+ /// <param name="securityRequirements">The security requirements that the returned association must meet.</param>
+ /// <returns>
+ /// The requested association, or null if no unexpired <see cref="Association"/>s exist for the given key.
+ /// </returns>
+ /// <remarks>
+ /// In the event that multiple associations exist for the given
+ /// <paramref name="providerEndpoint"/>, it is important for the
+ /// implementation for this method to use the <paramref name="securityRequirements"/>
+ /// to pick the best (highest grade or longest living as the host's policy may dictate)
+ /// association that fits the security requirements.
+ /// Associations that are returned that do not meet the security requirements will be
+ /// ignored and a new association created.
+ /// </remarks>
+ Association IRelyingPartyAssociationStore.GetAssociation(Uri providerEndpoint, SecuritySettings securityRequirements) {
+ Contract.Requires<ArgumentNullException>(providerEndpoint != null);
+ Contract.Requires<ArgumentNullException>(securityRequirements != null);
+ throw new NotImplementedException();
+ }
+
+ /// <summary>
+ /// Gets the association for a given key and handle.
+ /// </summary>
+ /// <param name="providerEndpoint">The Uri (for relying parties) or Smart/Dumb (for Providers).</param>
+ /// <param name="handle">The handle of the specific association that must be recalled.</param>
+ /// <returns>
+ /// The requested association, or null if no unexpired <see cref="Association"/>s exist for the given key and handle.
+ /// </returns>
+ Association IRelyingPartyAssociationStore.GetAssociation(Uri providerEndpoint, string handle) {
+ Contract.Requires<ArgumentNullException>(providerEndpoint != null);
+ Contract.Requires(!String.IsNullOrEmpty(handle));
+ throw new NotImplementedException();
+ }
+
+ /// <summary>
+ /// Removes a specified handle that may exist in the store.
+ /// </summary>
+ /// <param name="providerEndpoint">The Uri (for relying parties) or Smart/Dumb (for Providers).</param>
+ /// <param name="handle">The handle of the specific association that must be deleted.</param>
+ /// <returns>
+ /// True if the association existed in this store previous to this call.
+ /// </returns>
+ /// <remarks>
+ /// No exception should be thrown if the association does not exist in the store
+ /// before this call.
+ /// </remarks>
+ bool IRelyingPartyAssociationStore.RemoveAssociation(Uri providerEndpoint, string handle) {
+ Contract.Requires<ArgumentNullException>(providerEndpoint != null);
+ Contract.Requires(!String.IsNullOrEmpty(handle));
+ throw new NotImplementedException();
+ }
+
+ #endregion
+ }
+}
diff --git a/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/IRelyingPartyBehavior.cs b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/IRelyingPartyBehavior.cs
new file mode 100644
index 0000000..1bfa0db
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/IRelyingPartyBehavior.cs
@@ -0,0 +1,92 @@
+//-----------------------------------------------------------------------
+// <copyright file="IRelyingPartyBehavior.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OpenId.RelyingParty {
+ using System;
+ using System.Diagnostics.Contracts;
+
+ /// <summary>
+ /// Applies a custom security policy to certain OpenID security settings and behaviors.
+ /// </summary>
+ [ContractClass(typeof(IRelyingPartyBehaviorContract))]
+ public interface IRelyingPartyBehavior {
+ /// <summary>
+ /// Applies a well known set of security requirements to a default set of security settings.
+ /// </summary>
+ /// <param name="securitySettings">The security settings to enhance with the requirements of this profile.</param>
+ /// <remarks>
+ /// Care should be taken to never decrease security when applying a profile.
+ /// Profiles should only enhance security requirements to avoid being
+ /// incompatible with each other.
+ /// </remarks>
+ void ApplySecuritySettings(RelyingPartySecuritySettings securitySettings);
+
+ /// <summary>
+ /// Called when an authentication request is about to be sent.
+ /// </summary>
+ /// <param name="request">The request.</param>
+ /// <remarks>
+ /// Implementations should be prepared to be called multiple times on the same outgoing message
+ /// without malfunctioning.
+ /// </remarks>
+ void OnOutgoingAuthenticationRequest(IAuthenticationRequest request);
+
+ /// <summary>
+ /// Called when an incoming positive assertion is received.
+ /// </summary>
+ /// <param name="assertion">The positive assertion.</param>
+ void OnIncomingPositiveAssertion(IAuthenticationResponse assertion);
+ }
+
+ /// <summary>
+ /// Contract class for the <see cref="IRelyingPartyBehavior"/> interface.
+ /// </summary>
+ [ContractClassFor(typeof(IRelyingPartyBehavior))]
+ internal abstract class IRelyingPartyBehaviorContract : IRelyingPartyBehavior {
+ /// <summary>
+ /// Prevents a default instance of the <see cref="IRelyingPartyBehaviorContract"/> class from being created.
+ /// </summary>
+ private IRelyingPartyBehaviorContract() {
+ }
+
+ #region IRelyingPartyBehavior Members
+
+ /// <summary>
+ /// Applies a well known set of security requirements to a default set of security settings.
+ /// </summary>
+ /// <param name="securitySettings">The security settings to enhance with the requirements of this profile.</param>
+ /// <remarks>
+ /// Care should be taken to never decrease security when applying a profile.
+ /// Profiles should only enhance security requirements to avoid being
+ /// incompatible with each other.
+ /// </remarks>
+ void IRelyingPartyBehavior.ApplySecuritySettings(RelyingPartySecuritySettings securitySettings) {
+ Contract.Requires<ArgumentNullException>(securitySettings != null);
+ }
+
+ /// <summary>
+ /// Called when an authentication request is about to be sent.
+ /// </summary>
+ /// <param name="request">The request.</param>
+ /// <remarks>
+ /// Implementations should be prepared to be called multiple times on the same outgoing message
+ /// without malfunctioning.
+ /// </remarks>
+ void IRelyingPartyBehavior.OnOutgoingAuthenticationRequest(IAuthenticationRequest request) {
+ Contract.Requires<ArgumentNullException>(request != null);
+ }
+
+ /// <summary>
+ /// Called when an incoming positive assertion is received.
+ /// </summary>
+ /// <param name="assertion">The positive assertion.</param>
+ void IRelyingPartyBehavior.OnIncomingPositiveAssertion(IAuthenticationResponse assertion) {
+ Contract.Requires<ArgumentNullException>(assertion != null);
+ }
+
+ #endregion
+ }
+}
diff --git a/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/ISetupRequiredAuthenticationResponse.cs b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/ISetupRequiredAuthenticationResponse.cs
new file mode 100644
index 0000000..cfbccef
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/ISetupRequiredAuthenticationResponse.cs
@@ -0,0 +1,51 @@
+//-----------------------------------------------------------------------
+// <copyright file="ISetupRequiredAuthenticationResponse.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OpenId.RelyingParty {
+ using System;
+ using System.Diagnostics.Contracts;
+
+ /// <summary>
+ /// An interface to expose useful properties and functionality for handling
+ /// authentication responses that are returned from Immediate authentication
+ /// requests that require a subsequent request to be made in non-immediate mode.
+ /// </summary>
+ [ContractClass(typeof(ISetupRequiredAuthenticationResponseContract))]
+ public interface ISetupRequiredAuthenticationResponse {
+ /// <summary>
+ /// Gets the <see cref="Identifier"/> to pass to <see cref="OpenIdRelyingParty.CreateRequest(Identifier)"/>
+ /// in a subsequent authentication attempt.
+ /// </summary>
+ Identifier UserSuppliedIdentifier { get; }
+ }
+
+ /// <summary>
+ /// Code contract class for the <see cref="ISetupRequiredAuthenticationResponse"/> type.
+ /// </summary>
+ [ContractClassFor(typeof(ISetupRequiredAuthenticationResponse))]
+ internal abstract class ISetupRequiredAuthenticationResponseContract : ISetupRequiredAuthenticationResponse {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ISetupRequiredAuthenticationResponseContract"/> class.
+ /// </summary>
+ protected ISetupRequiredAuthenticationResponseContract() {
+ }
+
+ #region ISetupRequiredAuthenticationResponse Members
+
+ /// <summary>
+ /// Gets the <see cref="Identifier"/> to pass to <see cref="OpenIdRelyingParty.CreateRequest(Identifier)"/>
+ /// in a subsequent authentication attempt.
+ /// </summary>
+ Identifier ISetupRequiredAuthenticationResponse.UserSuppliedIdentifier {
+ get {
+ Contract.Requires<InvalidOperationException>(((IAuthenticationResponse)this).Status == AuthenticationStatus.SetupRequired, OpenIdStrings.OperationOnlyValidForSetupRequiredState);
+ throw new System.NotImplementedException();
+ }
+ }
+
+ #endregion
+ }
+}
diff --git a/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/NegativeAuthenticationResponse.cs b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/NegativeAuthenticationResponse.cs
new file mode 100644
index 0000000..9e3824d
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/NegativeAuthenticationResponse.cs
@@ -0,0 +1,312 @@
+//-----------------------------------------------------------------------
+// <copyright file="NegativeAuthenticationResponse.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OpenId.RelyingParty {
+ using System;
+ using System.Collections.Generic;
+ using System.Diagnostics.Contracts;
+ using System.Linq;
+ using System.Web;
+ using DotNetOpenAuth.Messaging;
+ using DotNetOpenAuth.OpenId.Messages;
+
+ /// <summary>
+ /// Wraps a negative assertion response in an <see cref="IAuthenticationResponse"/> instance
+ /// for public consumption by the host web site.
+ /// </summary>
+ internal class NegativeAuthenticationResponse : IAuthenticationResponse, ISetupRequiredAuthenticationResponse {
+ /// <summary>
+ /// The negative assertion message that was received by the RP that was used
+ /// to create this instance.
+ /// </summary>
+ private readonly NegativeAssertionResponse response;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="NegativeAuthenticationResponse"/> class.
+ /// </summary>
+ /// <param name="response">The negative assertion response received by the Relying Party.</param>
+ internal NegativeAuthenticationResponse(NegativeAssertionResponse response) {
+ Contract.Requires<ArgumentNullException>(response != null);
+ this.response = response;
+
+ Reporting.RecordEventOccurrence(this, string.Empty);
+ }
+
+ #region IAuthenticationResponse Properties
+
+ /// <summary>
+ /// Gets the Identifier that the end user claims to own. For use with user database storage and lookup.
+ /// May be null for some failed authentications (i.e. failed directed identity authentications).
+ /// </summary>
+ /// <value></value>
+ /// <remarks>
+ /// <para>
+ /// This is the secure identifier that should be used for database storage and lookup.
+ /// It is not always friendly (i.e. =Arnott becomes =!9B72.7DD1.50A9.5CCD), but it protects
+ /// user identities against spoofing and other attacks.
+ /// </para>
+ /// <para>
+ /// For user-friendly identifiers to display, use the
+ /// <see cref="FriendlyIdentifierForDisplay"/> property.
+ /// </para>
+ /// </remarks>
+ public Identifier ClaimedIdentifier {
+ get { return null; }
+ }
+
+ /// <summary>
+ /// Gets a user-friendly OpenID Identifier for display purposes ONLY.
+ /// </summary>
+ /// <value></value>
+ /// <remarks>
+ /// <para>
+ /// This <i>should</i> be put through <see cref="HttpUtility.HtmlEncode(string)"/> before
+ /// sending to a browser to secure against javascript injection attacks.
+ /// </para>
+ /// <para>
+ /// This property retains some aspects of the user-supplied identifier that get lost
+ /// in the <see cref="ClaimedIdentifier"/>. For example, XRIs used as user-supplied
+ /// identifiers (i.e. =Arnott) become unfriendly unique strings (i.e. =!9B72.7DD1.50A9.5CCD).
+ /// For display purposes, such as text on a web page that says "You're logged in as ...",
+ /// this property serves to provide the =Arnott string, or whatever else is the most friendly
+ /// string close to what the user originally typed in.
+ /// </para>
+ /// <para>
+ /// If the user-supplied identifier is a URI, this property will be the URI after all
+ /// redirects, and with the protocol and fragment trimmed off.
+ /// If the user-supplied identifier is an XRI, this property will be the original XRI.
+ /// If the user-supplied identifier is an OpenID Provider identifier (i.e. yahoo.com),
+ /// this property will be the Claimed Identifier, with the protocol stripped if it is a URI.
+ /// </para>
+ /// <para>
+ /// It is <b>very</b> important that this property <i>never</i> be used for database storage
+ /// or lookup to avoid identity spoofing and other security risks. For database storage
+ /// and lookup please use the <see cref="ClaimedIdentifier"/> property.
+ /// </para>
+ /// </remarks>
+ public string FriendlyIdentifierForDisplay {
+ get { return null; }
+ }
+
+ /// <summary>
+ /// Gets the detailed success or failure status of the authentication attempt.
+ /// </summary>
+ /// <value></value>
+ public AuthenticationStatus Status {
+ get { return this.response.Immediate ? AuthenticationStatus.SetupRequired : AuthenticationStatus.Canceled; }
+ }
+
+ /// <summary>
+ /// Gets information about the OpenId Provider, as advertised by the
+ /// OpenID discovery documents found at the <see cref="ClaimedIdentifier"/>
+ /// location.
+ /// </summary>
+ /// <value>
+ /// The Provider endpoint that issued the positive assertion;
+ /// or <c>null</c> if information about the Provider is unavailable.
+ /// </value>
+ public IProviderEndpoint Provider {
+ get { return null; }
+ }
+
+ /// <summary>
+ /// Gets the details regarding a failed authentication attempt, if available.
+ /// This will be set if and only if <see cref="Status"/> is <see cref="AuthenticationStatus.Failed"/>.
+ /// </summary>
+ /// <value></value>
+ public Exception Exception {
+ get { return null; }
+ }
+
+ #endregion
+
+ #region ISetupRequiredAuthenticationResponse Members
+
+ /// <summary>
+ /// Gets the <see cref="Identifier"/> to pass to <see cref="OpenIdRelyingParty.CreateRequest(Identifier)"/>
+ /// in a subsequent authentication attempt.
+ /// </summary>
+ /// <value></value>
+ public Identifier UserSuppliedIdentifier {
+ get {
+ string userSuppliedIdentifier;
+ this.response.ExtraData.TryGetValue(AuthenticationRequest.UserSuppliedIdentifierParameterName, out userSuppliedIdentifier);
+ return userSuppliedIdentifier;
+ }
+ }
+
+ #endregion
+
+ #region IAuthenticationResponse Methods
+
+ /// <summary>
+ /// Gets a callback argument's value that was previously added using
+ /// <see cref="IAuthenticationRequest.AddCallbackArguments(string, string)"/>.
+ /// </summary>
+ /// <param name="key">The name of the parameter whose value is sought.</param>
+ /// <returns>
+ /// The value of the argument, or null if the named parameter could not be found.
+ /// </returns>
+ /// <remarks>
+ /// <para>This may return any argument on the querystring that came with the authentication response,
+ /// which may include parameters not explicitly added using
+ /// <see cref="IAuthenticationRequest.AddCallbackArguments(string, string)"/>.</para>
+ /// <para>Note that these values are NOT protected against tampering in transit.</para>
+ /// </remarks>
+ public string GetCallbackArgument(string key) {
+ return null;
+ }
+
+ /// <summary>
+ /// Gets a callback argument's value that was previously added using
+ /// <see cref="IAuthenticationRequest.AddCallbackArguments(string, string)"/>.
+ /// </summary>
+ /// <param name="key">The name of the parameter whose value is sought.</param>
+ /// <returns>
+ /// The value of the argument, or null if the named parameter could not be found.
+ /// </returns>
+ /// <remarks>
+ /// Callback parameters are only available even if the RP is in stateless mode,
+ /// or the callback parameters are otherwise unverifiable as untampered with.
+ /// Therefore, use this method only when the callback argument is not to be
+ /// used to make a security-sensitive decision.
+ /// </remarks>
+ public string GetUntrustedCallbackArgument(string key) {
+ return null;
+ }
+
+ /// <summary>
+ /// Gets all the callback arguments that were previously added using
+ /// <see cref="IAuthenticationRequest.AddCallbackArguments(string, string)"/> or as a natural part
+ /// of the return_to URL.
+ /// </summary>
+ /// <returns>A name-value dictionary. Never null.</returns>
+ /// <remarks>
+ /// <para>This MAY return any argument on the querystring that came with the authentication response,
+ /// which may include parameters not explicitly added using
+ /// <see cref="IAuthenticationRequest.AddCallbackArguments(string, string)"/>.</para>
+ /// <para>Note that these values are NOT protected against tampering in transit.</para>
+ /// </remarks>
+ public IDictionary<string, string> GetCallbackArguments() {
+ return EmptyDictionary<string, string>.Instance;
+ }
+
+ /// <summary>
+ /// Gets all the callback arguments that were previously added using
+ /// <see cref="IAuthenticationRequest.AddCallbackArguments(string, string)"/> or as a natural part
+ /// of the return_to URL.
+ /// </summary>
+ /// <returns>A name-value dictionary. Never null.</returns>
+ /// <remarks>
+ /// Callback parameters are only available even if the RP is in stateless mode,
+ /// or the callback parameters are otherwise unverifiable as untampered with.
+ /// Therefore, use this method only when the callback argument is not to be
+ /// used to make a security-sensitive decision.
+ /// </remarks>
+ public IDictionary<string, string> GetUntrustedCallbackArguments() {
+ return EmptyDictionary<string, string>.Instance;
+ }
+
+ /// <summary>
+ /// Tries to get an OpenID extension that may be present in the response.
+ /// </summary>
+ /// <typeparam name="T">The type of extension to look for in the response message.</typeparam>
+ /// <returns>
+ /// The extension, if it is found. Null otherwise.
+ /// </returns>
+ /// <remarks>
+ /// <para>Extensions are returned only if the Provider signed them.
+ /// Relying parties that do not care if the values were modified in
+ /// transit should use the <see cref="GetUntrustedExtension&lt;T&gt;"/> method
+ /// in order to allow the Provider to not sign the extension. </para>
+ /// <para>Unsigned extensions are completely unreliable and should be
+ /// used only to prefill user forms since the user or any other third
+ /// party may have tampered with the data carried by the extension.</para>
+ /// <para>Signed extensions are only reliable if the relying party
+ /// trusts the OpenID Provider that signed them. Signing does not mean
+ /// the relying party can trust the values -- it only means that the values
+ /// have not been tampered with since the Provider sent the message.</para>
+ /// </remarks>
+ public T GetExtension<T>() where T : IOpenIdMessageExtension {
+ return default(T);
+ }
+
+ /// <summary>
+ /// Tries to get an OpenID extension that may be present in the response.
+ /// </summary>
+ /// <param name="extensionType">Type of the extension to look for in the response.</param>
+ /// <returns>
+ /// The extension, if it is found. Null otherwise.
+ /// </returns>
+ /// <remarks>
+ /// <para>Extensions are returned only if the Provider signed them.
+ /// Relying parties that do not care if the values were modified in
+ /// transit should use the <see cref="GetUntrustedExtension"/> method
+ /// in order to allow the Provider to not sign the extension. </para>
+ /// <para>Unsigned extensions are completely unreliable and should be
+ /// used only to prefill user forms since the user or any other third
+ /// party may have tampered with the data carried by the extension.</para>
+ /// <para>Signed extensions are only reliable if the relying party
+ /// trusts the OpenID Provider that signed them. Signing does not mean
+ /// the relying party can trust the values -- it only means that the values
+ /// have not been tampered with since the Provider sent the message.</para>
+ /// </remarks>
+ public IOpenIdMessageExtension GetExtension(Type extensionType) {
+ return null;
+ }
+
+ /// <summary>
+ /// Tries to get an OpenID extension that may be present in the response, without
+ /// requiring it to be signed by the Provider.
+ /// </summary>
+ /// <typeparam name="T">The type of extension to look for in the response message.</typeparam>
+ /// <returns>
+ /// The extension, if it is found. Null otherwise.
+ /// </returns>
+ /// <remarks>
+ /// <para>Extensions are returned whether they are signed or not.
+ /// Use the <see cref="GetExtension&lt;T&gt;"/> method to retrieve
+ /// extension responses only if they are signed by the Provider to
+ /// protect against tampering. </para>
+ /// <para>Unsigned extensions are completely unreliable and should be
+ /// used only to prefill user forms since the user or any other third
+ /// party may have tampered with the data carried by the extension.</para>
+ /// <para>Signed extensions are only reliable if the relying party
+ /// trusts the OpenID Provider that signed them. Signing does not mean
+ /// the relying party can trust the values -- it only means that the values
+ /// have not been tampered with since the Provider sent the message.</para>
+ /// </remarks>
+ public T GetUntrustedExtension<T>() where T : IOpenIdMessageExtension {
+ return this.response.Extensions.OfType<T>().FirstOrDefault();
+ }
+
+ /// <summary>
+ /// Tries to get an OpenID extension that may be present in the response.
+ /// </summary>
+ /// <param name="extensionType">Type of the extension to look for in the response.</param>
+ /// <returns>
+ /// The extension, if it is found. Null otherwise.
+ /// </returns>
+ /// <remarks>
+ /// <para>Extensions are returned whether they are signed or not.
+ /// Use the <see cref="GetExtension"/> method to retrieve
+ /// extension responses only if they are signed by the Provider to
+ /// protect against tampering. </para>
+ /// <para>Unsigned extensions are completely unreliable and should be
+ /// used only to prefill user forms since the user or any other third
+ /// party may have tampered with the data carried by the extension.</para>
+ /// <para>Signed extensions are only reliable if the relying party
+ /// trusts the OpenID Provider that signed them. Signing does not mean
+ /// the relying party can trust the values -- it only means that the values
+ /// have not been tampered with since the Provider sent the message.</para>
+ /// </remarks>
+ public IOpenIdMessageExtension GetUntrustedExtension(Type extensionType) {
+ return this.response.Extensions.OfType<IOpenIdMessageExtension>().Where(ext => extensionType.IsInstanceOfType(ext)).FirstOrDefault();
+ }
+
+ #endregion
+ }
+}
diff --git a/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/OpenIdAjaxRelyingParty.cs b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/OpenIdAjaxRelyingParty.cs
new file mode 100644
index 0000000..42bfbde
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/OpenIdAjaxRelyingParty.cs
@@ -0,0 +1,242 @@
+//-----------------------------------------------------------------------
+// <copyright file="OpenIdAjaxRelyingParty.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OpenId.RelyingParty {
+ using System;
+ using System.Collections.Generic;
+ using System.Diagnostics.CodeAnalysis;
+ using System.Diagnostics.Contracts;
+ using System.Globalization;
+ using System.Linq;
+ using System.Net;
+ using System.Net.Mime;
+ using System.Text;
+ using System.Web;
+ using System.Web.Script.Serialization;
+
+ using DotNetOpenAuth.Configuration;
+ using DotNetOpenAuth.Messaging;
+ using DotNetOpenAuth.OpenId.Extensions;
+ using DotNetOpenAuth.OpenId.Extensions.UI;
+
+ /// <summary>
+ /// Provides the programmatic facilities to act as an AJAX-enabled OpenID relying party.
+ /// </summary>
+ public class OpenIdAjaxRelyingParty : OpenIdRelyingParty {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="OpenIdAjaxRelyingParty"/> class.
+ /// </summary>
+ public OpenIdAjaxRelyingParty() {
+ Reporting.RecordFeatureUse(this);
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="OpenIdAjaxRelyingParty"/> class.
+ /// </summary>
+ /// <param name="applicationStore">The application store. If <c>null</c>, the relying party will always operate in "dumb mode".</param>
+ public OpenIdAjaxRelyingParty(IOpenIdApplicationStore applicationStore)
+ : base(applicationStore) {
+ Reporting.RecordFeatureUse(this);
+ }
+
+ /// <summary>
+ /// Generates AJAX-ready authentication requests that can satisfy the requirements of some OpenID Identifier.
+ /// </summary>
+ /// <param name="userSuppliedIdentifier">The Identifier supplied by the user. This may be a URL, an XRI or i-name.</param>
+ /// <param name="realm">The shorest URL that describes this relying party web site's address.
+ /// For example, if your login page is found at https://www.example.com/login.aspx,
+ /// your realm would typically be https://www.example.com/.</param>
+ /// <param name="returnToUrl">The URL of the login page, or the page prepared to receive authentication
+ /// responses from the OpenID Provider.</param>
+ /// <returns>
+ /// A sequence of authentication requests, any of which constitutes a valid identity assertion on the Claimed Identifier.
+ /// Never null, but may be empty.
+ /// </returns>
+ /// <remarks>
+ /// <para>Any individual generated request can satisfy the authentication.
+ /// The generated requests are sorted in preferred order.
+ /// Each request is generated as it is enumerated to. Associations are created only as
+ /// <see cref="IAuthenticationRequest.RedirectingResponse"/> is called.</para>
+ /// <para>No exception is thrown if no OpenID endpoints were discovered.
+ /// An empty enumerable is returned instead.</para>
+ /// </remarks>
+ public override IEnumerable<IAuthenticationRequest> CreateRequests(Identifier userSuppliedIdentifier, Realm realm, Uri returnToUrl) {
+ var requests = base.CreateRequests(userSuppliedIdentifier, realm, returnToUrl);
+
+ // Alter the requests so that have AJAX characteristics.
+ // 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.
+ int reqIndex = 0;
+ foreach (var req in requests) {
+ // Inform ourselves in return_to that we're in a popup.
+ req.SetUntrustedCallbackArgument(OpenIdRelyingPartyControlBase.UIPopupCallbackKey, "1");
+
+ if (req.DiscoveryResult.IsExtensionSupported<UIRequest>()) {
+ // Inform the OP that we'll be using a popup window consistent with the UI extension.
+ req.AddExtension(new UIRequest());
+
+ // Provide a hint for the client javascript about whether the OP supports the UI extension.
+ // This is so the window can be made the correct size for the extension.
+ // If the OP doesn't advertise support for the extension, the javascript will use
+ // a bigger popup window.
+ req.SetUntrustedCallbackArgument(OpenIdRelyingPartyControlBase.PopupUISupportedJSHint, "1");
+ }
+
+ req.SetUntrustedCallbackArgument("index", (reqIndex++).ToString(CultureInfo.InvariantCulture));
+
+ // If the ReturnToUrl was explicitly set, we'll need to reset our first parameter
+ if (OpenIdElement.Configuration.RelyingParty.PreserveUserSuppliedIdentifier) {
+ if (string.IsNullOrEmpty(HttpUtility.ParseQueryString(req.ReturnToUrl.Query)[AuthenticationRequest.UserSuppliedIdentifierParameterName])) {
+ req.SetUntrustedCallbackArgument(AuthenticationRequest.UserSuppliedIdentifierParameterName, userSuppliedIdentifier.OriginalString);
+ }
+ }
+
+ // Our javascript needs to let the user know which endpoint responded. So we force it here.
+ // This gives us the info even for 1.0 OPs and 2.0 setup_required responses.
+ req.SetUntrustedCallbackArgument(OpenIdRelyingPartyAjaxControlBase.OPEndpointParameterName, req.Provider.Uri.AbsoluteUri);
+ req.SetUntrustedCallbackArgument(OpenIdRelyingPartyAjaxControlBase.ClaimedIdParameterName, (string)req.ClaimedIdentifier ?? string.Empty);
+
+ // Inform ourselves in return_to that we're in a popup or iframe.
+ req.SetUntrustedCallbackArgument(OpenIdRelyingPartyAjaxControlBase.UIPopupCallbackKey, "1");
+
+ // We append a # at the end so that if the OP happens to support it,
+ // the OpenID response "query string" is appended after the hash rather than before, resulting in the
+ // 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>
+ /// Serializes discovery results on some <i>single</i> identifier on behalf of Javascript running on the browser.
+ /// </summary>
+ /// <param name="requests">The discovery results from just <i>one</i> identifier to serialize as a JSON response.</param>
+ /// <returns>
+ /// The JSON result to return to the user agent.
+ /// </returns>
+ /// <remarks>
+ /// We prepare a JSON object with this interface:
+ /// <code>
+ /// class jsonResponse {
+ /// string claimedIdentifier;
+ /// Array requests; // never null
+ /// string error; // null if no error
+ /// }
+ /// </code>
+ /// Each element in the requests array looks like this:
+ /// <code>
+ /// 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.
+ /// }
+ /// </code>
+ /// </remarks>
+ public OutgoingWebResponse AsAjaxDiscoveryResult(IEnumerable<IAuthenticationRequest> requests) {
+ Contract.Requires<ArgumentNullException>(requests != null);
+
+ var serializer = new JavaScriptSerializer();
+ return new OutgoingWebResponse {
+ Body = serializer.Serialize(this.AsJsonDiscoveryResult(requests)),
+ };
+ }
+
+ /// <summary>
+ /// Serializes discovery on a set of identifiers for preloading into an HTML page that carries
+ /// an AJAX-aware OpenID control.
+ /// </summary>
+ /// <param name="requests">The discovery results to serialize as a JSON response.</param>
+ /// <returns>
+ /// The JSON result to return to the user agent.
+ /// </returns>
+ public string AsAjaxPreloadedDiscoveryResult(IEnumerable<IAuthenticationRequest> requests) {
+ Contract.Requires<ArgumentNullException>(requests != null);
+
+ var serializer = new JavaScriptSerializer();
+ string json = serializer.Serialize(this.AsJsonPreloadedDiscoveryResult(requests));
+
+ string script = "window.dnoa_internal.loadPreloadedDiscoveryResults(" + json + ");";
+ return script;
+ }
+
+ /// <summary>
+ /// Converts a sequence of authentication requests to a JSON object for seeding an AJAX-enabled login page.
+ /// </summary>
+ /// <param name="requests">The discovery results from just <i>one</i> identifier to serialize as a JSON response.</param>
+ /// <returns>A JSON object, not yet serialized.</returns>
+ internal object AsJsonDiscoveryResult(IEnumerable<IAuthenticationRequest> requests) {
+ Contract.Requires<ArgumentNullException>(requests != null);
+
+ requests = requests.CacheGeneratedResults();
+
+ if (requests.Any()) {
+ return new {
+ claimedIdentifier = (string)requests.First().ClaimedIdentifier,
+ requests = requests.Select(req => new {
+ endpoint = req.Provider.Uri.AbsoluteUri,
+ immediate = this.GetRedirectUrl(req, true),
+ setup = this.GetRedirectUrl(req, false),
+ }).ToArray()
+ };
+ } else {
+ return new {
+ requests = new object[0],
+ error = OpenIdStrings.OpenIdEndpointNotFound,
+ };
+ }
+ }
+
+ /// <summary>
+ /// Serializes discovery on a set of identifiers for preloading into an HTML page that carries
+ /// an AJAX-aware OpenID control.
+ /// </summary>
+ /// <param name="requests">The discovery results to serialize as a JSON response.</param>
+ /// <returns>
+ /// A JSON object, not yet serialized to a string.
+ /// </returns>
+ private object AsJsonPreloadedDiscoveryResult(IEnumerable<IAuthenticationRequest> requests) {
+ Contract.Requires<ArgumentNullException>(requests != null);
+
+ // We prepare a JSON object with this interface:
+ // Array discoveryWrappers;
+ // Where each element in the above array has this interface:
+ // class discoveryWrapper {
+ // string userSuppliedIdentifier;
+ // jsonResponse discoveryResult; // contains result of call to SerializeDiscoveryAsJson(Identifier)
+ // }
+ var json = (from request in requests
+ group request by request.DiscoveryResult.UserSuppliedIdentifier into requestsByIdentifier
+ select new {
+ userSuppliedIdentifier = (string)requestsByIdentifier.Key,
+ discoveryResult = this.AsJsonDiscoveryResult(requestsByIdentifier),
+ }).ToArray();
+
+ return json;
+ }
+
+ /// <summary>
+ /// Gets the full URL that carries an OpenID message, even if it exceeds the normal maximum size of a URL,
+ /// for purposes of sending to an AJAX component running in the browser.
+ /// </summary>
+ /// <param name="request">The authentication request.</param>
+ /// <param name="immediate"><c>true</c>to create a checkid_immediate request;
+ /// <c>false</c> to create a checkid_setup request.</param>
+ /// <returns>The absolute URL that carries the entire OpenID message.</returns>
+ private Uri GetRedirectUrl(IAuthenticationRequest request, bool immediate) {
+ Contract.Requires<ArgumentNullException>(request != null);
+
+ request.Mode = immediate ? AuthenticationRequestMode.Immediate : AuthenticationRequestMode.Setup;
+ return request.RedirectingResponse.GetDirectUriRequest(this.Channel);
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/OpenIdAjaxTextBox.cs b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/OpenIdAjaxTextBox.cs
new file mode 100644
index 0000000..8be097f
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/OpenIdAjaxTextBox.cs
@@ -0,0 +1,877 @@
+//-----------------------------------------------------------------------
+// <copyright file="OpenIdAjaxTextBox.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+[assembly: System.Web.UI.WebResource(DotNetOpenAuth.OpenId.RelyingParty.OpenIdAjaxTextBox.EmbeddedScriptResourceName, "text/javascript")]
+[assembly: System.Web.UI.WebResource(DotNetOpenAuth.OpenId.RelyingParty.OpenIdAjaxTextBox.EmbeddedStylesheetResourceName, "text/css")]
+[assembly: System.Web.UI.WebResource(DotNetOpenAuth.OpenId.RelyingParty.OpenIdAjaxTextBox.EmbeddedSpinnerResourceName, "image/gif")]
+[assembly: System.Web.UI.WebResource(DotNetOpenAuth.OpenId.RelyingParty.OpenIdAjaxTextBox.EmbeddedLoginSuccessResourceName, "image/png")]
+[assembly: System.Web.UI.WebResource(DotNetOpenAuth.OpenId.RelyingParty.OpenIdAjaxTextBox.EmbeddedLoginFailureResourceName, "image/png")]
+
+#pragma warning disable 0809 // marking inherited, unsupported properties as obsolete to discourage their use
+
+namespace DotNetOpenAuth.OpenId.RelyingParty {
+ using System;
+ using System.Collections.Generic;
+ using System.Collections.Specialized;
+ using System.ComponentModel;
+ using System.Diagnostics;
+ using System.Diagnostics.CodeAnalysis;
+ using System.Diagnostics.Contracts;
+ using System.Drawing.Design;
+ using System.Globalization;
+ using System.Text;
+ using System.Web.UI;
+ using System.Web.UI.HtmlControls;
+ using DotNetOpenAuth.Messaging;
+
+ /// <summary>
+ /// An ASP.NET control that provides a minimal text box that is OpenID-aware and uses AJAX for
+ /// a premium login experience.
+ /// </summary>
+ [DefaultProperty("Text"), ValidationProperty("Text")]
+ [ToolboxData("<{0}:OpenIdAjaxTextBox runat=\"server\" />")]
+ public class OpenIdAjaxTextBox : OpenIdRelyingPartyAjaxControlBase, IEditableTextControl, ITextControl, IPostBackDataHandler {
+ /// <summary>
+ /// The name of the manifest stream containing the OpenIdAjaxTextBox.js file.
+ /// </summary>
+ internal const string EmbeddedScriptResourceName = Util.DefaultNamespace + ".OpenId.RelyingParty.OpenIdAjaxTextBox.js";
+
+ /// <summary>
+ /// The name of the manifest stream containing the OpenIdAjaxTextBox.css file.
+ /// </summary>
+ internal const string EmbeddedStylesheetResourceName = Util.DefaultNamespace + ".OpenId.RelyingParty.OpenIdAjaxTextBox.css";
+
+ /// <summary>
+ /// The name of the manifest stream containing the spinner.gif file.
+ /// </summary>
+ internal const string EmbeddedSpinnerResourceName = Util.DefaultNamespace + ".OpenId.RelyingParty.spinner.gif";
+
+ /// <summary>
+ /// The name of the manifest stream containing the login_success.png file.
+ /// </summary>
+ internal const string EmbeddedLoginSuccessResourceName = Util.DefaultNamespace + ".OpenId.RelyingParty.login_success.png";
+
+ /// <summary>
+ /// The name of the manifest stream containing the login_failure.png file.
+ /// </summary>
+ internal const string EmbeddedLoginFailureResourceName = Util.DefaultNamespace + ".OpenId.RelyingParty.login_failure.png";
+
+ /// <summary>
+ /// The default value for the <see cref="DownloadYahooUILibrary"/> property.
+ /// </summary>
+ internal const bool DownloadYahooUILibraryDefault = true;
+
+ /// <summary>
+ /// The default value for the <see cref="Throttle"/> property.
+ /// </summary>
+ internal const int ThrottleDefault = 3;
+
+ #region Property viewstate keys
+
+ /// <summary>
+ /// The viewstate key to use for storing the value of the <see cref="AutoPostBack"/> property.
+ /// </summary>
+ private const string AutoPostBackViewStateKey = "AutoPostback";
+
+ /// <summary>
+ /// The viewstate key to use for the <see cref="Text"/> property.
+ /// </summary>
+ private const string TextViewStateKey = "Text";
+
+ /// <summary>
+ /// The viewstate key to use for storing the value of the <see cref="Columns"/> property.
+ /// </summary>
+ private const string ColumnsViewStateKey = "Columns";
+
+ /// <summary>
+ /// The viewstate key to use for the <see cref="CssClass"/> property.
+ /// </summary>
+ private const string CssClassViewStateKey = "CssClass";
+
+ /// <summary>
+ /// The viewstate key to use for storing the value of the <see cref="OnClientAssertionReceived"/> property.
+ /// </summary>
+ private const string OnClientAssertionReceivedViewStateKey = "OnClientAssertionReceived";
+
+ /// <summary>
+ /// The viewstate key to use for storing the value of the <see cref="AuthenticatedAsToolTip"/> property.
+ /// </summary>
+ private const string AuthenticatedAsToolTipViewStateKey = "AuthenticatedAsToolTip";
+
+ /// <summary>
+ /// The viewstate key to use for storing the value of the <see cref="AuthenticationSucceededToolTip"/> property.
+ /// </summary>
+ private const string AuthenticationSucceededToolTipViewStateKey = "AuthenticationSucceededToolTip";
+
+ /// <summary>
+ /// The viewstate key to use for storing the value of the <see cref="LogOnInProgressMessage"/> property.
+ /// </summary>
+ private const string LogOnInProgressMessageViewStateKey = "BusyToolTip";
+
+ /// <summary>
+ /// The viewstate key to use for storing the value of the <see cref="AuthenticationFailedToolTip"/> property.
+ /// </summary>
+ private const string AuthenticationFailedToolTipViewStateKey = "AuthenticationFailedToolTip";
+
+ /// <summary>
+ /// The viewstate key to use for storing the value of the <see cref="IdentifierRequiredMessage"/> property.
+ /// </summary>
+ private const string IdentifierRequiredMessageViewStateKey = "BusyToolTip";
+
+ /// <summary>
+ /// The viewstate key to use for storing the value of the <see cref="BusyToolTip"/> property.
+ /// </summary>
+ private const string BusyToolTipViewStateKey = "BusyToolTip";
+
+ /// <summary>
+ /// The viewstate key to use for storing the value of the <see cref="LogOnText"/> property.
+ /// </summary>
+ private const string LogOnTextViewStateKey = "LoginText";
+
+ /// <summary>
+ /// The viewstate key to use for storing the value of the <see cref="Throttle"/> property.
+ /// </summary>
+ private const string ThrottleViewStateKey = "Throttle";
+
+ /// <summary>
+ /// The viewstate key to use for storing the value of the <see cref="LogOnToolTip"/> property.
+ /// </summary>
+ private const string LogOnToolTipViewStateKey = "LoginToolTip";
+
+ /// <summary>
+ /// The viewstate key to use for storing the value of the <see cref="LogOnPostBackToolTip"/> property.
+ /// </summary>
+ private const string LogOnPostBackToolTipViewStateKey = "LoginPostBackToolTip";
+
+ /// <summary>
+ /// The viewstate key to use for storing the value of the <see cref="Name"/> property.
+ /// </summary>
+ private const string NameViewStateKey = "Name";
+
+ /// <summary>
+ /// The viewstate key to use for storing the value of the <see cref="Timeout"/> property.
+ /// </summary>
+ private const string TimeoutViewStateKey = "Timeout";
+
+ /// <summary>
+ /// The viewstate key to use for storing the value of the <see cref="TabIndex"/> property.
+ /// </summary>
+ private const string TabIndexViewStateKey = "TabIndex";
+
+ /// <summary>
+ /// The viewstate key to use for the <see cref="Enabled"/> property.
+ /// </summary>
+ private const string EnabledViewStateKey = "Enabled";
+
+ /// <summary>
+ /// The viewstate key to use for storing the value of the <see cref="RetryToolTip"/> property.
+ /// </summary>
+ private const string RetryToolTipViewStateKey = "RetryToolTip";
+
+ /// <summary>
+ /// The viewstate key to use for storing the value of the <see cref="RetryText"/> property.
+ /// </summary>
+ private const string RetryTextViewStateKey = "RetryText";
+
+ /// <summary>
+ /// The viewstate key to use for storing the value of the <see cref="DownloadYahooUILibrary"/> property.
+ /// </summary>
+ private const string DownloadYahooUILibraryViewStateKey = "DownloadYahooUILibrary";
+
+ /// <summary>
+ /// The viewstate key to use for storing the value of the <see cref="ShowLogOnPostBackButton"/> property.
+ /// </summary>
+ private const string ShowLogOnPostBackButtonViewStateKey = "ShowLogOnPostBackButton";
+
+ #endregion
+
+ #region Property defaults
+
+ /// <summary>
+ /// The default value for the <see cref="AutoPostBack"/> property.
+ /// </summary>
+ private const bool AutoPostBackDefault = false;
+
+ /// <summary>
+ /// The default value for the <see cref="Columns"/> property.
+ /// </summary>
+ private const int ColumnsDefault = 40;
+
+ /// <summary>
+ /// The default value for the <see cref="CssClass"/> property.
+ /// </summary>
+ private const string CssClassDefault = "openid";
+
+ /// <summary>
+ /// The default value for the <see cref="LogOnInProgressMessage"/> property.
+ /// </summary>
+ private const string LogOnInProgressMessageDefault = "Please wait for login to complete.";
+
+ /// <summary>
+ /// The default value for the <see cref="AuthenticationSucceededToolTip"/> property.
+ /// </summary>
+ private const string AuthenticationSucceededToolTipDefault = "Authenticated by {0}.";
+
+ /// <summary>
+ /// The default value for the <see cref="AuthenticatedAsToolTip"/> property.
+ /// </summary>
+ private const string AuthenticatedAsToolTipDefault = "Authenticated as {0}.";
+
+ /// <summary>
+ /// The default value for the <see cref="AuthenticationFailedToolTip"/> property.
+ /// </summary>
+ private const string AuthenticationFailedToolTipDefault = "Authentication failed.";
+
+ /// <summary>
+ /// The default value for the <see cref="LogOnText"/> property.
+ /// </summary>
+ private const string LogOnTextDefault = "LOG IN";
+
+ /// <summary>
+ /// The default value for the <see cref="BusyToolTip"/> property.
+ /// </summary>
+ private const string BusyToolTipDefault = "Discovering/authenticating";
+
+ /// <summary>
+ /// The default value for the <see cref="IdentifierRequiredMessage"/> property.
+ /// </summary>
+ private const string IdentifierRequiredMessageDefault = "Please correct errors in OpenID identifier and allow login to complete before submitting.";
+
+ /// <summary>
+ /// The default value for the <see cref="Name"/> property.
+ /// </summary>
+ private const string NameDefault = "openid_identifier";
+
+ /// <summary>
+ /// Default value for <see cref="TabIndex"/> property.
+ /// </summary>
+ private const short TabIndexDefault = 0;
+
+ /// <summary>
+ /// The default value for the <see cref="RetryToolTip"/> property.
+ /// </summary>
+ private const string RetryToolTipDefault = "Retry a failed identifier discovery.";
+
+ /// <summary>
+ /// The default value for the <see cref="LogOnToolTip"/> property.
+ /// </summary>
+ private const string LogOnToolTipDefault = "Click here to log in using a pop-up window.";
+
+ /// <summary>
+ /// The default value for the <see cref="LogOnPostBackToolTip"/> property.
+ /// </summary>
+ private const string LogOnPostBackToolTipDefault = "Click here to log in immediately.";
+
+ /// <summary>
+ /// The default value for the <see cref="RetryText"/> property.
+ /// </summary>
+ private const string RetryTextDefault = "RETRY";
+
+ /// <summary>
+ /// The default value for the <see cref="ShowLogOnPostBackButton"/> property.
+ /// </summary>
+ private const bool ShowLogOnPostBackButtonDefault = false;
+
+ #endregion
+
+ /// <summary>
+ /// The path where the YUI control library should be downloaded from for HTTP pages.
+ /// </summary>
+ private const string YuiLoaderHttp = "http://ajax.googleapis.com/ajax/libs/yui/2.8.0r4/build/yuiloader/yuiloader-min.js";
+
+ /// <summary>
+ /// The path where the YUI control library should be downloaded from for HTTPS pages.
+ /// </summary>
+ private const string YuiLoaderHttps = "https://ajax.googleapis.com/ajax/libs/yui/2.8.0r4/build/yuiloader/yuiloader-min.js";
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="OpenIdAjaxTextBox"/> class.
+ /// </summary>
+ public OpenIdAjaxTextBox() {
+ this.HookFormSubmit = true;
+ }
+
+ #region Events
+
+ /// <summary>
+ /// Fired when the content of the text changes between posts to the server.
+ /// </summary>
+ [Description("Occurs when the content of the text changes between posts to the server."), Category(BehaviorCategory)]
+ public event EventHandler TextChanged;
+
+ /// <summary>
+ /// Gets or sets the client-side script that executes when an authentication
+ /// assertion is received (but before it is verified).
+ /// </summary>
+ /// <remarks>
+ /// <para>In the context of the executing javascript set in this property, the
+ /// local variable <i>sender</i> is set to the openid_identifier input box
+ /// that is executing this code.
+ /// This variable has a getClaimedIdentifier() method that may be used to
+ /// identify the user who is being authenticated.</para>
+ /// <para>It is <b>very</b> important to note that when this code executes,
+ /// the authentication has not been verified and may have been spoofed.
+ /// No security-sensitive operations should take place in this javascript code.
+ /// The authentication is verified on the server by the time the
+ /// <see cref="OpenIdRelyingPartyControlBase.LoggedIn"/> server-side event fires.</para>
+ /// </remarks>
+ [Description("Gets or sets the client-side script that executes when an authentication assertion is received (but before it is verified).")]
+ [Bindable(true), DefaultValue(""), Category(BehaviorCategory)]
+ public string OnClientAssertionReceived {
+ get { return this.ViewState[OnClientAssertionReceivedViewStateKey] as string; }
+ set { this.ViewState[OnClientAssertionReceivedViewStateKey] = value; }
+ }
+
+ #endregion
+
+ #region Properties
+
+ /// <summary>
+ /// Gets or sets the value in the text field, completely unprocessed or normalized.
+ /// </summary>
+ [Bindable(true), DefaultValue(""), Category(AppearanceCategory)]
+ [Description("The content of the text box.")]
+ public string Text {
+ get {
+ return this.Identifier != null ? this.Identifier.OriginalString : (this.ViewState[TextViewStateKey] as string ?? string.Empty);
+ }
+
+ set {
+ // Try to store it as a validated identifier,
+ // but failing that at least store the text.
+ Identifier id;
+ if (Identifier.TryParse(value, out id)) {
+ this.Identifier = id;
+ } else {
+ // Be sure to set the viewstate AFTER setting the Identifier,
+ // since setting the Identifier clears the viewstate in OnIdentifierChanged.
+ this.Identifier = null;
+ this.ViewState[TextViewStateKey] = value;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether a postback is made to fire the
+ /// <see cref="OpenIdRelyingPartyControlBase.LoggedIn"/> event as soon as authentication has completed
+ /// successfully.
+ /// </summary>
+ /// <value>
+ /// <c>true</c> if a postback should be made automatically upon authentication;
+ /// otherwise, <c>false</c> to delay the <see cref="OpenIdRelyingPartyControlBase.LoggedIn"/>
+ /// event from firing at the server until a postback is made by some other control.
+ /// </value>
+ [Bindable(true), Category(BehaviorCategory), DefaultValue(AutoPostBackDefault)]
+ [Description("Whether the LoggedIn event fires on the server as soon as authentication completes successfully.")]
+ public bool AutoPostBack {
+ get { return (bool)(this.ViewState[AutoPostBackViewStateKey] ?? AutoPostBackDefault); }
+ set { this.ViewState[AutoPostBackViewStateKey] = value; }
+ }
+
+ /// <summary>
+ /// Gets or sets the width of the text box in characters.
+ /// </summary>
+ [Bindable(true), Category(AppearanceCategory), DefaultValue(ColumnsDefault)]
+ [Description("The width of the text box in characters.")]
+ public int Columns {
+ get {
+ return (int)(this.ViewState[ColumnsViewStateKey] ?? ColumnsDefault);
+ }
+
+ set {
+ Contract.Requires<ArgumentOutOfRangeException>(value >= 0);
+ this.ViewState[ColumnsViewStateKey] = value;
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets the CSS class assigned to the text box.
+ /// </summary>
+ [Bindable(true), DefaultValue(CssClassDefault), Category(AppearanceCategory)]
+ [Description("The CSS class assigned to the text box.")]
+ public string CssClass {
+ get { return (string)this.ViewState[CssClassViewStateKey]; }
+ set { this.ViewState[CssClassViewStateKey] = value; }
+ }
+
+ /// <summary>
+ /// Gets or sets the tab index of the text box control. Use 0 to omit an explicit tabindex.
+ /// </summary>
+ [Bindable(true), Category(BehaviorCategory), DefaultValue(TabIndexDefault)]
+ [Description("The tab index of the text box control. Use 0 to omit an explicit tabindex.")]
+ public virtual short TabIndex {
+ get { return (short)(this.ViewState[TabIndexViewStateKey] ?? TabIndexDefault); }
+ set { this.ViewState[TabIndexViewStateKey] = value; }
+ }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this <see cref="OpenIdTextBox"/> is enabled
+ /// in the browser for editing and will respond to incoming OpenID messages.
+ /// </summary>
+ /// <value><c>true</c> if enabled; otherwise, <c>false</c>.</value>
+ [Bindable(true), DefaultValue(true), Category(BehaviorCategory)]
+ [Description("Whether the control is editable in the browser and will respond to OpenID messages.")]
+ public bool Enabled {
+ get { return (bool)(this.ViewState[EnabledViewStateKey] ?? true); }
+ set { this.ViewState[EnabledViewStateKey] = value; }
+ }
+
+ /// <summary>
+ /// Gets or sets the HTML name to assign to the text field.
+ /// </summary>
+ [Bindable(true), DefaultValue(NameDefault), Category("Misc")]
+ [Description("The HTML name to assign to the text field.")]
+ public string Name {
+ get {
+ return (string)(this.ViewState[NameViewStateKey] ?? NameDefault);
+ }
+
+ set {
+ Contract.Requires<ArgumentException>(!String.IsNullOrEmpty(value));
+ this.ViewState[NameViewStateKey] = value ?? string.Empty;
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets the time duration for the AJAX control to wait for an OP to respond before reporting failure to the user.
+ /// </summary>
+ [Browsable(true), DefaultValue(typeof(TimeSpan), "00:00:08"), Category(BehaviorCategory)]
+ [Description("The time duration for the AJAX control to wait for an OP to respond before reporting failure to the user.")]
+ public TimeSpan Timeout {
+ get {
+ return (TimeSpan)(this.ViewState[TimeoutViewStateKey] ?? TimeoutDefault);
+ }
+
+ set {
+ Contract.Requires<ArgumentOutOfRangeException>(value.TotalMilliseconds > 0);
+ this.ViewState[TimeoutViewStateKey] = value;
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets the maximum number of OpenID Providers to simultaneously try to authenticate with.
+ /// </summary>
+ [Browsable(true), DefaultValue(ThrottleDefault), Category(BehaviorCategory)]
+ [Description("The maximum number of OpenID Providers to simultaneously try to authenticate with.")]
+ public int Throttle {
+ get {
+ return (int)(this.ViewState[ThrottleViewStateKey] ?? ThrottleDefault);
+ }
+
+ set {
+ Contract.Requires<ArgumentOutOfRangeException>(value > 0);
+ this.ViewState[ThrottleViewStateKey] = value;
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets the text that appears on the LOG IN button in cases where immediate (invisible) authentication fails.
+ /// </summary>
+ [Bindable(true), DefaultValue(LogOnTextDefault), Localizable(true), Category(AppearanceCategory)]
+ [Description("The text that appears on the LOG IN button in cases where immediate (invisible) authentication fails.")]
+ public string LogOnText {
+ get {
+ return (string)(this.ViewState[LogOnTextViewStateKey] ?? LogOnTextDefault);
+ }
+
+ set {
+ Contract.Requires<ArgumentException>(!String.IsNullOrEmpty(value));
+ this.ViewState[LogOnTextViewStateKey] = value ?? string.Empty;
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets the rool tip text that appears on the LOG IN button in cases where immediate (invisible) authentication fails.
+ /// </summary>
+ [Bindable(true), DefaultValue(LogOnToolTipDefault), Localizable(true), Category(AppearanceCategory)]
+ [Description("The tool tip text that appears on the LOG IN button in cases where immediate (invisible) authentication fails.")]
+ public string LogOnToolTip {
+ get { return (string)(this.ViewState[LogOnToolTipViewStateKey] ?? LogOnToolTipDefault); }
+ set { this.ViewState[LogOnToolTipViewStateKey] = value ?? string.Empty; }
+ }
+
+ /// <summary>
+ /// Gets or sets the rool tip text that appears on the LOG IN button when clicking the button will result in an immediate postback.
+ /// </summary>
+ [Bindable(true), DefaultValue(LogOnPostBackToolTipDefault), Localizable(true), Category(AppearanceCategory)]
+ [Description("The tool tip text that appears on the LOG IN button when clicking the button will result in an immediate postback.")]
+ public string LogOnPostBackToolTip {
+ get { return (string)(this.ViewState[LogOnPostBackToolTipViewStateKey] ?? LogOnPostBackToolTipDefault); }
+ set { this.ViewState[LogOnPostBackToolTipViewStateKey] = value ?? string.Empty; }
+ }
+
+ /// <summary>
+ /// Gets or sets the text that appears on the RETRY button in cases where authentication times out.
+ /// </summary>
+ [Bindable(true), DefaultValue(RetryTextDefault), Localizable(true), Category(AppearanceCategory)]
+ [Description("The text that appears on the RETRY button in cases where authentication times out.")]
+ public string RetryText {
+ get {
+ return (string)(this.ViewState[RetryTextViewStateKey] ?? RetryTextDefault);
+ }
+
+ set {
+ Contract.Requires<ArgumentException>(!String.IsNullOrEmpty(value));
+ this.ViewState[RetryTextViewStateKey] = value ?? string.Empty;
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets the tool tip text that appears on the RETRY button in cases where authentication times out.
+ /// </summary>
+ [Bindable(true), DefaultValue(RetryToolTipDefault), Localizable(true), Category(AppearanceCategory)]
+ [Description("The tool tip text that appears on the RETRY button in cases where authentication times out.")]
+ public string RetryToolTip {
+ get { return (string)(this.ViewState[RetryToolTipViewStateKey] ?? RetryToolTipDefault); }
+ set { this.ViewState[RetryToolTipViewStateKey] = value ?? string.Empty; }
+ }
+
+ /// <summary>
+ /// Gets or sets the tool tip text that appears when authentication succeeds.
+ /// </summary>
+ [Bindable(true), DefaultValue(AuthenticationSucceededToolTipDefault), Localizable(true), Category(AppearanceCategory)]
+ [Description("The tool tip text that appears when authentication succeeds.")]
+ public string AuthenticationSucceededToolTip {
+ get { return (string)(this.ViewState[AuthenticationSucceededToolTipViewStateKey] ?? AuthenticationSucceededToolTipDefault); }
+ set { this.ViewState[AuthenticationSucceededToolTipViewStateKey] = value ?? string.Empty; }
+ }
+
+ /// <summary>
+ /// Gets or sets the tool tip text that appears on the green checkmark when authentication succeeds.
+ /// </summary>
+ [Bindable(true), DefaultValue(AuthenticatedAsToolTipDefault), Localizable(true), Category(AppearanceCategory)]
+ [Description("The tool tip text that appears on the green checkmark when authentication succeeds.")]
+ public string AuthenticatedAsToolTip {
+ get { return (string)(this.ViewState[AuthenticatedAsToolTipViewStateKey] ?? AuthenticatedAsToolTipDefault); }
+ set { this.ViewState[AuthenticatedAsToolTipViewStateKey] = value ?? string.Empty; }
+ }
+
+ /// <summary>
+ /// Gets or sets the tool tip text that appears when authentication fails.
+ /// </summary>
+ [Bindable(true), DefaultValue(AuthenticationFailedToolTipDefault), Localizable(true), Category(AppearanceCategory)]
+ [Description("The tool tip text that appears when authentication fails.")]
+ public string AuthenticationFailedToolTip {
+ get { return (string)(this.ViewState[AuthenticationFailedToolTipViewStateKey] ?? AuthenticationFailedToolTipDefault); }
+ set { this.ViewState[AuthenticationFailedToolTipViewStateKey] = value ?? string.Empty; }
+ }
+
+ /// <summary>
+ /// Gets or sets the tool tip text that appears over the text box when it is discovering and authenticating.
+ /// </summary>
+ [Bindable(true), DefaultValue(BusyToolTipDefault), Localizable(true), Category(AppearanceCategory)]
+ [Description("The tool tip text that appears over the text box when it is discovering and authenticating.")]
+ public string BusyToolTip {
+ get { return (string)(this.ViewState[BusyToolTipViewStateKey] ?? BusyToolTipDefault); }
+ set { this.ViewState[BusyToolTipViewStateKey] = value ?? string.Empty; }
+ }
+
+ /// <summary>
+ /// Gets or sets the message that is displayed if a postback is about to occur before the identifier has been supplied.
+ /// </summary>
+ [Bindable(true), DefaultValue(IdentifierRequiredMessageDefault), Localizable(true), Category(AppearanceCategory)]
+ [Description("The message that is displayed if a postback is about to occur before the identifier has been supplied.")]
+ public string IdentifierRequiredMessage {
+ get { return (string)(this.ViewState[IdentifierRequiredMessageViewStateKey] ?? IdentifierRequiredMessageDefault); }
+ set { this.ViewState[IdentifierRequiredMessageViewStateKey] = value ?? string.Empty; }
+ }
+
+ /// <summary>
+ /// Gets or sets the message that is displayed if a postback is attempted while login is in process.
+ /// </summary>
+ [Bindable(true), DefaultValue(LogOnInProgressMessageDefault), Localizable(true), Category(AppearanceCategory)]
+ [Description("The message that is displayed if a postback is attempted while login is in process.")]
+ public string LogOnInProgressMessage {
+ get { return (string)(this.ViewState[LogOnInProgressMessageViewStateKey] ?? LogOnInProgressMessageDefault); }
+ set { this.ViewState[LogOnInProgressMessageViewStateKey] = value ?? string.Empty; }
+ }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether the Yahoo! User Interface Library (YUI)
+ /// will be downloaded in order to provide a login split button.
+ /// </summary>
+ /// <value>
+ /// <c>true</c> to use a split button; otherwise, <c>false</c> to use a standard HTML button
+ /// or a split button by downloading the YUI library yourself on the hosting web page.
+ /// </value>
+ /// <remarks>
+ /// The split button brings in about 180KB of YUI javascript dependencies.
+ /// </remarks>
+ [Bindable(true), DefaultValue(DownloadYahooUILibraryDefault), Category(BehaviorCategory)]
+ [Description("Whether a split button will be used for the \"log in\" when the user provides an identifier that delegates to more than one Provider.")]
+ public bool DownloadYahooUILibrary {
+ get { return (bool)(this.ViewState[DownloadYahooUILibraryViewStateKey] ?? DownloadYahooUILibraryDefault); }
+ set { this.ViewState[DownloadYahooUILibraryViewStateKey] = value; }
+ }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether the "Log in" button will be shown
+ /// to initiate a postback containing the positive assertion.
+ /// </summary>
+ [Bindable(true), DefaultValue(ShowLogOnPostBackButtonDefault), Category(AppearanceCategory)]
+ [Description("Whether the log in button will be shown to initiate a postback containing the positive assertion.")]
+ public bool ShowLogOnPostBackButton {
+ get { return (bool)(this.ViewState[ShowLogOnPostBackButtonViewStateKey] ?? ShowLogOnPostBackButtonDefault); }
+ set { this.ViewState[ShowLogOnPostBackButtonViewStateKey] = value; }
+ }
+
+ #endregion
+
+ /// <summary>
+ /// Gets or sets a value indicating whether the ajax text box should hook the form's submit event for special behavior.
+ /// </summary>
+ internal bool HookFormSubmit { get; set; }
+
+ /// <summary>
+ /// Gets the name of the open id auth data form key.
+ /// </summary>
+ /// <value>
+ /// A concatenation of <see cref="Name"/> and <c>"_openidAuthData"</c>.
+ /// </value>
+ protected override string OpenIdAuthDataFormKey {
+ get { return this.Name + "_openidAuthData"; }
+ }
+
+ /// <summary>
+ /// Gets the default value for the <see cref="Timeout"/> property.
+ /// </summary>
+ /// <value>8 seconds; or eternity if the debugger is attached.</value>
+ private static TimeSpan TimeoutDefault {
+ get {
+ if (Debugger.IsAttached) {
+ Logger.OpenId.Warn("Debugger is attached. Inflating default OpenIdAjaxTextbox.Timeout value to infinity.");
+ return TimeSpan.MaxValue;
+ } else {
+ return TimeSpan.FromSeconds(8);
+ }
+ }
+ }
+
+ #region IPostBackDataHandler Members
+
+ /// <summary>
+ /// When implemented by a class, processes postback data for an ASP.NET server control.
+ /// </summary>
+ /// <param name="postDataKey">The key identifier for the control.</param>
+ /// <param name="postCollection">The collection of all incoming name values.</param>
+ /// <returns>
+ /// true if the server control's state changes as a result of the postback; otherwise, false.
+ /// </returns>
+ bool IPostBackDataHandler.LoadPostData(string postDataKey, NameValueCollection postCollection) {
+ return this.LoadPostData(postDataKey, postCollection);
+ }
+
+ /// <summary>
+ /// When implemented by a class, signals the server control to notify the ASP.NET application that the state of the control has changed.
+ /// </summary>
+ void IPostBackDataHandler.RaisePostDataChangedEvent() {
+ this.RaisePostDataChangedEvent();
+ }
+
+ #endregion
+
+ /// <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);
+
+ this.Page.RegisterRequiresPostBack(this);
+ }
+
+ /// <summary>
+ /// Called when the <see cref="Identifier"/> property is changed.
+ /// </summary>
+ protected override void OnIdentifierChanged() {
+ this.ViewState.Remove(TextViewStateKey);
+ base.OnIdentifierChanged();
+ }
+
+ /// <summary>
+ /// Prepares to render the control.
+ /// </summary>
+ /// <param name="e">An <see cref="T:System.EventArgs"/> object that contains the event data.</param>
+ protected override void OnPreRender(EventArgs e) {
+ base.OnPreRender(e);
+
+ if (!this.Visible) {
+ return;
+ }
+
+ if (this.DownloadYahooUILibrary) {
+ // Although we'll add the <script> tag to download the YAHOO component,
+ // a download failure may have occurred, so protect ourselves from a
+ // script error using an if (YAHOO) block. But apparently at least in IE
+ // that's not even enough, so we use a try/catch.
+ string yuiLoadScript = @"try { if (YAHOO) {
+ var loader = new YAHOO.util.YUILoader({
+ require: ['button', 'menu'],
+ loadOptional: false,
+ combine: true
+ });
+
+ loader.insert();
+} } catch (e) { }";
+ this.Page.ClientScript.RegisterClientScriptInclude("yuiloader", this.Page.Request.Url.IsTransportSecure() ? YuiLoaderHttps : YuiLoaderHttp);
+ this.Page.ClientScript.RegisterClientScriptBlock(this.GetType(), "requiredYuiComponents", yuiLoadScript, true);
+ }
+
+ var css = new HtmlLink();
+ try {
+ css.Href = this.Page.ClientScript.GetWebResourceUrl(this.GetType(), EmbeddedStylesheetResourceName);
+ css.Attributes["rel"] = "stylesheet";
+ css.Attributes["type"] = "text/css";
+ ErrorUtilities.VerifyHost(this.Page.Header != null, OpenIdStrings.HeadTagMustIncludeRunatServer);
+ this.Page.Header.Controls.AddAt(0, css); // insert at top so host page can override
+ } catch {
+ css.Dispose();
+ throw;
+ }
+
+ this.PrepareClientJavascript();
+
+ // If an Identifier is preset on this control, preload discovery on that identifier,
+ // but only if we're not already persisting an authentication result since that would
+ // be redundant.
+ if (this.Identifier != null && this.AuthenticationResponse == null) {
+ this.PreloadDiscovery(this.Identifier);
+ }
+ }
+
+ /// <summary>
+ /// Renders the control.
+ /// </summary>
+ /// <param name="writer">The <see cref="T:System.Web.UI.HtmlTextWriter"/> object that receives the control content.</param>
+ protected override void Render(HtmlTextWriter writer) {
+ base.Render(writer);
+
+ // We surround the textbox with a span so that the .js file can inject a
+ // login button within the text box with easy placement.
+ string css = this.CssClass ?? string.Empty;
+ css += " OpenIdAjaxTextBox";
+ writer.AddAttribute(HtmlTextWriterAttribute.Class, css);
+
+ writer.AddStyleAttribute(HtmlTextWriterStyle.Display, "inline-block");
+ writer.AddStyleAttribute(HtmlTextWriterStyle.Position, "relative");
+ writer.AddStyleAttribute(HtmlTextWriterStyle.FontSize, "16px");
+ writer.RenderBeginTag(HtmlTextWriterTag.Span);
+
+ writer.AddAttribute(HtmlTextWriterAttribute.Name, this.Name);
+ writer.AddAttribute(HtmlTextWriterAttribute.Id, this.ClientID);
+ writer.AddAttribute(HtmlTextWriterAttribute.Size, this.Columns.ToString(CultureInfo.InvariantCulture));
+ if (!string.IsNullOrEmpty(this.Text)) {
+ writer.AddAttribute(HtmlTextWriterAttribute.Value, this.Text, true);
+ }
+
+ if (this.TabIndex > 0) {
+ writer.AddAttribute(HtmlTextWriterAttribute.Tabindex, this.TabIndex.ToString(CultureInfo.InvariantCulture));
+ }
+ if (!this.Enabled) {
+ writer.AddAttribute(HtmlTextWriterAttribute.Disabled, "true");
+ }
+ if (!string.IsNullOrEmpty(this.CssClass)) {
+ writer.AddAttribute(HtmlTextWriterAttribute.Class, this.CssClass);
+ }
+ writer.AddStyleAttribute(HtmlTextWriterStyle.PaddingLeft, "18px");
+ writer.AddStyleAttribute(HtmlTextWriterStyle.BorderStyle, "solid");
+ writer.AddStyleAttribute(HtmlTextWriterStyle.BorderWidth, "1px");
+ writer.AddStyleAttribute(HtmlTextWriterStyle.BorderColor, "lightgray");
+ writer.RenderBeginTag(HtmlTextWriterTag.Input);
+ writer.RenderEndTag(); // </input>
+ writer.RenderEndTag(); // </span>
+ }
+
+ /// <summary>
+ /// When implemented by a class, processes postback data for an ASP.NET server control.
+ /// </summary>
+ /// <param name="postDataKey">The key identifier for the control.</param>
+ /// <param name="postCollection">The collection of all incoming name values.</param>
+ /// <returns>
+ /// true if the server control's state changes as a result of the postback; otherwise, false.
+ /// </returns>
+ protected virtual bool LoadPostData(string postDataKey, NameValueCollection postCollection) {
+ // If the control was temporarily hidden, it won't be in the Form data,
+ // and we'll just implicitly keep the last Text setting.
+ if (postCollection[this.Name] != null) {
+ Identifier identifier = postCollection[this.Name].Length == 0 ? null : postCollection[this.Name];
+ if (identifier != this.Identifier) {
+ this.Identifier = identifier;
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /// <summary>
+ /// When implemented by a class, signals the server control to notify the ASP.NET application that the state of the control has changed.
+ /// </summary>
+ [SuppressMessage("Microsoft.Design", "CA1030:UseEventsWhereAppropriate", Justification = "Predefined signature.")]
+ protected virtual void RaisePostDataChangedEvent() {
+ this.OnTextChanged();
+ }
+
+ /// <summary>
+ /// Called on a postback when the Text property has changed.
+ /// </summary>
+ protected virtual void OnTextChanged() {
+ EventHandler textChanged = this.TextChanged;
+ if (textChanged != null) {
+ textChanged(this, EventArgs.Empty);
+ }
+ }
+
+ /// <summary>
+ /// Assembles the javascript to send to the client and registers it with ASP.NET for transmission.
+ /// </summary>
+ private void PrepareClientJavascript() {
+ // Import the .js file where most of the code is.
+ this.Page.ClientScript.RegisterClientScriptResource(typeof(OpenIdAjaxTextBox), EmbeddedScriptResourceName);
+
+ // Call into the .js file with initialization information.
+ StringBuilder startupScript = new StringBuilder();
+ startupScript.AppendFormat("var box = document.getElementsByName('{0}')[0];{1}", this.Name, Environment.NewLine);
+ startupScript.AppendFormat(
+ CultureInfo.InvariantCulture,
+ "initAjaxOpenId(box, {0}, {1}, {2}, {3}, {4}, {5}, {6}, {7}, {8}, {9}, {10}, {11}, {12}, {13}, {14}, {15}, {16}, {17}, {18}, {19}, function() {{{20};}});{21}",
+ MessagingUtilities.GetSafeJavascriptValue(this.Page.ClientScript.GetWebResourceUrl(this.GetType(), OpenIdTextBox.EmbeddedLogoResourceName)),
+ MessagingUtilities.GetSafeJavascriptValue(this.Page.ClientScript.GetWebResourceUrl(this.GetType(), EmbeddedSpinnerResourceName)),
+ MessagingUtilities.GetSafeJavascriptValue(this.Page.ClientScript.GetWebResourceUrl(this.GetType(), EmbeddedLoginSuccessResourceName)),
+ MessagingUtilities.GetSafeJavascriptValue(this.Page.ClientScript.GetWebResourceUrl(this.GetType(), EmbeddedLoginFailureResourceName)),
+ this.Throttle,
+ this.Timeout.TotalMilliseconds,
+ string.IsNullOrEmpty(this.OnClientAssertionReceived) ? "null" : "'" + this.OnClientAssertionReceived.Replace(@"\", @"\\").Replace("'", @"\'") + "'",
+ MessagingUtilities.GetSafeJavascriptValue(this.LogOnText),
+ MessagingUtilities.GetSafeJavascriptValue(this.LogOnToolTip),
+ this.ShowLogOnPostBackButton ? "true" : "false",
+ MessagingUtilities.GetSafeJavascriptValue(this.LogOnPostBackToolTip),
+ MessagingUtilities.GetSafeJavascriptValue(this.RetryText),
+ MessagingUtilities.GetSafeJavascriptValue(this.RetryToolTip),
+ MessagingUtilities.GetSafeJavascriptValue(this.BusyToolTip),
+ MessagingUtilities.GetSafeJavascriptValue(this.IdentifierRequiredMessage),
+ MessagingUtilities.GetSafeJavascriptValue(this.LogOnInProgressMessage),
+ MessagingUtilities.GetSafeJavascriptValue(this.AuthenticationSucceededToolTip),
+ MessagingUtilities.GetSafeJavascriptValue(this.AuthenticatedAsToolTip),
+ MessagingUtilities.GetSafeJavascriptValue(this.AuthenticationFailedToolTip),
+ this.AutoPostBack ? "true" : "false",
+ Page.ClientScript.GetPostBackEventReference(this, null),
+ Environment.NewLine);
+
+ ScriptManager.RegisterStartupScript(this, this.GetType(), "ajaxstartup", startupScript.ToString(), true);
+ if (this.HookFormSubmit) {
+ string htmlFormat = @"
+var openidbox = document.getElementsByName('{0}')[0];
+if (!openidbox.dnoi_internal.onSubmit()) {{ return false; }}
+";
+ Page.ClientScript.RegisterOnSubmitStatement(
+ this.GetType(),
+ "loginvalidation",
+ string.Format(CultureInfo.InvariantCulture, htmlFormat, this.Name));
+ }
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/OpenIdAjaxTextBox.css b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/OpenIdAjaxTextBox.css
new file mode 100644
index 0000000..bed2e79
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/OpenIdAjaxTextBox.css
@@ -0,0 +1,49 @@
+.OpenIdAjaxTextBox input
+{
+ margin: 0px;
+}
+
+.OpenIdAjaxTextBox > span
+{
+ position: absolute;
+ right: -1px;
+ top: 2px;
+}
+
+.OpenIdAjaxTextBox input[type=button]
+{
+ visibility: hidden;
+ position: absolute;
+ padding: 0px;
+ font-size: 8px;
+ top: 1px;
+ bottom: 1px;
+ right: 2px;
+}
+
+.OpenIdAjaxTextBox .yui-split-button span button
+{
+ font-size: 50%;
+ font-size: 60%\9; /* the \9 is a hack that causes only IE7/8 to use this value. */
+ line-height: 1;
+ min-height: 1em;
+ padding-top: 2px;
+ padding-top: 3px\9;
+ padding-bottom: 1px;
+ padding-left: 5px;
+ height: auto;
+}
+
+.OpenIdAjaxTextBox .yuimenuitem .yuimenuitemlabel
+{
+ padding-left: 5px;
+}
+
+.OpenIdAjaxTextBox .yuimenuitem .yuimenuitemlabel img
+{
+ border: 0;
+ margin-right: 4px;
+ vertical-align: middle;
+ width: 16px;
+ height: 16px;
+}
diff --git a/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/OpenIdAjaxTextBox.js b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/OpenIdAjaxTextBox.js
new file mode 100644
index 0000000..9907b4e
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/OpenIdAjaxTextBox.js
@@ -0,0 +1,644 @@
+//-----------------------------------------------------------------------
+// <copyright file="OpenIdAjaxTextBox.js" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// This file may be used and redistributed under the terms of the
+// Microsoft Public License (Ms-PL) http://opensource.org/licenses/ms-pl.html
+// </copyright>
+//-----------------------------------------------------------------------
+
+function initAjaxOpenId(box, openid_logo_url, spinner_url, success_icon_url, failure_icon_url,
+ throttle, timeout, assertionReceivedCode,
+ loginButtonText, loginButtonToolTip, showLoginPostBackButton, loginPostBackToolTip,
+ retryButtonText, retryButtonToolTip, busyToolTip,
+ identifierRequiredMessage, loginInProgressMessage,
+ authenticatedByToolTip, authenticatedAsToolTip, authenticationFailedToolTip,
+ autoPostback, postback) {
+ box.dnoi_internal = {
+ postback: postback
+ };
+ if (assertionReceivedCode) {
+ box.dnoi_internal.onauthenticated = function(sender, e) { eval(assertionReceivedCode); };
+ }
+
+ box.dnoi_internal.originalBackground = box.style.background;
+ box.timeout = timeout;
+
+ box.dnoi_internal.authenticationIFrames = new window.dnoa_internal.FrameManager(throttle);
+
+ box.dnoi_internal.constructButton = function(text, tooltip, onclick) {
+ var button = document.createElement('input');
+ button.textContent = text; // Mozilla
+ button.value = text; // IE
+ button.type = 'button';
+ button.title = tooltip || '';
+ button.onclick = onclick;
+ box.parentNode.appendChild(button);
+ return button;
+ };
+
+ box.dnoi_internal.constructSplitButton = function(text, tooltip, onclick, menu) {
+ var htmlButton = box.dnoi_internal.constructButton(text, tooltip, onclick);
+
+ if (!box.parentNode.className || box.parentNode.className.indexOf(' yui-skin-sam') < 0) {
+ box.parentNode.className = (box.parentNode.className || '') + ' yui-skin-sam';
+ }
+
+ var splitButton = new YAHOO.widget.Button(htmlButton, {
+ type: 'split',
+ menu: menu
+ });
+
+ splitButton.on('click', onclick);
+
+ return splitButton;
+ };
+
+ box.dnoi_internal.createLoginPostBackButton = function() {
+ var postback = function() {
+ var discoveryResult = window.dnoa_internal.discoveryResults[box.value];
+ var respondingEndpoint = discoveryResult.findSuccessfulRequest();
+ box.dnoi_internal.postback(discoveryResult, respondingEndpoint, respondingEndpoint.extensionResponses, { background: false });
+ };
+ var button = box.dnoi_internal.constructButton(loginButtonText, loginPostBackToolTip, postback);
+ button.style.visibility = 'visible';
+ button.destroy = function() {
+ button.parentNode.removeChild(button);
+ };
+
+ return button;
+ };
+
+ box.dnoi_internal.createLoginButton = function(providers) {
+ var onMenuItemClick = function(p_sType, p_aArgs, p_oItem) {
+ var selectedProvider = (p_oItem && p_oItem.value) ? p_oItem.value : providers[0].value;
+ selectedProvider.loginPopup();
+ return false;
+ };
+
+ for (var i = 0; i < providers.length; i++) {
+ providers[i].onclick = { fn: onMenuItemClick };
+ }
+
+ // We'll use the split button if we have more than one Provider, and the YUI library is available.
+ if (providers.length > 1 && YAHOO && YAHOO.widget && YAHOO.widget.Button) {
+ return box.dnoi_internal.constructSplitButton(loginButtonText, loginButtonToolTip, onMenuItemClick, providers);
+ } else {
+ var button = box.dnoi_internal.constructButton(loginButtonText, loginButtonToolTip, onMenuItemClick);
+ button.style.visibility = 'visible';
+ button.destroy = function() {
+ button.parentNode.removeChild(button);
+ };
+ return button;
+ }
+ };
+
+ box.dnoi_internal.constructIcon = function(imageUrl, tooltip, rightSide, visible, height) {
+ var icon = document.createElement('img');
+ icon.src = imageUrl;
+ icon.title = tooltip || '';
+ icon.originalTitle = icon.title;
+ if (!visible) {
+ icon.style.visibility = 'hidden';
+ }
+ icon.style.position = 'absolute';
+ icon.style.top = "2px";
+ icon.style.bottom = "2px"; // for FireFox (and IE7, I think)
+ if (height) {
+ icon.style.height = height; // for Chrome and IE8
+ }
+ if (rightSide) {
+ icon.style.right = "2px";
+ } else {
+ icon.style.left = "2px";
+ }
+ box.parentNode.appendChild(icon);
+ return icon;
+ };
+
+ box.dnoi_internal.prefetchImage = function(imageUrl) {
+ var img = document.createElement('img');
+ img.src = imageUrl;
+ img.style.display = 'none';
+ box.parentNode.appendChild(img);
+ return img;
+ };
+
+ function findParentForm(element) {
+ if (!element || element.nodeName == "FORM") {
+ return element;
+ }
+
+ return findParentForm(element.parentNode);
+ }
+
+ box.parentForm = findParentForm(box);
+
+ function findOrCreateHiddenField() {
+ var name = box.name + '_openidAuthData';
+ var existing = window.document.getElementsByName(name);
+ if (existing && existing.length > 0) {
+ return existing[0];
+ }
+
+ var hiddenField = document.createElement('input');
+ hiddenField.setAttribute("name", name);
+ hiddenField.setAttribute("type", "hidden");
+ box.parentForm.appendChild(hiddenField);
+ return hiddenField;
+ }
+
+ box.dnoi_internal.retryButton = box.dnoi_internal.constructButton(retryButtonText, retryButtonToolTip, function() {
+ box.timeout += 5000; // give the retry attempt 5s longer than the last attempt
+ box.dnoi_internal.performDiscovery();
+ return false;
+ });
+ box.dnoi_internal.openid_logo = box.dnoi_internal.constructIcon(openid_logo_url, null, false, true);
+ box.dnoi_internal.op_logo = box.dnoi_internal.constructIcon('', authenticatedByToolTip, false, false, "16px");
+ box.dnoi_internal.op_logo.style.maxWidth = '16px';
+ box.dnoi_internal.spinner = box.dnoi_internal.constructIcon(spinner_url, busyToolTip, true);
+ box.dnoi_internal.success_icon = box.dnoi_internal.constructIcon(success_icon_url, authenticatedAsToolTip, true);
+ box.dnoi_internal.failure_icon = box.dnoi_internal.constructIcon(failure_icon_url, authenticationFailedToolTip, true);
+
+ box.dnoi_internal.dnoi_logo = box.dnoi_internal.openid_logo;
+
+ box.dnoi_internal.setVisualCue = function(state, authenticatedBy, authenticatedAs, providers, errorMessage) {
+ box.dnoi_internal.openid_logo.style.visibility = 'hidden';
+ box.dnoi_internal.dnoi_logo.style.visibility = 'hidden';
+ box.dnoi_internal.op_logo.style.visibility = 'hidden';
+ box.dnoi_internal.openid_logo.title = box.dnoi_internal.openid_logo.originalTitle;
+ box.dnoi_internal.spinner.style.visibility = 'hidden';
+ box.dnoi_internal.success_icon.style.visibility = 'hidden';
+ box.dnoi_internal.failure_icon.style.visibility = 'hidden';
+ box.dnoi_internal.retryButton.style.visibility = 'hidden';
+ if (box.dnoi_internal.loginButton) {
+ box.dnoi_internal.loginButton.destroy();
+ box.dnoi_internal.loginButton = null;
+ }
+ if (box.dnoi_internal.postbackLoginButton) {
+ box.dnoi_internal.postbackLoginButton.destroy();
+ box.dnoi_internal.postbackLoginButton = null;
+ }
+ box.title = '';
+ box.dnoi_internal.state = state;
+ var opLogo;
+ if (state == "discovering") {
+ box.dnoi_internal.dnoi_logo.style.visibility = 'visible';
+ box.dnoi_internal.spinner.style.visibility = 'visible';
+ box.dnoi_internal.claimedIdentifier = null;
+ box.title = '';
+ window.status = "Discovering OpenID Identifier '" + box.value + "'...";
+ } else if (state == "authenticated") {
+ opLogo = box.dnoi_internal.deriveOPFavIcon();
+ if (opLogo) {
+ box.dnoi_internal.op_logo.src = opLogo;
+ box.dnoi_internal.op_logo.style.visibility = 'visible';
+ box.dnoi_internal.op_logo.title = box.dnoi_internal.op_logo.originalTitle.replace('{0}', authenticatedBy.getHost());
+ }
+ //trace("OP icon size: " + box.dnoi_internal.op_logo.fileSize);
+ // The filesize check just doesn't seem to work any more.
+ if (!opLogo) {// || box.dnoi_internal.op_logo.fileSize == -1 /*IE*/ || box.dnoi_internal.op_logo.fileSize === undefined /* FF */) {
+ trace('recovering from missing OP icon');
+ box.dnoi_internal.op_logo.style.visibility = 'hidden';
+ box.dnoi_internal.openid_logo.style.visibility = 'visible';
+ box.dnoi_internal.openid_logo.title = box.dnoi_internal.op_logo.originalTitle.replace('{0}', authenticatedBy.getHost());
+ }
+ if (showLoginPostBackButton) {
+ box.dnoi_internal.postbackLoginButton = box.dnoi_internal.createLoginPostBackButton();
+ } else {
+ box.dnoi_internal.success_icon.style.visibility = 'visible';
+ box.dnoi_internal.success_icon.title = box.dnoi_internal.success_icon.originalTitle.replace('{0}', authenticatedAs);
+ }
+ box.title = box.dnoi_internal.claimedIdentifier;
+ window.status = "Authenticated as " + authenticatedAs;
+ } else if (state == "setup") {
+ opLogo = box.dnoi_internal.deriveOPFavIcon();
+ if (opLogo) {
+ box.dnoi_internal.op_logo.src = opLogo;
+ box.dnoi_internal.op_logo.style.visibility = 'visible';
+ } else {
+ box.dnoi_internal.openid_logo.style.visibility = 'visible';
+ }
+
+ box.dnoi_internal.loginButton = box.dnoi_internal.createLoginButton(providers);
+
+ box.dnoi_internal.claimedIdentifier = null;
+ window.status = "Authentication requires user interaction.";
+ } else if (state == "failed") {
+ box.dnoi_internal.openid_logo.style.visibility = 'visible';
+ box.dnoi_internal.retryButton.style.visibility = 'visible';
+ box.dnoi_internal.claimedIdentifier = null;
+ window.status = authenticationFailedToolTip;
+ box.title = authenticationFailedToolTip;
+ } else if (state == "failednoretry") {
+ box.dnoi_internal.failure_icon.title = errorMessage;
+ box.dnoi_internal.failure_icon.style.visibility = 'visible';
+ box.dnoi_internal.openid_logo.style.visibility = 'visible';
+ box.dnoi_internal.claimedIdentifier = null;
+ window.status = errorMessage;
+ box.title = errorMessage;
+ } else if (state == '' || !state) {
+ box.dnoi_internal.openid_logo.style.visibility = 'visible';
+ box.title = '';
+ box.dnoi_internal.claimedIdentifier = null;
+ window.status = null;
+ } else {
+ box.dnoi_internal.claimedIdentifier = null;
+ trace('unrecognized state ' + state);
+ }
+
+ if (box.onStateChanged) {
+ box.onStateChanged(state);
+ }
+ };
+
+ box.dnoi_internal.isBusy = function() {
+ var lastDiscovery = window.dnoa_internal.discoveryResults[box.lastDiscoveredIdentifier];
+ return box.dnoi_internal.state == 'discovering' ||
+ (lastDiscovery && lastDiscovery.busy());
+ };
+
+ box.dnoi_internal.canAttemptLogin = function() {
+ if (box.value.length === 0) { return false; }
+ if (!window.dnoa_internal.discoveryResults[box.value]) { return false; }
+ if (box.dnoi_internal.state == 'failed') { return false; }
+ return true;
+ };
+
+ box.dnoi_internal.getUserSuppliedIdentifierResults = function() {
+ return window.dnoa_internal.discoveryResults[box.value];
+ };
+
+ box.dnoi_internal.isAuthenticated = function() {
+ var results = box.dnoi_internal.getUserSuppliedIdentifierResults();
+ return results && results.findSuccessfulRequest();
+ };
+
+ box.dnoi_internal.onSubmit = function() {
+ var hiddenField = findOrCreateHiddenField();
+ if (box.dnoi_internal.isAuthenticated()) {
+ // stick the result in a hidden field so the RP can verify it
+ hiddenField.setAttribute("value", window.dnoa_internal.discoveryResults[box.value].successAuthData);
+ } else {
+ hiddenField.setAttribute("value", '');
+ if (box.dnoi_internal.isBusy()) {
+ alert(loginInProgressMessage);
+ } else {
+ if (box.value.length > 0) {
+ // submitPending will be true if we've already tried deferring submit for a login,
+ // in which case we just want to display a box to the user.
+ if (box.dnoi_internal.submitPending || !box.dnoi_internal.canAttemptLogin()) {
+ alert(identifierRequiredMessage);
+ } else {
+ // The user hasn't clicked "Login" yet. We'll click login for him,
+ // after leaving a note for ourselves to automatically click submit
+ // when login is complete.
+ box.dnoi_internal.submitPending = box.dnoi_internal.submitButtonJustClicked;
+ if (box.dnoi_internal.submitPending === null) {
+ box.dnoi_internal.submitPending = true;
+ }
+ box.dnoi_internal.loginButton.onclick();
+ return false; // abort submit for now
+ }
+ } else {
+ return true;
+ }
+ }
+ return false;
+ }
+ return true;
+ };
+
+ /// <summary>
+ /// Records which submit button caused this openid box to question whether it
+ /// was ready to submit the user's identifier so that that button can be re-invoked
+ /// automatically after authentication completes.
+ /// </summary>
+ box.dnoi_internal.setLastSubmitButtonClicked = function(evt) {
+ var button;
+ if (evt.target) {
+ button = evt.target;
+ } else {
+ button = evt.srcElement;
+ }
+
+ box.dnoi_internal.submitButtonJustClicked = button;
+ };
+
+ // Find all submit buttons and hook their click events so that we can validate
+ // whether we are ready for the user to postback.
+ var inputs = document.getElementsByTagName('input');
+ for (var i = 0; i < inputs.length; i++) {
+ var el = inputs[i];
+ if (el.type == 'submit') {
+ if (el.attachEvent) {
+ el.attachEvent("onclick", box.dnoi_internal.setLastSubmitButtonClicked);
+ } else {
+ el.addEventListener("click", box.dnoi_internal.setLastSubmitButtonClicked, true);
+ }
+ }
+ }
+
+ /// <summary>
+ /// Returns the URL of the authenticating OP's logo so it can be displayed to the user.
+ /// </summary>
+ /// <param name="opUri">The OP Endpoint, if known.</param>
+ box.dnoi_internal.deriveOPFavIcon = function(opUri) {
+ if (!opUri) {
+ var idresults = box.dnoi_internal.getUserSuppliedIdentifierResults();
+ var response = idresults ? idresults.successAuthData : null;
+ if (!response || response.length === 0) {
+ trace('No favicon because no successAuthData.');
+ return;
+ }
+ var authResult = new window.dnoa_internal.Uri(response);
+ if (authResult.getQueryArgValue("openid.op_endpoint")) {
+ opUri = new window.dnoa_internal.Uri(authResult.getQueryArgValue("openid.op_endpoint"));
+ } else if (authResult.getQueryArgValue("dnoa.op_endpoint")) {
+ opUri = new window.dnoa_internal.Uri(authResult.getQueryArgValue("dnoa.op_endpoint"));
+ } else if (authResult.getQueryArgValue("openid.user_setup_url")) {
+ opUri = new window.dnoa_internal.Uri(authResult.getQueryArgValue("openid.user_setup_url"));
+ } else {
+ return null;
+ }
+ }
+ var favicon = opUri.getAuthority() + "/favicon.ico";
+ trace('Guessing favicon location of: ' + favicon);
+ return favicon;
+ };
+
+ /*****************************************
+ * Event Handlers
+ *****************************************/
+
+ window.dnoa_internal.addDiscoveryStarted(function(identifier) {
+ if (identifier == box.value) {
+ box.dnoi_internal.setVisualCue('discovering');
+ }
+ }, box);
+
+ window.dnoa_internal.addDiscoverySuccess(function(identifier, discoveryResult, state) {
+ if (identifier == box.value && (box.dnoi_internal.state == 'discovering' || !box.dnoi_internal.state)) {
+ // Start pre-fetching the OP favicons
+ for (var i = 0; i < discoveryResult.length; i++) {
+ var favicon = box.dnoi_internal.deriveOPFavIcon(discoveryResult[i].endpoint);
+ if (favicon) {
+ trace('Prefetching ' + favicon);
+ box.dnoi_internal.prefetchImage(favicon);
+ }
+ }
+ if (discoveryResult.length > 0) {
+ discoveryResult.loginBackground(
+ box.dnoi_internal.authenticationIFrames,
+ null,
+ null,
+ null,
+ box.timeout);
+ } else {
+ // discovery completed successfully -- it just didn't yield any service endpoints.
+ box.dnoi_internal.setVisualCue('failednoretry', null, null, null, discoveryResult.error);
+ if (discoveryResult.error) { box.title = discoveryResult.error; }
+ }
+ }
+ }, box);
+
+ window.dnoa_internal.addDiscoveryFailed(function(identifier, message) {
+ if (identifier == box.value) {
+ box.dnoi_internal.setVisualCue('failed');
+ if (message) { box.title = message; }
+ }
+ }, box);
+
+ window.dnoa_internal.addAuthStarted(function(discoveryResult, serviceEndpoint, state) {
+ if (discoveryResult.userSuppliedIdentifier == box.value) {
+ box.dnoi_internal.setVisualCue('discovering');
+ }
+ }, box);
+
+ window.dnoa_internal.addAuthSuccess(function(discoveryResult, serviceEndpoint, extensionResponses, state) {
+ if (discoveryResult.userSuppliedIdentifier == box.value) {
+ // visual cue that auth was successful
+ var parsedPositiveAssertion = new window.dnoa_internal.PositiveAssertion(discoveryResult.successAuthData);
+ box.dnoi_internal.claimedIdentifier = parsedPositiveAssertion.claimedIdentifier;
+
+ // If the OP doesn't support delegation, "correct" the identifier the user entered
+ // so he realizes his identity didn't stick. But don't change out OP Identifiers.
+ if (discoveryResult.claimedIdentifier && discoveryResult.claimedIdentifier != parsedPositiveAssertion.claimedIdentifier) {
+ box.value = parsedPositiveAssertion.claimedIdentifier;
+ box.lastDiscoveredIdentifier = box.value;
+
+ // Also inject a fake discovery result for this new identifier to keep the UI from performing
+ // discovery on the new identifier (the RP will perform the necessary verification server-side).
+ if (!window.dnoa_internal.discoveryResults[box.value]) {
+ // We must make sure that the only service endpoint from the earlier discovery that
+ // is copied over is the one that sent the assertion just now. Deep clone, then strip
+ // out the other SEPs.
+ window.dnoa_internal.discoveryResults[box.value] = discoveryResult.cloneWithOneServiceEndpoint(serviceEndpoint);
+ }
+ }
+ box.dnoi_internal.setVisualCue('authenticated', parsedPositiveAssertion.endpoint, parsedPositiveAssertion.claimedIdentifier);
+ if (box.dnoi_internal.onauthenticated) {
+ box.dnoi_internal.onauthenticated(box, extensionResponses);
+ }
+
+ if (showLoginPostBackButton && !state.background) {
+ box.dnoi_internal.postback(discoveryResult, serviceEndpoint, extensionResponses, state);
+ } else if (box.dnoi_internal.submitPending) {
+ // We submit the form BEFORE resetting the submitPending so
+ // the submit handler knows we've already tried this route.
+ if (box.dnoi_internal.submitPending === true) {
+ box.parentForm.submit();
+ } else {
+ box.dnoi_internal.submitPending.click();
+ }
+
+ box.dnoi_internal.submitPending = null;
+ } else if (!state.deserialized && autoPostback) {
+ // as long as this is a fresh auth response, postback to the server if configured to do so.
+ box.dnoi_internal.postback(discoveryResult, serviceEndpoint, extensionResponses, state);
+ }
+ }
+ }, box);
+
+ window.dnoa_internal.addAuthFailed(function(discoveryResult, serviceEndpoint, state) {
+ if (discoveryResult.userSuppliedIdentifier == box.value) {
+ box.dnoi_internal.submitPending = null;
+ if (!serviceEndpoint || !state.background) { // if the last service endpoint just turned the user down
+ box.dnoi_internal.displayLoginButton(discoveryResult);
+ }
+ }
+ }, box);
+
+ window.dnoa_internal.addAuthCleared(function(discoveryResult, serviceEndpoint) {
+ if (discoveryResult.userSuppliedIdentifier == box.value) {
+ if (!discoveryResult.findSuccessfulRequest()) {
+ // attempt to renew the positive assertion.
+ discoveryResult.loginBackground(
+ box.dnoi_internal.authenticationIFrames,
+ null,
+ null,
+ null,
+ box.timeout);
+ }
+ }
+ }, box);
+
+ /*****************************************
+ * Flow
+ *****************************************/
+
+ box.dnoi_internal.displayLoginButton = function(discoveryResult) {
+ trace('No asynchronous authentication attempt is in progress. Display setup view.');
+ var providers = [];
+ for (var i = 0; i < discoveryResult.length; i++) {
+ var favicon = box.dnoi_internal.deriveOPFavIcon(discoveryResult[i].endpoint);
+ var img = '<img src="' + favicon + '" />';
+ providers.push({ text: img + discoveryResult[i].host, value: discoveryResult[i] });
+ }
+
+ // visual cue that auth failed
+ box.dnoi_internal.setVisualCue('setup', null, null, providers);
+ };
+
+ /// <summary>Called to initiate discovery on some identifier.</summary>
+ box.dnoi_internal.performDiscovery = function() {
+ box.dnoi_internal.authenticationIFrames.closeFrames();
+ box.lastDiscoveredIdentifier = box.value;
+ var openid = new window.OpenIdIdentifier(box.value);
+ openid.discover();
+ };
+
+ box.onblur = function(event) {
+ if (box.lastDiscoveredIdentifier != box.value || !box.dnoi_internal.state) {
+ if (box.value.length > 0) {
+ box.dnoi_internal.resetAndDiscover();
+ } else {
+ box.dnoi_internal.setVisualCue();
+ }
+ }
+
+ return true;
+ };
+
+ //{
+ var rate = NaN;
+ var lastValue = box.value;
+ var keyPresses = 0;
+ var startTime = null;
+ var lastKeyPress = null;
+ var discoveryTimer;
+
+ function cancelTimer() {
+ if (discoveryTimer) {
+ trace('canceling timer', 'gray');
+ clearTimeout(discoveryTimer);
+ discoveryTimer = null;
+ }
+ }
+
+ function identifierSanityCheck(id) {
+ return id.match("^[=@+$!(].+|.*?\\..*[^\\.]|\\w+://.+");
+ }
+
+ function discover() {
+ cancelTimer();
+ trace('typist discovery candidate', 'gray');
+ if (identifierSanityCheck(box.value)) {
+ trace('typist discovery begun', 'gray');
+ box.dnoi_internal.performDiscovery();
+ } else {
+ trace('typist discovery canceled due to incomplete identifier.', 'gray');
+ }
+ }
+
+ function reset() {
+ keyPresses = 0;
+ startTime = null;
+ rate = NaN;
+ trace('resetting state', 'gray');
+ }
+
+ box.dnoi_internal.resetAndDiscover = function() {
+ reset();
+ discover();
+ };
+
+ box.onkeyup = function(e) {
+ e = e || window.event; // for IE
+
+ if (new Date() - lastKeyPress > 3000) {
+ // the user seems to have altogether stopped typing,
+ // so reset our typist speed detector.
+ reset();
+ }
+ lastKeyPress = new Date();
+
+ var newValue = box.value;
+ if (e.keyCode == 13) {
+ if (box.dnoi_internal.state === 'setup') {
+ box.dnoi_internal.loginButton.click();
+ } else if (box.dnoi_internal.postbackLoginButton) {
+ box.dnoi_internal.postbackLoginButton.click();
+ } else {
+ discover();
+ }
+ } else {
+ if (lastValue != newValue && newValue != box.lastDiscoveredIdentifier) {
+ box.dnoi_internal.setVisualCue();
+ if (newValue.length === 0) {
+ reset();
+ } else if (Math.abs((lastValue || '').length - newValue.length) > 1) {
+ // One key press is responsible for multiple character changes.
+ // The user may have pasted in his identifier in which case
+ // we want to begin discovery immediately.
+ trace(newValue + ': paste detected (old value ' + lastValue + ')', 'gray');
+ discover();
+ } else {
+ keyPresses++;
+ var timeout = 3000; // timeout to use if we don't have enough keying to figure out type rate
+ if (startTime === null) {
+ startTime = new Date();
+ } else if (keyPresses > 1) {
+ cancelTimer();
+ rate = (new Date() - startTime) / keyPresses;
+ var minTimeout = 300;
+ var maxTimeout = 3000;
+ var typistFactor = 5;
+ timeout = Math.max(minTimeout, Math.min(rate * typistFactor, maxTimeout));
+ }
+
+ trace(newValue + ': setting timer for ' + timeout, 'gray');
+ discoveryTimer = setTimeout(discover, timeout);
+ }
+ }
+ }
+
+ trace(newValue + ': updating lastValue', 'gray');
+ lastValue = newValue;
+
+ return true;
+ };
+ //}
+
+ box.getClaimedIdentifier = function() { return box.dnoi_internal.claimedIdentifier; };
+
+ // If an identifier is preset on the box, perform discovery on it, but only
+ // if there isn't a prior authentication that we're about to deserialize.
+ if (box.value.length > 0 && findOrCreateHiddenField().value.length === 0) {
+ trace('jumpstarting discovery on ' + box.value + ' because it was preset.');
+ box.dnoi_internal.performDiscovery();
+ }
+
+ // Restore a previously achieved state (from pre-postback) if it is given.
+ window.dnoa_internal.deserializePreviousAuthentication(findOrCreateHiddenField().value);
+
+ // public methods
+ box.setValue = function(value) {
+ box.value = value;
+ if (box.value) {
+ box.dnoi_internal.performDiscovery();
+ }
+ };
+
+ // public events
+ // box.onStateChanged(state)
+}
diff --git a/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/OpenIdButton.cs b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/OpenIdButton.cs
new file mode 100644
index 0000000..6243917
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/OpenIdButton.cs
@@ -0,0 +1,179 @@
+//-----------------------------------------------------------------------
+// <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]!";
+
+ /// <summary>
+ /// The default value for the <see cref="PrecreateRequest"/> property.
+ /// </summary>
+ private const bool PrecreateRequestDefault = false;
+
+ #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";
+
+ /// <summary>
+ /// The key under which the value for the <see cref="PrecreateRequest"/> property will be stored.
+ /// </summary>
+ private const string PrecreateRequestViewStateKey = "PrecreateRequest";
+
+ #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 whether to pre-discover the identifier so
+ /// the user agent has an immediate redirect.
+ /// </summary>
+ [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Precreate", Justification = "Breaking change to public API")]
+ [Bindable(true), Category(OpenIdCategory), DefaultValue(PrecreateRequestDefault)]
+ [Description("Whether to pre-discover the identifier so the user agent has an immediate redirect.")]
+ public bool PrecreateRequest {
+ get { return (bool)(ViewState[PrecreateRequestViewStateKey] ?? PrecreateRequestDefault); }
+ set { ViewState[PrecreateRequestViewStateKey] = 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>
+ /// When implemented by a class, enables a server control to process an event raised when a form is posted to the server.
+ /// </summary>
+ /// <param name="eventArgument">A <see cref="T:System.String"/> that represents an optional event argument to be passed to the event handler.</param>
+ protected override void RaisePostBackEvent(string eventArgument) {
+ if (!this.PrecreateRequest) {
+ try {
+ IAuthenticationRequest request = this.CreateRequests().First();
+ request.RedirectToProvider();
+ } catch (InvalidOperationException ex) {
+ throw ErrorUtilities.Wrap(ex, OpenIdStrings.OpenIdEndpointNotFound);
+ }
+ }
+ }
+
+ /// <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>
+ [SuppressMessage("Microsoft.Globalization", "CA1303:Do not pass literals as localized parameters", MessageId = "System.Web.UI.HtmlTextWriter.WriteEncodedText(System.String)", Justification = "Not localizable")]
+ 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;
+ if (this.PrecreateRequest && !this.DesignMode) {
+ IAuthenticationRequest request = this.CreateRequests().FirstOrDefault();
+ if (request != null) {
+ RenderOpenIdMessageTransmissionAsAnchorAttributes(writer, request, tooltip);
+ } else {
+ tooltip = OpenIdStrings.OpenIdEndpointNotFound;
+ }
+ } else {
+ writer.AddAttribute(HtmlTextWriterAttribute.Href, this.Page.ClientScript.GetPostBackClientHyperlink(this, null));
+ }
+
+ 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/OpenId/RelyingParty/OpenIdEventArgs.cs b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/OpenIdEventArgs.cs
new file mode 100644
index 0000000..5668cf4
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/OpenIdEventArgs.cs
@@ -0,0 +1,74 @@
+//-----------------------------------------------------------------------
+// <copyright file="OpenIdEventArgs.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OpenId.RelyingParty {
+ using System;
+ using System.Collections.Generic;
+ using System.Diagnostics.Contracts;
+ using System.Linq;
+ using System.Text;
+ using DotNetOpenAuth.Messaging;
+
+ /// <summary>
+ /// The event details passed to event handlers.
+ /// </summary>
+ public class OpenIdEventArgs : EventArgs {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="OpenIdEventArgs"/> class
+ /// with minimal information of an incomplete or failed authentication attempt.
+ /// </summary>
+ /// <param name="request">The outgoing authentication request.</param>
+ internal OpenIdEventArgs(IAuthenticationRequest request) {
+ Contract.Requires<ArgumentNullException>(request != null);
+
+ this.Request = request;
+ this.ClaimedIdentifier = request.ClaimedIdentifier;
+ this.IsDirectedIdentity = request.IsDirectedIdentity;
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="OpenIdEventArgs"/> class
+ /// with information on a completed authentication attempt
+ /// (whether that attempt was successful or not).
+ /// </summary>
+ /// <param name="response">The incoming authentication response.</param>
+ internal OpenIdEventArgs(IAuthenticationResponse response) {
+ Contract.Requires<ArgumentNullException>(response != null);
+
+ this.Response = response;
+ this.ClaimedIdentifier = response.ClaimedIdentifier;
+ }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether to cancel
+ /// the OpenID authentication and/or login process.
+ /// </summary>
+ public bool Cancel { get; set; }
+
+ /// <summary>
+ /// Gets the Identifier the user is claiming to own. Or null if the user
+ /// is using Directed Identity.
+ /// </summary>
+ public Identifier ClaimedIdentifier { get; private set; }
+
+ /// <summary>
+ /// Gets a value indicating whether the user has selected to let his Provider determine
+ /// the ClaimedIdentifier to use as part of successful authentication.
+ /// </summary>
+ public bool IsDirectedIdentity { get; private set; }
+
+ /// <summary>
+ /// Gets the details of the OpenID authentication request,
+ /// and allows for adding extensions.
+ /// </summary>
+ public IAuthenticationRequest Request { get; private set; }
+
+ /// <summary>
+ /// Gets the details of the OpenID authentication response.
+ /// </summary>
+ public IAuthenticationResponse Response { get; private set; }
+ }
+}
diff --git a/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/OpenIdLogin.cs b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/OpenIdLogin.cs
new file mode 100644
index 0000000..eccdacf
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/OpenIdLogin.cs
@@ -0,0 +1,1001 @@
+//-----------------------------------------------------------------------
+// <copyright file="OpenIdLogin.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OpenId.RelyingParty {
+ using System;
+ using System.ComponentModel;
+ using System.Diagnostics.CodeAnalysis;
+ using System.Diagnostics.Contracts;
+ using System.Globalization;
+ using System.Linq;
+ using System.Web.UI;
+ using System.Web.UI.HtmlControls;
+ using System.Web.UI.WebControls;
+ using DotNetOpenAuth.Messaging;
+
+ /// <summary>
+ /// An ASP.NET control providing a complete OpenID login experience.
+ /// </summary>
+ [SuppressMessage("Microsoft.Naming", "CA1726:UsePreferredTerms", MessageId = "Login", Justification = "Legacy code")]
+ [DefaultProperty("Text"), ValidationProperty("Text")]
+ [ToolboxData("<{0}:OpenIdLogin runat=\"server\" />")]
+ public class OpenIdLogin : OpenIdTextBox {
+ #region Property defaults
+
+ /// <summary>
+ /// The default value for the <see cref="RegisterToolTip"/> property.
+ /// </summary>
+ private const string RegisterToolTipDefault = "Sign up free for an OpenID with MyOpenID now.";
+
+ /// <summary>
+ /// The default value for the <see cref="RememberMeText"/> property.
+ /// </summary>
+ private const string RememberMeTextDefault = "Remember me";
+
+ /// <summary>
+ /// The default value for the <see cref="ButtonText"/> property.
+ /// </summary>
+ private const string ButtonTextDefault = "Login »";
+
+ /// <summary>
+ /// The default value for the <see cref="CanceledText"/> property.
+ /// </summary>
+ private const string CanceledTextDefault = "Login canceled.";
+
+ /// <summary>
+ /// The default value for the <see cref="FailedMessageText"/> property.
+ /// </summary>
+ private const string FailedMessageTextDefault = "Login failed: {0}";
+
+ /// <summary>
+ /// The default value for the <see cref="ExamplePrefix"/> property.
+ /// </summary>
+ private const string ExamplePrefixDefault = "Example:";
+
+ /// <summary>
+ /// The default value for the <see cref="ExampleUrl"/> property.
+ /// </summary>
+ private const string ExampleUrlDefault = "http://your.name.myopenid.com";
+
+ /// <summary>
+ /// The default value for the <see cref="LabelText"/> property.
+ /// </summary>
+ private const string LabelTextDefault = "OpenID Login:";
+
+ /// <summary>
+ /// The default value for the <see cref="RequiredText"/> property.
+ /// </summary>
+ private const string RequiredTextDefault = "Provide an OpenID first.";
+
+ /// <summary>
+ /// The default value for the <see cref="UriFormatText"/> property.
+ /// </summary>
+ private const string UriFormatTextDefault = "Invalid OpenID URL.";
+
+ /// <summary>
+ /// The default value for the <see cref="RegisterText"/> property.
+ /// </summary>
+ private const string RegisterTextDefault = "register";
+
+ /// <summary>
+ /// The default value for the <see cref="RegisterUrl"/> property.
+ /// </summary>
+ private const string RegisterUrlDefault = "https://www.myopenid.com/signup";
+
+ /// <summary>
+ /// The default value for the <see cref="ButtonToolTip"/> property.
+ /// </summary>
+ private const string ButtonToolTipDefault = "Account login";
+
+ /// <summary>
+ /// The default value for the <see cref="ValidationGroup"/> property.
+ /// </summary>
+ private const string ValidationGroupDefault = "OpenIdLogin";
+
+ /// <summary>
+ /// The default value for the <see cref="RegisterVisible"/> property.
+ /// </summary>
+ private const bool RegisterVisibleDefault = true;
+
+ /// <summary>
+ /// The default value for the <see cref="RememberMeVisible"/> property.
+ /// </summary>
+ private const bool RememberMeVisibleDefault = false;
+
+ /// <summary>
+ /// The default value for the <see cref="RememberMe"/> property.
+ /// </summary>
+ private const bool RememberMeDefault = false;
+
+ /// <summary>
+ /// The default value for the <see cref="UriValidatorEnabled"/> property.
+ /// </summary>
+ private const bool UriValidatorEnabledDefault = true;
+
+ #endregion
+
+ #region Property viewstate keys
+
+ /// <summary>
+ /// The viewstate key to use for the <see cref="FailedMessageText"/> property.
+ /// </summary>
+ private const string FailedMessageTextViewStateKey = "FailedMessageText";
+
+ /// <summary>
+ /// The viewstate key to use for the <see cref="CanceledText"/> property.
+ /// </summary>
+ private const string CanceledTextViewStateKey = "CanceledText";
+
+ /// <summary>
+ /// The viewstate key to use for the <see cref="IdSelectorIdentifier"/> property.
+ /// </summary>
+ private const string IdSelectorIdentifierViewStateKey = "IdSelectorIdentifier";
+
+ #endregion
+
+ /// <summary>
+ /// The HTML to append to the <see cref="RequiredText"/> property value when rendering.
+ /// </summary>
+ private const string RequiredTextSuffix = "<br/>";
+
+ /// <summary>
+ /// The number to add to <see cref="TabIndex"/> to get the tab index of the textbox control.
+ /// </summary>
+ private const short TextBoxTabIndexOffset = 0;
+
+ /// <summary>
+ /// The number to add to <see cref="TabIndex"/> to get the tab index of the login button control.
+ /// </summary>
+ private const short LoginButtonTabIndexOffset = 1;
+
+ /// <summary>
+ /// The number to add to <see cref="TabIndex"/> to get the tab index of the remember me checkbox control.
+ /// </summary>
+ private const short RememberMeTabIndexOffset = 2;
+
+ /// <summary>
+ /// The number to add to <see cref="TabIndex"/> to get the tab index of the register link control.
+ /// </summary>
+ private const short RegisterTabIndexOffset = 3;
+
+ #region Controls
+
+ /// <summary>
+ /// The control into which all other controls are added.
+ /// </summary>
+ private Panel panel;
+
+ /// <summary>
+ /// The Login button.
+ /// </summary>
+ private Button loginButton;
+
+ /// <summary>
+ /// The label that presents the text box.
+ /// </summary>
+ private HtmlGenericControl label;
+
+ /// <summary>
+ /// The validator that flags an empty text box.
+ /// </summary>
+ private RequiredFieldValidator requiredValidator;
+
+ /// <summary>
+ /// The validator that flags invalid formats of OpenID identifiers.
+ /// </summary>
+ private CustomValidator identifierFormatValidator;
+
+ /// <summary>
+ /// The label that precedes an example OpenID identifier.
+ /// </summary>
+ private Label examplePrefixLabel;
+
+ /// <summary>
+ /// The label that contains the example OpenID identifier.
+ /// </summary>
+ private Label exampleUrlLabel;
+
+ /// <summary>
+ /// A link to allow the user to create an account with a popular OpenID Provider.
+ /// </summary>
+ private HyperLink registerLink;
+
+ /// <summary>
+ /// The Remember Me checkbox.
+ /// </summary>
+ private CheckBox rememberMeCheckBox;
+
+ /// <summary>
+ /// The javascript snippet that activates the ID Selector javascript control.
+ /// </summary>
+ private Literal idselectorJavascript;
+
+ /// <summary>
+ /// The label that will display login failure messages.
+ /// </summary>
+ private Label errorLabel;
+
+ #endregion
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="OpenIdLogin"/> class.
+ /// </summary>
+ public OpenIdLogin() {
+ }
+
+ #region Events
+
+ /// <summary>
+ /// Fired when the Remember Me checkbox is changed by the user.
+ /// </summary>
+ [Description("Fires when the Remember Me checkbox is changed by the user.")]
+ public event EventHandler RememberMeChanged;
+
+ #endregion
+
+ #region Properties
+
+ /// <summary>
+ /// Gets a <see cref="T:System.Web.UI.ControlCollection"/> object that represents the child controls for a specified server control in the UI hierarchy.
+ /// </summary>
+ /// <returns>
+ /// The collection of child controls for the specified server control.
+ /// </returns>
+ public override ControlCollection Controls {
+ get {
+ this.EnsureChildControls();
+ return base.Controls;
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets the caption that appears before the text box.
+ /// </summary>
+ [Bindable(true)]
+ [Category("Appearance")]
+ [DefaultValue(LabelTextDefault)]
+ [Localizable(true)]
+ [Description("The caption that appears before the text box.")]
+ public string LabelText {
+ get {
+ EnsureChildControls();
+ return this.label.InnerText;
+ }
+
+ set {
+ EnsureChildControls();
+ this.label.InnerText = value;
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets the text that introduces the example OpenID url.
+ /// </summary>
+ [Bindable(true)]
+ [Category("Appearance")]
+ [DefaultValue(ExamplePrefixDefault)]
+ [Localizable(true)]
+ [Description("The text that introduces the example OpenID url.")]
+ public string ExamplePrefix {
+ get {
+ EnsureChildControls();
+ return this.examplePrefixLabel.Text;
+ }
+
+ set {
+ EnsureChildControls();
+ this.examplePrefixLabel.Text = value;
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets the example OpenID Identifier to display to the user.
+ /// </summary>
+ [SuppressMessage("Microsoft.Design", "CA1056:UriPropertiesShouldNotBeStrings", Justification = "Property grid only supports primitive types.")]
+ [Bindable(true)]
+ [Category("Appearance")]
+ [DefaultValue(ExampleUrlDefault)]
+ [Localizable(true)]
+ [Description("The example OpenID Identifier to display to the user.")]
+ public string ExampleUrl {
+ get {
+ EnsureChildControls();
+ return this.exampleUrlLabel.Text;
+ }
+
+ set {
+ EnsureChildControls();
+ this.exampleUrlLabel.Text = value;
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets the text to display if the user attempts to login
+ /// without providing an Identifier.
+ /// </summary>
+ [SuppressMessage("Microsoft.Naming", "CA2204:Literals should be spelled correctly", MessageId = "br", Justification = "HTML"), Bindable(true)]
+ [Category("Appearance")]
+ [DefaultValue(RequiredTextDefault)]
+ [Localizable(true)]
+ [Description("The text to display if the user attempts to login without providing an Identifier.")]
+ public string RequiredText {
+ get {
+ EnsureChildControls();
+ return this.requiredValidator.Text.Substring(0, this.requiredValidator.Text.Length - RequiredTextSuffix.Length);
+ }
+
+ set {
+ EnsureChildControls();
+ this.requiredValidator.ErrorMessage = this.requiredValidator.Text = value + RequiredTextSuffix;
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets the text to display if the user provides an invalid form for an Identifier.
+ /// </summary>
+ [SuppressMessage("Microsoft.Naming", "CA2204:Literals should be spelled correctly", MessageId = "br", Justification = "HTML"), SuppressMessage("Microsoft.Design", "CA1056:UriPropertiesShouldNotBeStrings", Justification = "Property grid only supports primitive types.")]
+ [Bindable(true)]
+ [Category("Appearance")]
+ [DefaultValue(UriFormatTextDefault)]
+ [Localizable(true)]
+ [Description("The text to display if the user provides an invalid form for an Identifier.")]
+ public string UriFormatText {
+ get {
+ EnsureChildControls();
+ return this.identifierFormatValidator.Text.Substring(0, this.identifierFormatValidator.Text.Length - RequiredTextSuffix.Length);
+ }
+
+ set {
+ EnsureChildControls();
+ this.identifierFormatValidator.ErrorMessage = this.identifierFormatValidator.Text = value + RequiredTextSuffix;
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether to perform Identifier
+ /// format validation prior to an authentication attempt.
+ /// </summary>
+ [Bindable(true)]
+ [Category("Behavior")]
+ [DefaultValue(UriValidatorEnabledDefault)]
+ [Description("Whether to perform Identifier format validation prior to an authentication attempt.")]
+ public bool UriValidatorEnabled {
+ get {
+ EnsureChildControls();
+ return this.identifierFormatValidator.Enabled;
+ }
+
+ set {
+ EnsureChildControls();
+ this.identifierFormatValidator.Enabled = value;
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets the text of the link users can click on to obtain an OpenID.
+ /// </summary>
+ [Bindable(true)]
+ [Category("Appearance")]
+ [DefaultValue(RegisterTextDefault)]
+ [Localizable(true)]
+ [Description("The text of the link users can click on to obtain an OpenID.")]
+ public string RegisterText {
+ get {
+ EnsureChildControls();
+ return this.registerLink.Text;
+ }
+
+ set {
+ EnsureChildControls();
+ this.registerLink.Text = value;
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets the URL to link users to who click the link to obtain a new OpenID.
+ /// </summary>
+ [SuppressMessage("Microsoft.Design", "CA1056:UriPropertiesShouldNotBeStrings", Justification = "Property grid only supports primitive types.")]
+ [Bindable(true)]
+ [Category("Appearance")]
+ [DefaultValue(RegisterUrlDefault)]
+ [Localizable(true)]
+ [Description("The URL to link users to who click the link to obtain a new OpenID.")]
+ public string RegisterUrl {
+ get {
+ EnsureChildControls();
+ return this.registerLink.NavigateUrl;
+ }
+
+ set {
+ EnsureChildControls();
+ this.registerLink.NavigateUrl = value;
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets the text of the tooltip to display when the user hovers
+ /// over the link to obtain a new OpenID.
+ /// </summary>
+ [Bindable(true)]
+ [Category("Appearance")]
+ [DefaultValue(RegisterToolTipDefault)]
+ [Localizable(true)]
+ [Description("The text of the tooltip to display when the user hovers over the link to obtain a new OpenID.")]
+ public string RegisterToolTip {
+ get {
+ EnsureChildControls();
+ return this.registerLink.ToolTip;
+ }
+
+ set {
+ EnsureChildControls();
+ this.registerLink.ToolTip = value;
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether to display a link to
+ /// allow users to easily obtain a new OpenID.
+ /// </summary>
+ [Bindable(true)]
+ [Category("Appearance")]
+ [DefaultValue(RegisterVisibleDefault)]
+ [Description("Whether to display a link to allow users to easily obtain a new OpenID.")]
+ public bool RegisterVisible {
+ get {
+ EnsureChildControls();
+ return this.registerLink.Visible;
+ }
+
+ set {
+ EnsureChildControls();
+ this.registerLink.Visible = value;
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets the text that appears on the button that initiates login.
+ /// </summary>
+ [Bindable(true)]
+ [Category("Appearance")]
+ [DefaultValue(ButtonTextDefault)]
+ [Localizable(true)]
+ [Description("The text that appears on the button that initiates login.")]
+ public string ButtonText {
+ get {
+ EnsureChildControls();
+ return this.loginButton.Text;
+ }
+
+ set {
+ EnsureChildControls();
+ this.loginButton.Text = value;
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets the text of the "Remember Me" checkbox.
+ /// </summary>
+ [Bindable(true)]
+ [Category("Appearance")]
+ [DefaultValue(RememberMeTextDefault)]
+ [Localizable(true)]
+ [Description("The text of the \"Remember Me\" checkbox.")]
+ public string RememberMeText {
+ get {
+ EnsureChildControls();
+ return this.rememberMeCheckBox.Text;
+ }
+
+ set {
+ EnsureChildControls();
+ this.rememberMeCheckBox.Text = value;
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets the message display in the event of a failed
+ /// authentication. {0} may be used to insert the actual error.
+ /// </summary>
+ [Bindable(true)]
+ [Category("Appearance")]
+ [DefaultValue(FailedMessageTextDefault)]
+ [Localizable(true)]
+ [Description("The message display in the event of a failed authentication. {0} may be used to insert the actual error.")]
+ public string FailedMessageText {
+ get { return (string)ViewState[FailedMessageTextViewStateKey] ?? FailedMessageTextDefault; }
+ set { ViewState[FailedMessageTextViewStateKey] = value; }
+ }
+
+ /// <summary>
+ /// Gets or sets the text to display in the event of an authentication canceled at the Provider.
+ /// </summary>
+ [Bindable(true)]
+ [Category("Appearance")]
+ [DefaultValue(CanceledTextDefault)]
+ [Localizable(true)]
+ [Description("The text to display in the event of an authentication canceled at the Provider.")]
+ public string CanceledText {
+ get { return (string)ViewState[CanceledTextViewStateKey] ?? CanceledTextDefault; }
+ set { ViewState[CanceledTextViewStateKey] = value; }
+ }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether the "Remember Me" checkbox should be displayed.
+ /// </summary>
+ [Bindable(true)]
+ [Category("Appearance")]
+ [DefaultValue(RememberMeVisibleDefault)]
+ [Description("Whether the \"Remember Me\" checkbox should be displayed.")]
+ public bool RememberMeVisible {
+ get {
+ EnsureChildControls();
+ return this.rememberMeCheckBox.Visible;
+ }
+
+ set {
+ EnsureChildControls();
+ this.rememberMeCheckBox.Visible = value;
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether a successful authentication should result in a persistent
+ /// cookie being saved to the browser.
+ /// </summary>
+ [Bindable(true)]
+ [Category("Appearance")]
+ [DefaultValue(RememberMeDefault)]
+ [Description("Whether a successful authentication should result in a persistent cookie being saved to the browser.")]
+ public bool RememberMe {
+ get { return this.UsePersistentCookie != LogOnPersistence.Session; }
+ set { this.UsePersistentCookie = value ? LogOnPersistence.PersistentAuthentication : LogOnPersistence.Session; }
+ }
+
+ /// <summary>
+ /// Gets or sets the starting tab index to distribute across the controls.
+ /// </summary>
+ [SuppressMessage("Microsoft.Usage", "CA2233:OperationsShouldNotOverflow", MessageId = "value+1", Justification = "Overflow would provide desired UI behavior.")]
+ [SuppressMessage("Microsoft.Usage", "CA2233:OperationsShouldNotOverflow", MessageId = "value+2", Justification = "Overflow would provide desired UI behavior.")]
+ [SuppressMessage("Microsoft.Usage", "CA2233:OperationsShouldNotOverflow", MessageId = "value+3", Justification = "Overflow would provide desired UI behavior.")]
+ public override short TabIndex {
+ get {
+ return base.TabIndex;
+ }
+
+ set {
+ unchecked {
+ EnsureChildControls();
+ base.TabIndex = (short)(value + TextBoxTabIndexOffset);
+ this.loginButton.TabIndex = (short)(value + LoginButtonTabIndexOffset);
+ this.rememberMeCheckBox.TabIndex = (short)(value + RememberMeTabIndexOffset);
+ this.registerLink.TabIndex = (short)(value + RegisterTabIndexOffset);
+ }
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets the tooltip to display when the user hovers over the login button.
+ /// </summary>
+ [Bindable(true)]
+ [Category("Appearance")]
+ [DefaultValue(ButtonToolTipDefault)]
+ [Localizable(true)]
+ [Description("The tooltip to display when the user hovers over the login button.")]
+ public string ButtonToolTip {
+ get {
+ EnsureChildControls();
+ return this.loginButton.ToolTip;
+ }
+
+ set {
+ EnsureChildControls();
+ this.loginButton.ToolTip = value;
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets the validation group that the login button and text box validator belong to.
+ /// </summary>
+ [Category("Behavior")]
+ [DefaultValue(ValidationGroupDefault)]
+ [Description("The validation group that the login button and text box validator belong to.")]
+ public string ValidationGroup {
+ get {
+ EnsureChildControls();
+ return this.requiredValidator.ValidationGroup;
+ }
+
+ set {
+ EnsureChildControls();
+ this.requiredValidator.ValidationGroup = value;
+ this.loginButton.ValidationGroup = value;
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets the unique hash string that ends your idselector.com account.
+ /// </summary>
+ [Category("Behavior")]
+ [Description("The unique hash string that ends your idselector.com account.")]
+ public string IdSelectorIdentifier {
+ get { return (string)(ViewState[IdSelectorIdentifierViewStateKey]); }
+ set { ViewState[IdSelectorIdentifierViewStateKey] = value; }
+ }
+
+ #endregion
+
+ #region Properties to hide
+
+ /// <summary>
+ /// Gets or sets a value indicating whether a FormsAuthentication
+ /// cookie should persist across user sessions.
+ /// </summary>
+ [Browsable(false), Bindable(false)]
+ public override LogOnPersistence UsePersistentCookie {
+ get {
+ return base.UsePersistentCookie;
+ }
+
+ set {
+ base.UsePersistentCookie = value;
+
+ if (this.rememberMeCheckBox != null) {
+ // use conditional here to prevent infinite recursion
+ // with CheckedChanged event.
+ bool rememberMe = value != LogOnPersistence.Session;
+ if (this.rememberMeCheckBox.Checked != rememberMe) {
+ this.rememberMeCheckBox.Checked = rememberMe;
+ }
+ }
+ }
+ }
+
+ #endregion
+
+ /// <summary>
+ /// Outputs server control content to a provided <see cref="T:System.Web.UI.HtmlTextWriter"/> object and stores tracing information about the control if tracing is enabled.
+ /// </summary>
+ /// <param name="writer">The <see cref="T:System.Web.UI.HTmlTextWriter"/> object that receives the control content.</param>
+ public override void RenderControl(HtmlTextWriter writer) {
+ this.RenderChildren(writer);
+ }
+
+ /// <summary>
+ /// Creates the child controls.
+ /// </summary>
+ protected override void CreateChildControls() {
+ this.InitializeControls();
+
+ // Just add the panel we've assembled earlier.
+ base.Controls.Add(this.panel);
+ }
+
+ /// <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.EnsureChildControls();
+ }
+
+ /// <summary>
+ /// Initializes the child controls.
+ /// </summary>
+ [SuppressMessage("Microsoft.Globalization", "CA1303:Do not pass literals as localized parameters", MessageId = "System.Web.UI.WebControls.WebControl.set_ToolTip(System.String)", Justification = "By design"), SuppressMessage("Microsoft.Globalization", "CA1303:Do not pass literals as localized parameters", MessageId = "System.Web.UI.WebControls.Label.set_Text(System.String)", Justification = "By design"), SuppressMessage("Microsoft.Globalization", "CA1303:Do not pass literals as localized parameters", MessageId = "System.Web.UI.WebControls.HyperLink.set_Text(System.String)", Justification = "By design"), SuppressMessage("Microsoft.Globalization", "CA1303:Do not pass literals as localized parameters", MessageId = "System.Web.UI.WebControls.CheckBox.set_Text(System.String)", Justification = "By design"), SuppressMessage("Microsoft.Globalization", "CA1303:Do not pass literals as localized parameters", MessageId = "System.Web.UI.WebControls.Button.set_Text(System.String)", Justification = "By design"), SuppressMessage("Microsoft.Globalization", "CA1303:Do not pass literals as localized parameters", MessageId = "System.Web.UI.WebControls.BaseValidator.set_ErrorMessage(System.String)", Justification = "By design"), SuppressMessage("Microsoft.Naming", "CA2204:Literals should be spelled correctly", MessageId = "br", Justification = "HTML"), SuppressMessage("Microsoft.Naming", "CA2204:Literals should be spelled correctly", MessageId = "OpenID", Justification = "It is correct"), SuppressMessage("Microsoft.Naming", "CA2204:Literals should be spelled correctly", MessageId = "MyOpenID", Justification = "Correct spelling"), SuppressMessage("Microsoft.Naming", "CA2204:Literals should be spelled correctly", MessageId = "myopenid", Justification = "URL")]
+ protected void InitializeControls() {
+ this.panel = new Panel();
+
+ Table table = new Table();
+ try {
+ TableRow row1, row2, row3;
+ TableCell cell;
+ table.Rows.Add(row1 = new TableRow());
+ table.Rows.Add(row2 = new TableRow());
+ table.Rows.Add(row3 = new TableRow());
+
+ // top row, left cell
+ cell = new TableCell();
+ try {
+ this.label = new HtmlGenericControl("label");
+ this.label.InnerText = LabelTextDefault;
+ cell.Controls.Add(this.label);
+ row1.Cells.Add(cell);
+ } catch {
+ cell.Dispose();
+ throw;
+ }
+
+ // top row, middle cell
+ cell = new TableCell();
+ try {
+ cell.Controls.Add(new InPlaceControl(this));
+ row1.Cells.Add(cell);
+ } catch {
+ cell.Dispose();
+ throw;
+ }
+
+ // top row, right cell
+ cell = new TableCell();
+ try {
+ this.loginButton = new Button();
+ this.loginButton.ID = this.ID + "_loginButton";
+ this.loginButton.Text = ButtonTextDefault;
+ this.loginButton.ToolTip = ButtonToolTipDefault;
+ this.loginButton.Click += this.LoginButton_Click;
+ this.loginButton.ValidationGroup = ValidationGroupDefault;
+#if !Mono
+ this.panel.DefaultButton = this.loginButton.ID;
+#endif
+ cell.Controls.Add(this.loginButton);
+ row1.Cells.Add(cell);
+ } catch {
+ cell.Dispose();
+ throw;
+ }
+
+ // middle row, left cell
+ row2.Cells.Add(new TableCell());
+
+ // middle row, middle cell
+ cell = new TableCell();
+ try {
+ cell.Style[HtmlTextWriterStyle.Color] = "gray";
+ cell.Style[HtmlTextWriterStyle.FontSize] = "smaller";
+ this.requiredValidator = new RequiredFieldValidator();
+ this.requiredValidator.ErrorMessage = RequiredTextDefault + RequiredTextSuffix;
+ this.requiredValidator.Text = RequiredTextDefault + RequiredTextSuffix;
+ this.requiredValidator.Display = ValidatorDisplay.Dynamic;
+ this.requiredValidator.ValidationGroup = ValidationGroupDefault;
+ cell.Controls.Add(this.requiredValidator);
+ this.identifierFormatValidator = new CustomValidator();
+ this.identifierFormatValidator.ErrorMessage = UriFormatTextDefault + RequiredTextSuffix;
+ this.identifierFormatValidator.Text = UriFormatTextDefault + RequiredTextSuffix;
+ this.identifierFormatValidator.ServerValidate += this.IdentifierFormatValidator_ServerValidate;
+ this.identifierFormatValidator.Enabled = UriValidatorEnabledDefault;
+ this.identifierFormatValidator.Display = ValidatorDisplay.Dynamic;
+ this.identifierFormatValidator.ValidationGroup = ValidationGroupDefault;
+ cell.Controls.Add(this.identifierFormatValidator);
+ this.errorLabel = new Label();
+ this.errorLabel.EnableViewState = false;
+ this.errorLabel.ForeColor = System.Drawing.Color.Red;
+ this.errorLabel.Style[HtmlTextWriterStyle.Display] = "block"; // puts it on its own line
+ this.errorLabel.Visible = false;
+ cell.Controls.Add(this.errorLabel);
+ this.examplePrefixLabel = new Label();
+ this.examplePrefixLabel.Text = ExamplePrefixDefault;
+ cell.Controls.Add(this.examplePrefixLabel);
+ cell.Controls.Add(new LiteralControl(" "));
+ this.exampleUrlLabel = new Label();
+ this.exampleUrlLabel.Font.Bold = true;
+ this.exampleUrlLabel.Text = ExampleUrlDefault;
+ cell.Controls.Add(this.exampleUrlLabel);
+ row2.Cells.Add(cell);
+ } catch {
+ cell.Dispose();
+ throw;
+ }
+
+ // middle row, right cell
+ cell = new TableCell();
+ try {
+ cell.Style[HtmlTextWriterStyle.Color] = "gray";
+ cell.Style[HtmlTextWriterStyle.FontSize] = "smaller";
+ cell.Style[HtmlTextWriterStyle.TextAlign] = "center";
+ this.registerLink = new HyperLink();
+ this.registerLink.Text = RegisterTextDefault;
+ this.registerLink.ToolTip = RegisterToolTipDefault;
+ this.registerLink.NavigateUrl = RegisterUrlDefault;
+ this.registerLink.Visible = RegisterVisibleDefault;
+ cell.Controls.Add(this.registerLink);
+ row2.Cells.Add(cell);
+ } catch {
+ cell.Dispose();
+ throw;
+ }
+
+ // bottom row, left cell
+ cell = new TableCell();
+ row3.Cells.Add(cell);
+
+ // bottom row, middle cell
+ cell = new TableCell();
+ try {
+ this.rememberMeCheckBox = new CheckBox();
+ this.rememberMeCheckBox.Text = RememberMeTextDefault;
+ this.rememberMeCheckBox.Checked = this.UsePersistentCookie != LogOnPersistence.Session;
+ this.rememberMeCheckBox.Visible = RememberMeVisibleDefault;
+ this.rememberMeCheckBox.CheckedChanged += this.RememberMeCheckBox_CheckedChanged;
+ cell.Controls.Add(this.rememberMeCheckBox);
+ row3.Cells.Add(cell);
+ } catch {
+ cell.Dispose();
+ throw;
+ }
+
+ // bottom row, right cell
+ cell = new TableCell();
+ try {
+ row3.Cells.Add(cell);
+ } catch {
+ cell.Dispose();
+ throw;
+ }
+
+ // this sets all the controls' tab indexes
+ this.TabIndex = TabIndexDefault;
+
+ this.panel.Controls.Add(table);
+ } catch {
+ table.Dispose();
+ throw;
+ }
+
+ this.idselectorJavascript = new Literal();
+ this.panel.Controls.Add(this.idselectorJavascript);
+ }
+
+ /// <summary>
+ /// Raises the <see cref="E:System.Web.UI.Control.Init"/> event.
+ /// </summary>
+ /// <param name="e">An <see cref="T:System.EventArgs"/> object that contains the event data.</param>
+ protected override void OnInit(EventArgs e) {
+ this.SetChildControlReferenceIds();
+
+ base.OnInit(e);
+ }
+
+ /// <summary>
+ /// Renders the child controls.
+ /// </summary>
+ /// <param name="writer">The <see cref="T:System.Web.UI.HtmlTextWriter"/> object that receives the rendered content.</param>
+ [SuppressMessage("Microsoft.Globalization", "CA1303:Do not pass literals as localized parameters", MessageId = "System.Web.UI.WebControls.Literal.set_Text(System.String)", Justification = "By design"), SuppressMessage("Microsoft.Naming", "CA2204:Literals should be spelled correctly", MessageId = "idselector", Justification = "HTML"), SuppressMessage("Microsoft.Naming", "CA2204:Literals should be spelled correctly", MessageId = "charset", Justification = "html"), SuppressMessage("Microsoft.Naming", "CA2204:Literals should be spelled correctly", MessageId = "src", Justification = "html"), SuppressMessage("Microsoft.Naming", "CA2204:Literals should be spelled correctly", MessageId = "openidselector", Justification = "html"), SuppressMessage("Microsoft.Naming", "CA2204:Literals should be spelled correctly", MessageId = "idselectorinputid", Justification = "html")]
+ protected override void RenderChildren(HtmlTextWriter writer) {
+ if (!this.DesignMode) {
+ this.label.Attributes["for"] = this.ClientID;
+
+ if (!string.IsNullOrEmpty(this.IdSelectorIdentifier)) {
+ this.idselectorJavascript.Visible = true;
+ this.idselectorJavascript.Text = @"<script type='text/javascript'><!--
+idselector_input_id = '" + this.ClientID + @"';
+// --></script>
+<script type='text/javascript' id='__openidselector' src='https://www.idselector.com/selector/" + this.IdSelectorIdentifier + @"' charset='utf-8'></script>";
+ } else {
+ this.idselectorJavascript.Visible = false;
+ }
+ }
+
+ base.RenderChildren(writer);
+ }
+
+ /// <summary>
+ /// Adds failure handling to display an error message to the user.
+ /// </summary>
+ /// <param name="response">The response.</param>
+ protected override void OnFailed(IAuthenticationResponse response) {
+ base.OnFailed(response);
+
+ if (!string.IsNullOrEmpty(this.FailedMessageText)) {
+ this.errorLabel.Text = string.Format(CultureInfo.CurrentCulture, this.FailedMessageText, response.Exception.ToStringDescriptive());
+ this.errorLabel.Visible = true;
+ }
+ }
+
+ /// <summary>
+ /// Adds authentication cancellation behavior to display a message to the user.
+ /// </summary>
+ /// <param name="response">The response.</param>
+ protected override void OnCanceled(IAuthenticationResponse response) {
+ base.OnCanceled(response);
+
+ if (!string.IsNullOrEmpty(this.CanceledText)) {
+ this.errorLabel.Text = this.CanceledText;
+ this.errorLabel.Visible = true;
+ }
+ }
+
+ /// <summary>
+ /// Fires the <see cref="RememberMeChanged"/> event.
+ /// </summary>
+ protected virtual void OnRememberMeChanged() {
+ EventHandler rememberMeChanged = this.RememberMeChanged;
+ if (rememberMeChanged != null) {
+ rememberMeChanged(this, new EventArgs());
+ }
+ }
+
+ /// <summary>
+ /// Handles the ServerValidate event of the identifierFormatValidator control.
+ /// </summary>
+ /// <param name="source">The source of the event.</param>
+ /// <param name="args">The <see cref="System.Web.UI.WebControls.ServerValidateEventArgs"/> instance containing the event data.</param>
+ private void IdentifierFormatValidator_ServerValidate(object source, ServerValidateEventArgs args) {
+ args.IsValid = Identifier.IsValid(args.Value);
+ }
+
+ /// <summary>
+ /// Handles the CheckedChanged event of the rememberMeCheckBox control.
+ /// </summary>
+ /// <param name="sender">The source of the event.</param>
+ /// <param name="e">The <see cref="System.EventArgs"/> instance containing the event data.</param>
+ private void RememberMeCheckBox_CheckedChanged(object sender, EventArgs e) {
+ this.RememberMe = this.rememberMeCheckBox.Checked;
+ this.OnRememberMeChanged();
+ }
+
+ /// <summary>
+ /// Handles the Click event of the loginButton control.
+ /// </summary>
+ /// <param name="sender">The source of the event.</param>
+ /// <param name="e">The <see cref="System.EventArgs"/> instance containing the event data.</param>
+ private void LoginButton_Click(object sender, EventArgs e) {
+ if (!this.Page.IsValid) {
+ return;
+ }
+
+ IAuthenticationRequest request = this.CreateRequests().FirstOrDefault();
+ if (request != null) {
+ this.LogOn(request);
+ } else {
+ if (!string.IsNullOrEmpty(this.FailedMessageText)) {
+ this.errorLabel.Text = string.Format(CultureInfo.CurrentCulture, this.FailedMessageText, OpenIdStrings.OpenIdEndpointNotFound);
+ this.errorLabel.Visible = true;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Renders the control inner.
+ /// </summary>
+ /// <param name="writer">The writer.</param>
+ private void RenderControlInner(HtmlTextWriter writer) {
+ base.RenderControl(writer);
+ }
+
+ /// <summary>
+ /// Sets child control properties that depend on this control's ID.
+ /// </summary>
+ private void SetChildControlReferenceIds() {
+ this.EnsureChildControls();
+ this.EnsureID();
+ ErrorUtilities.VerifyInternal(!string.IsNullOrEmpty(this.ID), "No control ID available yet!");
+ this.requiredValidator.ControlToValidate = this.ID;
+ this.requiredValidator.ID = this.ID + "_requiredValidator";
+ this.identifierFormatValidator.ControlToValidate = this.ID;
+ this.identifierFormatValidator.ID = this.ID + "_identifierFormatValidator";
+ }
+
+ /// <summary>
+ /// A control that acts as a placeholder to indicate where
+ /// the OpenIdLogin control should render its OpenIdTextBox parent.
+ /// </summary>
+ private class InPlaceControl : PlaceHolder {
+ /// <summary>
+ /// The owning control to render.
+ /// </summary>
+ private OpenIdLogin renderControl;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="InPlaceControl"/> class.
+ /// </summary>
+ /// <param name="renderControl">The render control.</param>
+ internal InPlaceControl(OpenIdLogin renderControl) {
+ this.renderControl = renderControl;
+ }
+
+ /// <summary>
+ /// Sends server control content to a provided <see cref="T:System.Web.UI.HtmlTextWriter"/> object, which writes the content to be rendered on the client.
+ /// </summary>
+ /// <param name="writer">The <see cref="T:System.Web.UI.HtmlTextWriter"/> object that receives the server control content.</param>
+ protected override void Render(HtmlTextWriter writer) {
+ this.renderControl.RenderControlInner(writer);
+ }
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/OpenIdMobileTextBox.cs b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/OpenIdMobileTextBox.cs
new file mode 100644
index 0000000..fc80b32
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/OpenIdMobileTextBox.cs
@@ -0,0 +1,778 @@
+//-----------------------------------------------------------------------
+// <copyright file="OpenIdMobileTextBox.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+[assembly: System.Web.UI.WebResource(DotNetOpenAuth.OpenId.RelyingParty.OpenIdMobileTextBox.EmbeddedLogoResourceName, "image/gif")]
+
+namespace DotNetOpenAuth.OpenId.RelyingParty {
+ using System;
+ using System.ComponentModel;
+ using System.Diagnostics;
+ using System.Diagnostics.CodeAnalysis;
+ using System.Diagnostics.Contracts;
+ using System.Globalization;
+ using System.Text.RegularExpressions;
+ using System.Web.Security;
+ using System.Web.UI;
+ using System.Web.UI.MobileControls;
+ using DotNetOpenAuth.Configuration;
+ using DotNetOpenAuth.Messaging;
+ using DotNetOpenAuth.OpenId.Extensions.SimpleRegistration;
+
+ /// <summary>
+ /// An ASP.NET control for mobile devices that provides a minimal text box that is OpenID-aware.
+ /// </summary>
+ [DefaultProperty("Text"), ValidationProperty("Text")]
+ [ToolboxData("<{0}:OpenIdMobileTextBox runat=\"server\" />")]
+ public class OpenIdMobileTextBox : TextBox {
+ /// <summary>
+ /// The name of the manifest stream containing the
+ /// OpenID logo that is placed inside the text box.
+ /// </summary>
+ internal const string EmbeddedLogoResourceName = OpenIdTextBox.EmbeddedLogoResourceName;
+
+ /// <summary>
+ /// Default value of <see cref="UsePersistentCookie"/>.
+ /// </summary>
+ protected const bool UsePersistentCookieDefault = false;
+
+ #region Property category constants
+
+ /// <summary>
+ /// The "Appearance" category for properties.
+ /// </summary>
+ private const string AppearanceCategory = "Appearance";
+
+ /// <summary>
+ /// The "Simple Registration" category for properties.
+ /// </summary>
+ private const string ProfileCategory = "Simple Registration";
+
+ /// <summary>
+ /// The "Behavior" category for properties.
+ /// </summary>
+ private const string BehaviorCategory = "Behavior";
+
+ #endregion
+
+ #region Property viewstate keys
+
+ /// <summary>
+ /// The viewstate key to use for the <see cref="RequestEmail"/> property.
+ /// </summary>
+ private const string RequestEmailViewStateKey = "RequestEmail";
+
+ /// <summary>
+ /// The viewstate key to use for the <see cref="RequestNickname"/> property.
+ /// </summary>
+ private const string RequestNicknameViewStateKey = "RequestNickname";
+
+ /// <summary>
+ /// The viewstate key to use for the <see cref="RequestPostalCode"/> property.
+ /// </summary>
+ private const string RequestPostalCodeViewStateKey = "RequestPostalCode";
+
+ /// <summary>
+ /// The viewstate key to use for the <see cref="RequestCountry"/> property.
+ /// </summary>
+ private const string RequestCountryViewStateKey = "RequestCountry";
+
+ /// <summary>
+ /// The viewstate key to use for the <see cref="RequireSsl"/> property.
+ /// </summary>
+ private const string RequireSslViewStateKey = "RequireSsl";
+
+ /// <summary>
+ /// The viewstate key to use for the <see cref="RequestLanguage"/> property.
+ /// </summary>
+ private const string RequestLanguageViewStateKey = "RequestLanguage";
+
+ /// <summary>
+ /// The viewstate key to use for the <see cref="RequestTimeZone"/> property.
+ /// </summary>
+ private const string RequestTimeZoneViewStateKey = "RequestTimeZone";
+
+ /// <summary>
+ /// The viewstate key to use for the <see cref="EnableRequestProfile"/> property.
+ /// </summary>
+ private const string EnableRequestProfileViewStateKey = "EnableRequestProfile";
+
+ /// <summary>
+ /// The viewstate key to use for the <see cref="PolicyUrl"/> property.
+ /// </summary>
+ private const string PolicyUrlViewStateKey = "PolicyUrl";
+
+ /// <summary>
+ /// The viewstate key to use for the <see cref="RequestFullName"/> property.
+ /// </summary>
+ private const string RequestFullNameViewStateKey = "RequestFullName";
+
+ /// <summary>
+ /// The viewstate key to use for the <see cref="UsePersistentCookie"/> property.
+ /// </summary>
+ private const string UsePersistentCookieViewStateKey = "UsePersistentCookie";
+
+ /// <summary>
+ /// The viewstate key to use for the <see cref="RequestGender"/> property.
+ /// </summary>
+ private const string RequestGenderViewStateKey = "RequestGender";
+
+ /// <summary>
+ /// The viewstate key to use for the <see cref="ReturnToUrl"/> property.
+ /// </summary>
+ private const string ReturnToUrlViewStateKey = "ReturnToUrl";
+
+ /// <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="ImmediateMode"/> property.
+ /// </summary>
+ private const string ImmediateModeViewStateKey = "ImmediateMode";
+
+ /// <summary>
+ /// The viewstate key to use for the <see cref="RequestBirthDate"/> property.
+ /// </summary>
+ private const string RequestBirthDateViewStateKey = "RequestBirthDate";
+
+ /// <summary>
+ /// The viewstate key to use for the <see cref="RealmUrl"/> property.
+ /// </summary>
+ private const string RealmUrlViewStateKey = "RealmUrl";
+
+ #endregion
+
+ #region Property defaults
+
+ /// <summary>
+ /// The default value for the <see cref="EnableRequestProfile"/> property.
+ /// </summary>
+ private const bool EnableRequestProfileDefault = true;
+
+ /// <summary>
+ /// The default value for the <see cref="RequireSsl"/> property.
+ /// </summary>
+ private const bool RequireSslDefault = false;
+
+ /// <summary>
+ /// The default value for the <see cref="ImmediateMode"/> property.
+ /// </summary>
+ private const bool ImmediateModeDefault = false;
+
+ /// <summary>
+ /// The default value for the <see cref="Stateless"/> property.
+ /// </summary>
+ private const bool StatelessDefault = false;
+
+ /// <summary>
+ /// The default value for the <see cref="PolicyUrl"/> property.
+ /// </summary>
+ private const string PolicyUrlDefault = "";
+
+ /// <summary>
+ /// The default value for the <see cref="ReturnToUrl"/> property.
+ /// </summary>
+ private const string ReturnToUrlDefault = "";
+
+ /// <summary>
+ /// The default value for the <see cref="RealmUrl"/> property.
+ /// </summary>
+ private const string RealmUrlDefault = "~/";
+
+ /// <summary>
+ /// The default value for the <see cref="RequestEmail"/> property.
+ /// </summary>
+ private const DemandLevel RequestEmailDefault = DemandLevel.NoRequest;
+
+ /// <summary>
+ /// The default value for the <see cref="RequestPostalCode"/> property.
+ /// </summary>
+ private const DemandLevel RequestPostalCodeDefault = DemandLevel.NoRequest;
+
+ /// <summary>
+ /// The default value for the <see cref="RequestCountry"/> property.
+ /// </summary>
+ private const DemandLevel RequestCountryDefault = DemandLevel.NoRequest;
+
+ /// <summary>
+ /// The default value for the <see cref="RequestLanguage"/> property.
+ /// </summary>
+ private const DemandLevel RequestLanguageDefault = DemandLevel.NoRequest;
+
+ /// <summary>
+ /// The default value for the <see cref="RequestTimeZone"/> property.
+ /// </summary>
+ private const DemandLevel RequestTimeZoneDefault = DemandLevel.NoRequest;
+
+ /// <summary>
+ /// The default value for the <see cref="RequestNickname"/> property.
+ /// </summary>
+ private const DemandLevel RequestNicknameDefault = DemandLevel.NoRequest;
+
+ /// <summary>
+ /// The default value for the <see cref="RequestFullName"/> property.
+ /// </summary>
+ private const DemandLevel RequestFullNameDefault = DemandLevel.NoRequest;
+
+ /// <summary>
+ /// The default value for the <see cref="RequestBirthDate"/> property.
+ /// </summary>
+ private const DemandLevel RequestBirthDateDefault = DemandLevel.NoRequest;
+
+ /// <summary>
+ /// The default value for the <see cref="RequestGender"/> property.
+ /// </summary>
+ private const DemandLevel RequestGenderDefault = DemandLevel.NoRequest;
+
+ #endregion
+
+ /// <summary>
+ /// The callback parameter for use with persisting the <see cref="UsePersistentCookie"/> property.
+ /// </summary>
+ private const string UsePersistentCookieCallbackKey = "OpenIdTextBox_UsePersistentCookie";
+
+ /// <summary>
+ /// Backing field for the <see cref="RelyingParty"/> property.
+ /// </summary>
+ private OpenIdRelyingParty relyingParty;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="OpenIdMobileTextBox"/> class.
+ /// </summary>
+ public OpenIdMobileTextBox() {
+ Reporting.RecordFeatureUse(this);
+ }
+
+ #region Events
+
+ /// <summary>
+ /// Fired upon completion of a successful login.
+ /// </summary>
+ [Description("Fired upon completion of a successful login.")]
+ public event EventHandler<OpenIdEventArgs> LoggedIn;
+
+ /// <summary>
+ /// Fired when a login attempt fails.
+ /// </summary>
+ [Description("Fired when a login attempt fails.")]
+ public event EventHandler<OpenIdEventArgs> Failed;
+
+ /// <summary>
+ /// Fired when an authentication attempt is canceled at the OpenID Provider.
+ /// </summary>
+ [Description("Fired when an authentication attempt is canceled at the OpenID Provider.")]
+ public event EventHandler<OpenIdEventArgs> Canceled;
+
+ /// <summary>
+ /// Fired when an Immediate authentication attempt fails, and the Provider suggests using non-Immediate mode.
+ /// </summary>
+ [Description("Fired when an Immediate authentication attempt fails, and the Provider suggests using non-Immediate mode.")]
+ public event EventHandler<OpenIdEventArgs> SetupRequired;
+
+ #endregion
+
+ #region Properties
+
+ /// <summary>
+ /// Gets or sets the OpenID <see cref="Realm"/> of the relying party web site.
+ /// </summary>
+ [SuppressMessage("Microsoft.Usage", "CA1806:DoNotIgnoreMethodResults", MessageId = "DotNetOpenAuth.OpenId.Realm", Justification = "Using Realm.ctor for validation.")]
+ [SuppressMessage("Microsoft.Usage", "CA1806:DoNotIgnoreMethodResults", MessageId = "System.Uri", Justification = "Using Uri.ctor for validation.")]
+ [SuppressMessage("Microsoft.Usage", "CA1806:DoNotIgnoreMethodResults", MessageId = "DotNetOpenAuth.OpenId", 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.")]
+ 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 = "Uri(Uri, string) accepts second arguments that Uri(Uri, new Uri(string)) does not that we must support.")]
+ [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.")]
+ public string ReturnToUrl {
+ get {
+ return (string)(ViewState[ReturnToUrlViewStateKey] ?? ReturnToUrlDefault);
+ }
+
+ set {
+ if (Page != null && !DesignMode) {
+ // Validate new value by trying to construct a Uri based on it.
+ new Uri(this.RelyingParty.Channel.GetRequestFromContext().UrlBeforeRewriting, 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();
+ }
+ }
+
+ ViewState[ReturnToUrlViewStateKey] = value;
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether to use immediate mode in the
+ /// OpenID protocol.
+ /// </summary>
+ /// <value>
+ /// True if a Provider should reply immediately to the authentication request
+ /// without interacting with the user. False if the Provider can take time
+ /// to authenticate the user in order to complete an authentication attempt.
+ /// </value>
+ /// <remarks>
+ /// Setting this to true is sometimes useful in AJAX scenarios. Setting this to
+ /// true can cause failed authentications when the user truly controls an
+ /// Identifier, but must complete an authentication step with the Provider before
+ /// the Provider will approve the login from this relying party.
+ /// </remarks>
+ [Bindable(true), DefaultValue(ImmediateModeDefault), Category(BehaviorCategory)]
+ [Description("Whether the Provider should respond immediately to an authentication attempt without interacting with the user.")]
+ public bool ImmediateMode {
+ get { return (bool)(ViewState[ImmediateModeViewStateKey] ?? ImmediateModeDefault); }
+ set { ViewState[ImmediateModeViewStateKey] = 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 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 your level of interest in receiving the user's nickname from the Provider.
+ /// </summary>
+ [Bindable(true), DefaultValue(RequestNicknameDefault), Category(ProfileCategory)]
+ [Description("Your level of interest in receiving the user's nickname from the Provider.")]
+ public DemandLevel RequestNickname {
+ get { return (DemandLevel)(ViewState[RequestNicknameViewStateKey] ?? RequestNicknameDefault); }
+ set { ViewState[RequestNicknameViewStateKey] = value; }
+ }
+
+ /// <summary>
+ /// Gets or sets your level of interest in receiving the user's email address from the Provider.
+ /// </summary>
+ [Bindable(true), DefaultValue(RequestEmailDefault), Category(ProfileCategory)]
+ [Description("Your level of interest in receiving the user's email address from the Provider.")]
+ public DemandLevel RequestEmail {
+ get { return (DemandLevel)(ViewState[RequestEmailViewStateKey] ?? RequestEmailDefault); }
+ set { ViewState[RequestEmailViewStateKey] = value; }
+ }
+
+ /// <summary>
+ /// Gets or sets your level of interest in receiving the user's full name from the Provider.
+ /// </summary>
+ [Bindable(true), DefaultValue(RequestFullNameDefault), Category(ProfileCategory)]
+ [Description("Your level of interest in receiving the user's full name from the Provider")]
+ public DemandLevel RequestFullName {
+ get { return (DemandLevel)(ViewState[RequestFullNameViewStateKey] ?? RequestFullNameDefault); }
+ set { ViewState[RequestFullNameViewStateKey] = value; }
+ }
+
+ /// <summary>
+ /// Gets or sets your level of interest in receiving the user's birthdate from the Provider.
+ /// </summary>
+ [Bindable(true), DefaultValue(RequestBirthDateDefault), Category(ProfileCategory)]
+ [Description("Your level of interest in receiving the user's birthdate from the Provider.")]
+ public DemandLevel RequestBirthDate {
+ get { return (DemandLevel)(ViewState[RequestBirthDateViewStateKey] ?? RequestBirthDateDefault); }
+ set { ViewState[RequestBirthDateViewStateKey] = value; }
+ }
+
+ /// <summary>
+ /// Gets or sets your level of interest in receiving the user's gender from the Provider.
+ /// </summary>
+ [Bindable(true), DefaultValue(RequestGenderDefault), Category(ProfileCategory)]
+ [Description("Your level of interest in receiving the user's gender from the Provider.")]
+ public DemandLevel RequestGender {
+ get { return (DemandLevel)(ViewState[RequestGenderViewStateKey] ?? RequestGenderDefault); }
+ set { ViewState[RequestGenderViewStateKey] = value; }
+ }
+
+ /// <summary>
+ /// Gets or sets your level of interest in receiving the user's postal code from the Provider.
+ /// </summary>
+ [Bindable(true), DefaultValue(RequestPostalCodeDefault), Category(ProfileCategory)]
+ [Description("Your level of interest in receiving the user's postal code from the Provider.")]
+ public DemandLevel RequestPostalCode {
+ get { return (DemandLevel)(ViewState[RequestPostalCodeViewStateKey] ?? RequestPostalCodeDefault); }
+ set { ViewState[RequestPostalCodeViewStateKey] = value; }
+ }
+
+ /// <summary>
+ /// Gets or sets your level of interest in receiving the user's country from the Provider.
+ /// </summary>
+ [Bindable(true)]
+ [Category(ProfileCategory)]
+ [DefaultValue(RequestCountryDefault)]
+ [Description("Your level of interest in receiving the user's country from the Provider.")]
+ public DemandLevel RequestCountry {
+ get { return (DemandLevel)(ViewState[RequestCountryViewStateKey] ?? RequestCountryDefault); }
+ set { ViewState[RequestCountryViewStateKey] = value; }
+ }
+
+ /// <summary>
+ /// Gets or sets your level of interest in receiving the user's preferred language from the Provider.
+ /// </summary>
+ [Bindable(true), DefaultValue(RequestLanguageDefault), Category(ProfileCategory)]
+ [Description("Your level of interest in receiving the user's preferred language from the Provider.")]
+ public DemandLevel RequestLanguage {
+ get { return (DemandLevel)(ViewState[RequestLanguageViewStateKey] ?? RequestLanguageDefault); }
+ set { ViewState[RequestLanguageViewStateKey] = value; }
+ }
+
+ /// <summary>
+ /// Gets or sets your level of interest in receiving the user's time zone from the Provider.
+ /// </summary>
+ [Bindable(true), DefaultValue(RequestTimeZoneDefault), Category(ProfileCategory)]
+ [Description("Your level of interest in receiving the user's time zone from the Provider.")]
+ public DemandLevel RequestTimeZone {
+ get { return (DemandLevel)(ViewState[RequestTimeZoneViewStateKey] ?? RequestTimeZoneDefault); }
+ set { ViewState[RequestTimeZoneViewStateKey] = value; }
+ }
+
+ /// <summary>
+ /// Gets or sets the URL to your privacy policy page that describes how
+ /// claims will be used and/or shared.
+ /// </summary>
+ [SuppressMessage("Microsoft.Design", "CA1056:UriPropertiesShouldNotBeStrings", Justification = "Bindable property must be simple type")]
+ [Bindable(true), DefaultValue(PolicyUrlDefault), Category(ProfileCategory)]
+ [Description("The URL to your privacy policy page that describes how claims will be used and/or shared.")]
+ public string PolicyUrl {
+ get {
+ return (string)ViewState[PolicyUrlViewStateKey] ?? PolicyUrlDefault;
+ }
+
+ set {
+ UriUtil.ValidateResolvableUrl(Page, DesignMode, value);
+ ViewState[PolicyUrlViewStateKey] = value;
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether to use OpenID extensions
+ /// to retrieve profile data of the authenticating user.
+ /// </summary>
+ [Bindable(true), DefaultValue(EnableRequestProfileDefault), Category(ProfileCategory)]
+ [Description("Turns the entire Simple Registration extension on or off.")]
+ public bool EnableRequestProfile {
+ get { return (bool)(ViewState[EnableRequestProfileViewStateKey] ?? EnableRequestProfileDefault); }
+ set { ViewState[EnableRequestProfileViewStateKey] = value; }
+ }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether to enforce on high security mode,
+ /// which requires the full authentication pipeline to be protected by SSL.
+ /// </summary>
+ [Bindable(true), DefaultValue(RequireSslDefault), Category(BehaviorCategory)]
+ [Description("Turns on high security mode, requiring the full authentication pipeline to be protected by SSL.")]
+ public bool RequireSsl {
+ get { return (bool)(ViewState[RequireSslViewStateKey] ?? RequireSslDefault); }
+ set { ViewState[RequireSslViewStateKey] = value; }
+ }
+
+ /// <summary>
+ /// Gets or sets the type of the custom application store to use, or <c>null</c> to use the default.
+ /// </summary>
+ /// <remarks>
+ /// If set, this property must be set in each Page Load event
+ /// as it is not persisted across postbacks.
+ /// </remarks>
+ public IOpenIdApplicationStore CustomApplicationStore { get; set; }
+
+ #endregion
+
+ /// <summary>
+ /// Gets or sets the <see cref="OpenIdRelyingParty"/> instance to use.
+ /// </summary>
+ /// <value>The default value is an <see cref="OpenIdRelyingParty"/> instance initialized according to the web.config file.</value>
+ /// <remarks>
+ /// A performance optimization would be to store off the
+ /// instance as a static member in your web site and set it
+ /// to this property in your <see cref="Control.Load">Page.Load</see>
+ /// event since instantiating these instances can be expensive on
+ /// heavily trafficked web pages.
+ /// </remarks>
+ public OpenIdRelyingParty RelyingParty {
+ get {
+ if (this.relyingParty == null) {
+ this.relyingParty = this.CreateRelyingParty();
+ }
+ return this.relyingParty;
+ }
+
+ set {
+ this.relyingParty = value;
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets the OpenID authentication request that is about to be sent.
+ /// </summary>
+ protected IAuthenticationRequest Request { get; set; }
+
+ /// <summary>
+ /// Immediately redirects to the OpenID Provider to verify the Identifier
+ /// provided in the text box.
+ /// </summary>
+ public void LogOn() {
+ if (this.Request == null) {
+ this.CreateRequest(); // sets this.Request
+ }
+
+ if (this.Request != null) {
+ this.Request.RedirectToProvider();
+ }
+ }
+
+ /// <summary>
+ /// Constructs the authentication request and returns it.
+ /// </summary>
+ /// <returns>The instantiated authentication request.</returns>
+ /// <remarks>
+ /// <para>This method need not be called before calling the <see cref="LogOn"/> method,
+ /// but is offered in the event that adding extensions to the request is desired.</para>
+ /// <para>The Simple Registration extension arguments are added to the request
+ /// before returning if <see cref="EnableRequestProfile"/> is set to true.</para>
+ /// </remarks>
+ [SuppressMessage("Microsoft.Usage", "CA2234:PassSystemUriObjectsInsteadOfStrings", Justification = "Uri(Uri, string) accepts second arguments that Uri(Uri, new Uri(string)) does not that we must support.")]
+ public IAuthenticationRequest CreateRequest() {
+ Contract.Requires<InvalidOperationException>(this.Request == null, OpenIdStrings.CreateRequestAlreadyCalled);
+ Contract.Requires<InvalidOperationException>(!string.IsNullOrEmpty(this.Text), OpenIdStrings.OpenIdTextBoxEmpty);
+
+ try {
+ // 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 = Page.Request.Url.Scheme;
+ realm.Port = Page.Request.Url.Port;
+
+ // Initiate openid request
+ // We use TryParse here to avoid throwing an exception which
+ // might slip through our validator control if it is disabled.
+ Identifier userSuppliedIdentifier;
+ if (Identifier.TryParse(this.Text, out userSuppliedIdentifier)) {
+ Realm typedRealm = new Realm(realm);
+ if (string.IsNullOrEmpty(this.ReturnToUrl)) {
+ this.Request = this.RelyingParty.CreateRequest(userSuppliedIdentifier, typedRealm);
+ } else {
+ Uri returnTo = new Uri(this.RelyingParty.Channel.GetRequestFromContext().UrlBeforeRewriting, this.ReturnToUrl);
+ this.Request = this.RelyingParty.CreateRequest(userSuppliedIdentifier, typedRealm, returnTo);
+ }
+ this.Request.Mode = this.ImmediateMode ? AuthenticationRequestMode.Immediate : AuthenticationRequestMode.Setup;
+ if (this.EnableRequestProfile) {
+ this.AddProfileArgs(this.Request);
+ }
+
+ // Add state that needs to survive across the redirect.
+ this.Request.SetUntrustedCallbackArgument(UsePersistentCookieCallbackKey, this.UsePersistentCookie.ToString(CultureInfo.InvariantCulture));
+ } else {
+ Logger.OpenId.WarnFormat("An invalid identifier was entered ({0}), but not caught by any validation routine.", this.Text);
+ this.Request = null;
+ }
+ } catch (ProtocolException ex) {
+ this.OnFailed(new FailedAuthenticationResponse(ex));
+ }
+
+ return this.Request;
+ }
+
+ /// <summary>
+ /// Checks for incoming OpenID authentication responses and fires appropriate events.
+ /// </summary>
+ /// <param name="e">The <see cref="T:System.EventArgs"/> object that contains the event data.</param>
+ protected override void OnLoad(EventArgs e) {
+ base.OnLoad(e);
+
+ if (Page.IsPostBack) {
+ return;
+ }
+
+ var response = this.RelyingParty.GetResponse();
+ if (response != null) {
+ string persistentString = response.GetUntrustedCallbackArgument(UsePersistentCookieCallbackKey);
+ bool persistentBool;
+ if (persistentString != null && bool.TryParse(persistentString, out persistentBool)) {
+ this.UsePersistentCookie = persistentBool;
+ }
+
+ switch (response.Status) {
+ case AuthenticationStatus.Canceled:
+ this.OnCanceled(response);
+ break;
+ case AuthenticationStatus.Authenticated:
+ this.OnLoggedIn(response);
+ break;
+ case AuthenticationStatus.SetupRequired:
+ this.OnSetupRequired(response);
+ break;
+ case AuthenticationStatus.Failed:
+ this.OnFailed(response);
+ break;
+ default:
+ throw new InvalidOperationException("Unexpected response status code.");
+ }
+ }
+ }
+
+ #region Events
+
+ /// <summary>
+ /// Fires the <see cref="LoggedIn"/> event.
+ /// </summary>
+ /// <param name="response">The response.</param>
+ protected virtual void OnLoggedIn(IAuthenticationResponse response) {
+ Contract.Requires<ArgumentNullException>(response != null);
+ 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="Failed"/> event.
+ /// </summary>
+ /// <param name="response">The response.</param>
+ protected virtual void OnFailed(IAuthenticationResponse response) {
+ Contract.Requires<ArgumentNullException>(response != null);
+ 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>
+ /// Fires the <see cref="Canceled"/> event.
+ /// </summary>
+ /// <param name="response">The response.</param>
+ protected virtual void OnCanceled(IAuthenticationResponse response) {
+ Contract.Requires<ArgumentNullException>(response != null);
+ 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="SetupRequired"/> event.
+ /// </summary>
+ /// <param name="response">The response.</param>
+ protected virtual void OnSetupRequired(IAuthenticationResponse response) {
+ Contract.Requires<ArgumentNullException>(response != null);
+ ErrorUtilities.VerifyInternal(response.Status == AuthenticationStatus.SetupRequired, "Firing SetupRequired event for the wrong response type.");
+
+ // Why are we firing Failed when we're OnSetupRequired? Backward compatibility.
+ var setupRequired = this.SetupRequired;
+ if (setupRequired != null) {
+ setupRequired(this, new OpenIdEventArgs(response));
+ }
+ }
+
+ #endregion
+
+ /// <summary>
+ /// Adds extensions to a given authentication request to ask the Provider
+ /// for user profile data.
+ /// </summary>
+ /// <param name="request">The authentication request to add the extensions to.</param>
+ private void AddProfileArgs(IAuthenticationRequest request) {
+ Contract.Requires<ArgumentNullException>(request != null);
+
+ request.AddExtension(new ClaimsRequest() {
+ Nickname = this.RequestNickname,
+ Email = this.RequestEmail,
+ FullName = this.RequestFullName,
+ BirthDate = this.RequestBirthDate,
+ Gender = this.RequestGender,
+ PostalCode = this.RequestPostalCode,
+ Country = this.RequestCountry,
+ Language = this.RequestLanguage,
+ TimeZone = this.RequestTimeZone,
+ PolicyUrl = string.IsNullOrEmpty(this.PolicyUrl) ?
+ null : new Uri(this.RelyingParty.Channel.GetRequestFromContext().UrlBeforeRewriting, this.Page.ResolveUrl(this.PolicyUrl)),
+ });
+ }
+
+ /// <summary>
+ /// Creates the relying party instance used to generate authentication requests.
+ /// </summary>
+ /// <returns>The instantiated relying party.</returns>
+ private OpenIdRelyingParty CreateRelyingParty() {
+ // If we're in stateful mode, first use the explicitly given one on this control if there
+ // is one. Then try the configuration file specified one. Finally, use the default
+ // in-memory one that's built into OpenIdRelyingParty.
+ IOpenIdApplicationStore store = this.Stateless ? null :
+ (this.CustomApplicationStore ?? OpenIdElement.Configuration.RelyingParty.ApplicationStore.CreateInstance(OpenIdRelyingParty.HttpApplicationStore));
+ var rp = new OpenIdRelyingParty(store);
+ try {
+ // 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;
+ } catch {
+ rp.Dispose();
+ throw;
+ }
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/OpenIdRelyingParty.cd b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/OpenIdRelyingParty.cd
new file mode 100644
index 0000000..0519ecb
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/OpenIdRelyingParty.cd
@@ -0,0 +1 @@
+ \ No newline at end of file
diff --git a/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/OpenIdRelyingParty.cs b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/OpenIdRelyingParty.cs
new file mode 100644
index 0000000..290d29e
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/OpenIdRelyingParty.cs
@@ -0,0 +1,891 @@
+//-----------------------------------------------------------------------
+// <copyright file="OpenIdRelyingParty.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OpenId.RelyingParty {
+ using System;
+ using System.Collections.Generic;
+ using System.Collections.ObjectModel;
+ using System.Collections.Specialized;
+ using System.ComponentModel;
+ using System.Diagnostics.CodeAnalysis;
+ using System.Diagnostics.Contracts;
+ using System.Globalization;
+ using System.Linq;
+ using System.Net;
+ using System.Net.Mime;
+ using System.Text;
+ using System.Web;
+ using DotNetOpenAuth.Configuration;
+ using DotNetOpenAuth.Messaging;
+ using DotNetOpenAuth.Messaging.Bindings;
+ using DotNetOpenAuth.OpenId.ChannelElements;
+ using DotNetOpenAuth.OpenId.Extensions;
+ using DotNetOpenAuth.OpenId.Messages;
+
+ /// <summary>
+ /// A delegate that decides whether a given OpenID Provider endpoint may be
+ /// considered for authenticating a user.
+ /// </summary>
+ /// <param name="endpoint">The endpoint for consideration.</param>
+ /// <returns>
+ /// <c>True</c> if the endpoint should be considered.
+ /// <c>False</c> to remove it from the pool of acceptable providers.
+ /// </returns>
+ public delegate bool EndpointSelector(IProviderEndpoint endpoint);
+
+ /// <summary>
+ /// Provides the programmatic facilities to act as an OpenID relying party.
+ /// </summary>
+ [SuppressMessage("Microsoft.Maintainability", "CA1506:AvoidExcessiveClassCoupling", Justification = "Unavoidable")]
+ [ContractVerification(true)]
+ public class OpenIdRelyingParty : IDisposable {
+ /// <summary>
+ /// The name of the key to use in the HttpApplication cache to store the
+ /// instance of <see cref="StandardRelyingPartyApplicationStore"/> to use.
+ /// </summary>
+ private const string ApplicationStoreKey = "DotNetOpenAuth.OpenId.RelyingParty.OpenIdRelyingParty.HttpApplicationStore";
+
+ /// <summary>
+ /// Backing store for the <see cref="Behaviors"/> property.
+ /// </summary>
+ private readonly ObservableCollection<IRelyingPartyBehavior> behaviors = new ObservableCollection<IRelyingPartyBehavior>();
+
+ /// <summary>
+ /// Backing field for the <see cref="DiscoveryServices"/> property.
+ /// </summary>
+ private readonly IList<IIdentifierDiscoveryService> discoveryServices = new List<IIdentifierDiscoveryService>(2);
+
+ /// <summary>
+ /// Backing field for the <see cref="NonVerifyingRelyingParty"/> property.
+ /// </summary>
+ private OpenIdRelyingParty nonVerifyingRelyingParty;
+
+ /// <summary>
+ /// The lock to obtain when initializing the <see cref="nonVerifyingRelyingParty"/> member.
+ /// </summary>
+ private object nonVerifyingRelyingPartyInitLock = new object();
+
+ /// <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>
+ /// Backing field for the <see cref="SecuritySettings"/> property.
+ /// </summary>
+ private RelyingPartySecuritySettings securitySettings;
+
+ /// <summary>
+ /// Backing store for the <see cref="EndpointOrder"/> property.
+ /// </summary>
+ private Comparison<IdentifierDiscoveryResult> endpointOrder = DefaultEndpointOrder;
+
+ /// <summary>
+ /// Backing field for the <see cref="Channel"/> property.
+ /// </summary>
+ private Channel channel;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="OpenIdRelyingParty"/> class.
+ /// </summary>
+ public OpenIdRelyingParty()
+ : this(OpenIdElement.Configuration.RelyingParty.ApplicationStore.CreateInstance(HttpApplicationStore)) {
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="OpenIdRelyingParty"/> class.
+ /// </summary>
+ /// <param name="applicationStore">The application store. If <c>null</c>, the relying party will always operate in "dumb mode".</param>
+ public OpenIdRelyingParty(IOpenIdApplicationStore applicationStore)
+ : this(applicationStore, applicationStore) {
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="OpenIdRelyingParty"/> class.
+ /// </summary>
+ /// <param name="cryptoKeyStore">The association store. If null, the relying party will always operate in "dumb mode".</param>
+ /// <param name="nonceStore">The nonce store to use. If null, the relying party will always operate in "dumb mode".</param>
+ [SuppressMessage("Microsoft.Maintainability", "CA1506:AvoidExcessiveClassCoupling", Justification = "Unavoidable")]
+ private OpenIdRelyingParty(ICryptoKeyStore cryptoKeyStore, INonceStore nonceStore) {
+ // If we are a smart-mode RP (supporting associations), then we MUST also be
+ // capable of storing nonces to prevent replay attacks.
+ // If we're a dumb-mode RP, then 2.0 OPs are responsible for preventing replays.
+ Contract.Requires<ArgumentException>(cryptoKeyStore == null || nonceStore != null, OpenIdStrings.AssociationStoreRequiresNonceStore);
+
+ this.securitySettings = OpenIdElement.Configuration.RelyingParty.SecuritySettings.CreateSecuritySettings();
+
+ foreach (var discoveryService in OpenIdElement.Configuration.RelyingParty.DiscoveryServices.CreateInstances(true)) {
+ this.discoveryServices.Add(discoveryService);
+ }
+
+ this.behaviors.CollectionChanged += this.OnBehaviorsChanged;
+ foreach (var behavior in OpenIdElement.Configuration.RelyingParty.Behaviors.CreateInstances(false)) {
+ this.behaviors.Add(behavior);
+ }
+
+ // Without a nonce store, we must rely on the Provider to protect against
+ // replay attacks. But only 2.0+ Providers can be expected to provide
+ // replay protection.
+ if (nonceStore == null &&
+ this.SecuritySettings.ProtectDownlevelReplayAttacks &&
+ this.SecuritySettings.MinimumRequiredOpenIdVersion < ProtocolVersion.V20) {
+ Logger.OpenId.Warn("Raising minimum OpenID version requirement for Providers to 2.0 to protect this stateless RP from replay attacks.");
+ this.SecuritySettings.MinimumRequiredOpenIdVersion = ProtocolVersion.V20;
+ }
+
+ if (cryptoKeyStore == null) {
+ cryptoKeyStore = new MemoryCryptoKeyStore();
+ }
+
+ this.channel = new OpenIdChannel(cryptoKeyStore, nonceStore, this.SecuritySettings);
+ this.AssociationManager = new AssociationManager(this.Channel, new CryptoKeyStoreAsRelyingPartyAssociationStore(cryptoKeyStore), this.SecuritySettings);
+
+ Reporting.RecordFeatureAndDependencyUse(this, cryptoKeyStore, nonceStore);
+ }
+
+ /// <summary>
+ /// Gets an XRDS sorting routine that uses the XRDS Service/@Priority
+ /// attribute to determine order.
+ /// </summary>
+ /// <remarks>
+ /// Endpoints lacking any priority value are sorted to the end of the list.
+ /// </remarks>
+ [EditorBrowsable(EditorBrowsableState.Advanced)]
+ public static Comparison<IdentifierDiscoveryResult> DefaultEndpointOrder {
+ get { return IdentifierDiscoveryResult.EndpointOrder; }
+ }
+
+ /// <summary>
+ /// Gets the standard state storage mechanism that uses ASP.NET's
+ /// HttpApplication state dictionary to store associations and nonces.
+ /// </summary>
+ [EditorBrowsable(EditorBrowsableState.Advanced)]
+ public static IOpenIdApplicationStore HttpApplicationStore {
+ get {
+ Contract.Ensures(Contract.Result<IOpenIdApplicationStore>() != null);
+
+ HttpContext context = HttpContext.Current;
+ ErrorUtilities.VerifyOperation(context != null, Strings.StoreRequiredWhenNoHttpContextAvailable, typeof(IOpenIdApplicationStore).Name);
+ var store = (IOpenIdApplicationStore)context.Application[ApplicationStoreKey];
+ if (store == null) {
+ context.Application.Lock();
+ try {
+ if ((store = (IOpenIdApplicationStore)context.Application[ApplicationStoreKey]) == null) {
+ context.Application[ApplicationStoreKey] = store = new StandardRelyingPartyApplicationStore();
+ }
+ } finally {
+ context.Application.UnLock();
+ }
+ }
+
+ return store;
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets the channel to use for sending/receiving messages.
+ /// </summary>
+ public Channel Channel {
+ get {
+ return this.channel;
+ }
+
+ set {
+ Contract.Requires<ArgumentNullException>(value != null);
+ this.channel = value;
+ this.AssociationManager.Channel = value;
+ }
+ }
+
+ /// <summary>
+ /// Gets the security settings used by this Relying Party.
+ /// </summary>
+ public RelyingPartySecuritySettings SecuritySettings {
+ get {
+ Contract.Ensures(Contract.Result<RelyingPartySecuritySettings>() != null);
+ return this.securitySettings;
+ }
+
+ internal set {
+ Contract.Requires<ArgumentNullException>(value != null);
+ this.securitySettings = value;
+ this.AssociationManager.SecuritySettings = value;
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets the optional Provider Endpoint filter to use.
+ /// </summary>
+ /// <remarks>
+ /// Provides a way to optionally filter the providers that may be used in authenticating a user.
+ /// If provided, the delegate should return true to accept an endpoint, and false to reject it.
+ /// If null, all identity providers will be accepted. This is the default.
+ /// </remarks>
+ [EditorBrowsable(EditorBrowsableState.Advanced)]
+ public EndpointSelector EndpointFilter { get; set; }
+
+ /// <summary>
+ /// Gets or sets the ordering routine that will determine which XRDS
+ /// Service element to try first
+ /// </summary>
+ /// <value>Default is <see cref="DefaultEndpointOrder"/>.</value>
+ /// <remarks>
+ /// This may never be null. To reset to default behavior this property
+ /// can be set to the value of <see cref="DefaultEndpointOrder"/>.
+ /// </remarks>
+ [EditorBrowsable(EditorBrowsableState.Advanced)]
+ public Comparison<IdentifierDiscoveryResult> EndpointOrder {
+ get {
+ return this.endpointOrder;
+ }
+
+ set {
+ Contract.Requires<ArgumentNullException>(value != null);
+ this.endpointOrder = value;
+ }
+ }
+
+ /// <summary>
+ /// Gets the extension factories.
+ /// </summary>
+ public IList<IOpenIdExtensionFactory> ExtensionFactories {
+ get { return this.Channel.GetExtensionFactories(); }
+ }
+
+ /// <summary>
+ /// Gets a list of custom behaviors to apply to OpenID actions.
+ /// </summary>
+ /// <remarks>
+ /// Adding behaviors can impact the security settings of this <see cref="OpenIdRelyingParty"/>
+ /// instance in ways that subsequently removing the behaviors will not reverse.
+ /// </remarks>
+ public ICollection<IRelyingPartyBehavior> Behaviors {
+ get { return this.behaviors; }
+ }
+
+ /// <summary>
+ /// Gets the list of services that can perform discovery on identifiers given to this relying party.
+ /// </summary>
+ public IList<IIdentifierDiscoveryService> DiscoveryServices {
+ get { return this.discoveryServices; }
+ }
+
+ /// <summary>
+ /// Gets a value indicating whether this Relying Party can sign its return_to
+ /// parameter in outgoing authentication requests.
+ /// </summary>
+ internal bool CanSignCallbackArguments {
+ get { return this.Channel.BindingElements.OfType<ReturnToSignatureBindingElement>().Any(); }
+ }
+
+ /// <summary>
+ /// Gets the web request handler to use for discovery and the part of
+ /// authentication where direct messages are sent to an untrusted remote party.
+ /// </summary>
+ internal IDirectWebRequestHandler WebRequestHandler {
+ get { return this.Channel.WebRequestHandler; }
+ }
+
+ /// <summary>
+ /// Gets the association manager.
+ /// </summary>
+ internal AssociationManager AssociationManager { get; private set; }
+
+ /// <summary>
+ /// Gets the <see cref="OpenIdRelyingParty"/> instance used to process authentication responses
+ /// without verifying the assertion or consuming nonces.
+ /// </summary>
+ protected OpenIdRelyingParty NonVerifyingRelyingParty {
+ get {
+ if (this.nonVerifyingRelyingParty == null) {
+ lock (this.nonVerifyingRelyingPartyInitLock) {
+ if (this.nonVerifyingRelyingParty == null) {
+ this.nonVerifyingRelyingParty = OpenIdRelyingParty.CreateNonVerifying();
+ }
+ }
+ }
+
+ return this.nonVerifyingRelyingParty;
+ }
+ }
+
+ /// <summary>
+ /// Creates an authentication request to verify that a user controls
+ /// some given Identifier.
+ /// </summary>
+ /// <param name="userSuppliedIdentifier">
+ /// The Identifier supplied by the user. This may be a URL, an XRI or i-name.
+ /// </param>
+ /// <param name="realm">
+ /// The shorest URL that describes this relying party web site's address.
+ /// For example, if your login page is found at https://www.example.com/login.aspx,
+ /// your realm would typically be https://www.example.com/.
+ /// </param>
+ /// <param name="returnToUrl">
+ /// The URL of the login page, or the page prepared to receive authentication
+ /// responses from the OpenID Provider.
+ /// </param>
+ /// <returns>
+ /// An authentication request object to customize the request and generate
+ /// an object to send to the user agent to initiate the authentication.
+ /// </returns>
+ /// <exception cref="ProtocolException">Thrown if no OpenID endpoint could be found.</exception>
+ public IAuthenticationRequest CreateRequest(Identifier userSuppliedIdentifier, Realm realm, Uri returnToUrl) {
+ Contract.Requires<ArgumentNullException>(userSuppliedIdentifier != null);
+ Contract.Requires<ArgumentNullException>(realm != null);
+ Contract.Requires<ArgumentNullException>(returnToUrl != null);
+ Contract.Ensures(Contract.Result<IAuthenticationRequest>() != null);
+ try {
+ return this.CreateRequests(userSuppliedIdentifier, realm, returnToUrl).First();
+ } catch (InvalidOperationException ex) {
+ throw ErrorUtilities.Wrap(ex, OpenIdStrings.OpenIdEndpointNotFound);
+ }
+ }
+
+ /// <summary>
+ /// Creates an authentication request to verify that a user controls
+ /// some given Identifier.
+ /// </summary>
+ /// <param name="userSuppliedIdentifier">
+ /// The Identifier supplied by the user. This may be a URL, an XRI or i-name.
+ /// </param>
+ /// <param name="realm">
+ /// The shorest URL that describes this relying party web site's address.
+ /// For example, if your login page is found at https://www.example.com/login.aspx,
+ /// your realm would typically be https://www.example.com/.
+ /// </param>
+ /// <returns>
+ /// An authentication request object that describes the HTTP response to
+ /// send to the user agent to initiate the authentication.
+ /// </returns>
+ /// <remarks>
+ /// <para>Requires an <see cref="HttpContext.Current">HttpContext.Current</see> context.</para>
+ /// </remarks>
+ /// <exception cref="ProtocolException">Thrown if no OpenID endpoint could be found.</exception>
+ /// <exception cref="InvalidOperationException">Thrown if <see cref="HttpContext.Current">HttpContext.Current</see> == <c>null</c>.</exception>
+ public IAuthenticationRequest CreateRequest(Identifier userSuppliedIdentifier, Realm realm) {
+ Contract.Requires<ArgumentNullException>(userSuppliedIdentifier != null);
+ Contract.Requires<ArgumentNullException>(realm != null);
+ Contract.Ensures(Contract.Result<IAuthenticationRequest>() != null);
+ try {
+ var result = this.CreateRequests(userSuppliedIdentifier, realm).First();
+ Contract.Assume(result != null);
+ return result;
+ } catch (InvalidOperationException ex) {
+ throw ErrorUtilities.Wrap(ex, OpenIdStrings.OpenIdEndpointNotFound);
+ }
+ }
+
+ /// <summary>
+ /// Creates an authentication request to verify that a user controls
+ /// some given Identifier.
+ /// </summary>
+ /// <param name="userSuppliedIdentifier">
+ /// The Identifier supplied by the user. This may be a URL, an XRI or i-name.
+ /// </param>
+ /// <returns>
+ /// An authentication request object that describes the HTTP response to
+ /// send to the user agent to initiate the authentication.
+ /// </returns>
+ /// <remarks>
+ /// <para>Requires an <see cref="HttpContext.Current">HttpContext.Current</see> context.</para>
+ /// </remarks>
+ /// <exception cref="ProtocolException">Thrown if no OpenID endpoint could be found.</exception>
+ /// <exception cref="InvalidOperationException">Thrown if <see cref="HttpContext.Current">HttpContext.Current</see> == <c>null</c>.</exception>
+ public IAuthenticationRequest CreateRequest(Identifier userSuppliedIdentifier) {
+ Contract.Requires<ArgumentNullException>(userSuppliedIdentifier != null);
+ Contract.Ensures(Contract.Result<IAuthenticationRequest>() != null);
+ try {
+ return this.CreateRequests(userSuppliedIdentifier).First();
+ } catch (InvalidOperationException ex) {
+ throw ErrorUtilities.Wrap(ex, OpenIdStrings.OpenIdEndpointNotFound);
+ }
+ }
+
+ /// <summary>
+ /// Generates the authentication requests that can satisfy the requirements of some OpenID Identifier.
+ /// </summary>
+ /// <param name="userSuppliedIdentifier">
+ /// The Identifier supplied by the user. This may be a URL, an XRI or i-name.
+ /// </param>
+ /// <param name="realm">
+ /// The shorest URL that describes this relying party web site's address.
+ /// For example, if your login page is found at https://www.example.com/login.aspx,
+ /// your realm would typically be https://www.example.com/.
+ /// </param>
+ /// <param name="returnToUrl">
+ /// The URL of the login page, or the page prepared to receive authentication
+ /// responses from the OpenID Provider.
+ /// </param>
+ /// <returns>
+ /// A sequence of authentication requests, any of which constitutes a valid identity assertion on the Claimed Identifier.
+ /// Never null, but may be empty.
+ /// </returns>
+ /// <remarks>
+ /// <para>Any individual generated request can satisfy the authentication.
+ /// The generated requests are sorted in preferred order.
+ /// Each request is generated as it is enumerated to. Associations are created only as
+ /// <see cref="IAuthenticationRequest.RedirectingResponse"/> is called.</para>
+ /// <para>No exception is thrown if no OpenID endpoints were discovered.
+ /// An empty enumerable is returned instead.</para>
+ /// </remarks>
+ public virtual IEnumerable<IAuthenticationRequest> CreateRequests(Identifier userSuppliedIdentifier, Realm realm, Uri returnToUrl) {
+ Contract.Requires<ArgumentNullException>(userSuppliedIdentifier != null);
+ Contract.Requires<ArgumentNullException>(realm != null);
+ Contract.Requires<ArgumentNullException>(returnToUrl != null);
+ Contract.Ensures(Contract.Result<IEnumerable<IAuthenticationRequest>>() != null);
+
+ return AuthenticationRequest.Create(userSuppliedIdentifier, this, realm, returnToUrl, true).Cast<IAuthenticationRequest>().CacheGeneratedResults();
+ }
+
+ /// <summary>
+ /// Generates the authentication requests that can satisfy the requirements of some OpenID Identifier.
+ /// </summary>
+ /// <param name="userSuppliedIdentifier">
+ /// The Identifier supplied by the user. This may be a URL, an XRI or i-name.
+ /// </param>
+ /// <param name="realm">
+ /// The shorest URL that describes this relying party web site's address.
+ /// For example, if your login page is found at https://www.example.com/login.aspx,
+ /// your realm would typically be https://www.example.com/.
+ /// </param>
+ /// <returns>
+ /// A sequence of authentication requests, any of which constitutes a valid identity assertion on the Claimed Identifier.
+ /// Never null, but may be empty.
+ /// </returns>
+ /// <remarks>
+ /// <para>Any individual generated request can satisfy the authentication.
+ /// The generated requests are sorted in preferred order.
+ /// Each request is generated as it is enumerated to. Associations are created only as
+ /// <see cref="IAuthenticationRequest.RedirectingResponse"/> is called.</para>
+ /// <para>No exception is thrown if no OpenID endpoints were discovered.
+ /// An empty enumerable is returned instead.</para>
+ /// <para>Requires an <see cref="HttpContext.Current">HttpContext.Current</see> context.</para>
+ /// </remarks>
+ /// <exception cref="InvalidOperationException">Thrown if <see cref="HttpContext.Current">HttpContext.Current</see> == <c>null</c>.</exception>
+ public IEnumerable<IAuthenticationRequest> CreateRequests(Identifier userSuppliedIdentifier, Realm realm) {
+ Contract.Requires<InvalidOperationException>(HttpContext.Current != null && HttpContext.Current.Request != null, MessagingStrings.HttpContextRequired);
+ Contract.Requires<ArgumentNullException>(userSuppliedIdentifier != null);
+ Contract.Requires<ArgumentNullException>(realm != null);
+ Contract.Ensures(Contract.Result<IEnumerable<IAuthenticationRequest>>() != null);
+
+ // This next code contract is a BAD idea, because it causes each authentication request to be generated
+ // at least an extra time.
+ ////Contract.Ensures(Contract.ForAll(Contract.Result<IEnumerable<IAuthenticationRequest>>(), el => el != null));
+
+ // Build the return_to URL
+ UriBuilder returnTo = new UriBuilder(this.Channel.GetRequestFromContext().UrlBeforeRewriting);
+
+ // Trim off any parameters with an "openid." prefix, and a few known others
+ // to avoid carrying state from a prior login attempt.
+ returnTo.Query = string.Empty;
+ NameValueCollection queryParams = this.Channel.GetRequestFromContext().QueryStringBeforeRewriting;
+ var returnToParams = new Dictionary<string, string>(queryParams.Count);
+ foreach (string key in queryParams) {
+ if (!IsOpenIdSupportingParameter(key) && key != null) {
+ returnToParams.Add(key, queryParams[key]);
+ }
+ }
+ returnTo.AppendQueryArgs(returnToParams);
+
+ return this.CreateRequests(userSuppliedIdentifier, realm, returnTo.Uri);
+ }
+
+ /// <summary>
+ /// Generates the authentication requests that can satisfy the requirements of some OpenID Identifier.
+ /// </summary>
+ /// <param name="userSuppliedIdentifier">
+ /// The Identifier supplied by the user. This may be a URL, an XRI or i-name.
+ /// </param>
+ /// <returns>
+ /// A sequence of authentication requests, any of which constitutes a valid identity assertion on the Claimed Identifier.
+ /// Never null, but may be empty.
+ /// </returns>
+ /// <remarks>
+ /// <para>Any individual generated request can satisfy the authentication.
+ /// The generated requests are sorted in preferred order.
+ /// Each request is generated as it is enumerated to. Associations are created only as
+ /// <see cref="IAuthenticationRequest.RedirectingResponse"/> is called.</para>
+ /// <para>No exception is thrown if no OpenID endpoints were discovered.
+ /// An empty enumerable is returned instead.</para>
+ /// <para>Requires an <see cref="HttpContext.Current">HttpContext.Current</see> context.</para>
+ /// </remarks>
+ /// <exception cref="InvalidOperationException">Thrown if <see cref="HttpContext.Current">HttpContext.Current</see> == <c>null</c>.</exception>
+ public IEnumerable<IAuthenticationRequest> CreateRequests(Identifier userSuppliedIdentifier) {
+ Contract.Requires<ArgumentNullException>(userSuppliedIdentifier != null);
+ Contract.Requires<InvalidOperationException>(HttpContext.Current != null && HttpContext.Current.Request != null, MessagingStrings.HttpContextRequired);
+ Contract.Ensures(Contract.Result<IEnumerable<IAuthenticationRequest>>() != null);
+
+ return this.CreateRequests(userSuppliedIdentifier, Realm.AutoDetect);
+ }
+
+ /// <summary>
+ /// Gets an authentication response from a Provider.
+ /// </summary>
+ /// <returns>The processed authentication response if there is any; <c>null</c> otherwise.</returns>
+ /// <remarks>
+ /// <para>Requires an <see cref="HttpContext.Current">HttpContext.Current</see> context.</para>
+ /// </remarks>
+ public IAuthenticationResponse GetResponse() {
+ Contract.Requires<InvalidOperationException>(HttpContext.Current != null && HttpContext.Current.Request != null, MessagingStrings.HttpContextRequired);
+ return this.GetResponse(this.Channel.GetRequestFromContext());
+ }
+
+ /// <summary>
+ /// Gets an authentication response from a Provider.
+ /// </summary>
+ /// <param name="httpRequestInfo">The HTTP request that may be carrying an authentication response from the Provider.</param>
+ /// <returns>The processed authentication response if there is any; <c>null</c> otherwise.</returns>
+ public IAuthenticationResponse GetResponse(HttpRequestInfo httpRequestInfo) {
+ Contract.Requires<ArgumentNullException>(httpRequestInfo != null);
+ try {
+ var message = this.Channel.ReadFromRequest(httpRequestInfo);
+ PositiveAssertionResponse positiveAssertion;
+ NegativeAssertionResponse negativeAssertion;
+ IndirectSignedResponse positiveExtensionOnly;
+ if ((positiveAssertion = message as PositiveAssertionResponse) != null) {
+ // We need to make sure that this assertion is coming from an endpoint
+ // that the host deems acceptable.
+ var providerEndpoint = new SimpleXrdsProviderEndpoint(positiveAssertion);
+ ErrorUtilities.VerifyProtocol(
+ this.FilterEndpoint(providerEndpoint),
+ OpenIdStrings.PositiveAssertionFromNonQualifiedProvider,
+ providerEndpoint.Uri);
+
+ var response = new PositiveAuthenticationResponse(positiveAssertion, this);
+ foreach (var behavior in this.Behaviors) {
+ behavior.OnIncomingPositiveAssertion(response);
+ }
+
+ return response;
+ } else if ((positiveExtensionOnly = message as IndirectSignedResponse) != null) {
+ return new PositiveAnonymousResponse(positiveExtensionOnly);
+ } else if ((negativeAssertion = message as NegativeAssertionResponse) != null) {
+ return new NegativeAuthenticationResponse(negativeAssertion);
+ } else if (message != null) {
+ Logger.OpenId.WarnFormat("Received unexpected message type {0} when expecting an assertion message.", message.GetType().Name);
+ }
+
+ return null;
+ } catch (ProtocolException ex) {
+ return new FailedAuthenticationResponse(ex);
+ }
+ }
+
+ /// <summary>
+ /// Processes the response received in a popup window or iframe to an AJAX-directed OpenID authentication.
+ /// </summary>
+ /// <returns>The HTTP response to send to this HTTP request.</returns>
+ /// <remarks>
+ /// <para>Requires an <see cref="HttpContext.Current">HttpContext.Current</see> context.</para>
+ /// </remarks>
+ public OutgoingWebResponse ProcessResponseFromPopup() {
+ Contract.Requires<InvalidOperationException>(HttpContext.Current != null && HttpContext.Current.Request != null, MessagingStrings.HttpContextRequired);
+ Contract.Ensures(Contract.Result<OutgoingWebResponse>() != null);
+
+ return this.ProcessResponseFromPopup(this.Channel.GetRequestFromContext());
+ }
+
+ /// <summary>
+ /// Processes the response received in a popup window or iframe to an AJAX-directed OpenID authentication.
+ /// </summary>
+ /// <param name="request">The incoming HTTP request that is expected to carry an OpenID authentication response.</param>
+ /// <returns>The HTTP response to send to this HTTP request.</returns>
+ public OutgoingWebResponse ProcessResponseFromPopup(HttpRequestInfo request) {
+ Contract.Requires<ArgumentNullException>(request != null);
+ Contract.Ensures(Contract.Result<OutgoingWebResponse>() != null);
+
+ return this.ProcessResponseFromPopup(request, null);
+ }
+
+ /// <summary>
+ /// Allows an OpenID extension to read data out of an unverified positive authentication assertion
+ /// and send it down to the client browser so that Javascript running on the page can perform
+ /// some preprocessing on the extension data.
+ /// </summary>
+ /// <typeparam name="T">The extension <i>response</i> type that will read data from the assertion.</typeparam>
+ /// <param name="propertyName">The property name on the openid_identifier input box object that will be used to store the extension data. For example: sreg</param>
+ /// <remarks>
+ /// This method should be called before <see cref="ProcessResponseFromPopup()"/>.
+ /// </remarks>
+ [SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter", Justification = "By design")]
+ public void RegisterClientScriptExtension<T>(string propertyName) where T : IClientScriptExtensionResponse {
+ Contract.Requires<ArgumentException>(!string.IsNullOrEmpty(propertyName));
+ ErrorUtilities.VerifyArgumentNamed(!this.clientScriptExtensions.ContainsValue(propertyName), "propertyName", OpenIdStrings.ClientScriptExtensionPropertyNameCollision, propertyName);
+ foreach (var ext in this.clientScriptExtensions.Keys) {
+ ErrorUtilities.VerifyArgument(ext != typeof(T), OpenIdStrings.ClientScriptExtensionTypeCollision, typeof(T).FullName);
+ }
+ this.clientScriptExtensions.Add(typeof(T), propertyName);
+ }
+
+ #region 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
+
+ /// <summary>
+ /// Determines whether some parameter name belongs to OpenID or this library
+ /// as a protocol or internal parameter name.
+ /// </summary>
+ /// <param name="parameterName">Name of the parameter.</param>
+ /// <returns>
+ /// <c>true</c> if the named parameter is a library- or protocol-specific parameter; otherwise, <c>false</c>.
+ /// </returns>
+ internal static bool IsOpenIdSupportingParameter(string parameterName) {
+ // Yes, it is possible with some query strings to have a null or empty parameter name
+ if (string.IsNullOrEmpty(parameterName)) {
+ return false;
+ }
+
+ Protocol protocol = Protocol.Default;
+ return parameterName.StartsWith(protocol.openid.Prefix, StringComparison.OrdinalIgnoreCase)
+ || parameterName.StartsWith(OpenIdUtilities.CustomParameterPrefix, StringComparison.Ordinal);
+ }
+
+ /// <summary>
+ /// Creates a relying party that does not verify incoming messages against
+ /// nonce or association stores.
+ /// </summary>
+ /// <returns>The instantiated <see cref="OpenIdRelyingParty"/>.</returns>
+ /// <remarks>
+ /// Useful for previewing messages while
+ /// allowing them to be fully processed and verified later.
+ /// </remarks>
+ internal static OpenIdRelyingParty CreateNonVerifying() {
+ OpenIdRelyingParty rp = new OpenIdRelyingParty();
+ try {
+ rp.Channel = OpenIdChannel.CreateNonVerifyingChannel();
+ return rp;
+ } catch {
+ rp.Dispose();
+ throw;
+ }
+ }
+
+ /// <summary>
+ /// Processes the response received in a popup window or iframe to an AJAX-directed OpenID authentication.
+ /// </summary>
+ /// <param name="request">The incoming HTTP request that is expected to carry an OpenID authentication response.</param>
+ /// <param name="callback">The callback fired after the response status has been determined but before the Javascript response is formulated.</param>
+ /// <returns>
+ /// The HTTP response to send to this HTTP request.
+ /// </returns>
+ [SuppressMessage("Microsoft.Naming", "CA2204:Literals should be spelled correctly", MessageId = "OpenID", Justification = "real word"), SuppressMessage("Microsoft.Naming", "CA2204:Literals should be spelled correctly", MessageId = "iframe", Justification = "Code contracts")]
+ internal OutgoingWebResponse ProcessResponseFromPopup(HttpRequestInfo request, Action<AuthenticationStatus> callback) {
+ Contract.Requires<ArgumentNullException>(request != null);
+ Contract.Ensures(Contract.Result<OutgoingWebResponse>() != null);
+
+ string extensionsJson = null;
+ var authResponse = this.NonVerifyingRelyingParty.GetResponse();
+ ErrorUtilities.VerifyProtocol(authResponse != null, OpenIdStrings.PopupRedirectMissingResponse);
+
+ // Give the caller a chance to notify the hosting page and fill up the clientScriptExtensions collection.
+ if (callback != null) {
+ callback(authResponse.Status);
+ }
+
+ Logger.OpenId.DebugFormat("Popup or iframe callback from OP: {0}", request.Url);
+ Logger.Controls.DebugFormat(
+ "An authentication response was found in a popup window or iframe using a non-verifying RP with status: {0}",
+ authResponse.Status);
+ if (authResponse.Status == AuthenticationStatus.Authenticated) {
+ var extensionsDictionary = new Dictionary<string, string>();
+ 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)) {
+ extensionsDictionary[pair.Value] = js;
+ }
+ }
+
+ extensionsJson = MessagingUtilities.CreateJsonObject(extensionsDictionary, true);
+ }
+
+ string payload = "document.URL";
+ if (request.HttpMethod == "POST") {
+ // Promote all form variables to the query string, but since it won't be passed
+ // to any server (this is a javascript window-to-window transfer) the length of
+ // it can be arbitrarily long, whereas it was POSTed here probably because it
+ // was too long for HTTP transit.
+ UriBuilder payloadUri = new UriBuilder(request.Url);
+ payloadUri.AppendQueryArgs(request.Form.ToDictionary());
+ payload = MessagingUtilities.GetSafeJavascriptValue(payloadUri.Uri.AbsoluteUri);
+ }
+
+ if (!string.IsNullOrEmpty(extensionsJson)) {
+ payload += ", " + extensionsJson;
+ }
+
+ return InvokeParentPageScript("dnoa_internal.processAuthorizationResult(" + payload + ")");
+ }
+
+ /// <summary>
+ /// Performs discovery on the specified identifier.
+ /// </summary>
+ /// <param name="identifier">The identifier to discover services for.</param>
+ /// <returns>A non-null sequence of services discovered for the identifier.</returns>
+ internal IEnumerable<IdentifierDiscoveryResult> Discover(Identifier identifier) {
+ Contract.Requires<ArgumentNullException>(identifier != null);
+ Contract.Ensures(Contract.Result<IEnumerable<IdentifierDiscoveryResult>>() != null);
+
+ IEnumerable<IdentifierDiscoveryResult> results = Enumerable.Empty<IdentifierDiscoveryResult>();
+ foreach (var discoverer in this.DiscoveryServices) {
+ bool abortDiscoveryChain;
+ var discoveryResults = discoverer.Discover(identifier, this.WebRequestHandler, out abortDiscoveryChain).CacheGeneratedResults();
+ results = results.Concat(discoveryResults);
+ if (abortDiscoveryChain) {
+ Logger.OpenId.InfoFormat("Further discovery on '{0}' was stopped by the {1} discovery service.", identifier, discoverer.GetType().Name);
+ break;
+ }
+ }
+
+ // If any OP Identifier service elements were found, we must not proceed
+ // to use any Claimed Identifier services, per OpenID 2.0 sections 7.3.2.2 and 11.2.
+ // For a discussion on this topic, see
+ // http://groups.google.com/group/dotnetopenid/browse_thread/thread/4b5a8c6b2210f387/5e25910e4d2252c8
+ // Sometimes the IIdentifierDiscoveryService will automatically filter this for us, but
+ // just to be sure, we'll do it here as well.
+ if (!this.SecuritySettings.AllowDualPurposeIdentifiers) {
+ results = results.CacheGeneratedResults(); // avoid performing discovery repeatedly
+ var opIdentifiers = results.Where(result => result.ClaimedIdentifier == result.Protocol.ClaimedIdentifierForOPIdentifier);
+ var claimedIdentifiers = results.Where(result => result.ClaimedIdentifier != result.Protocol.ClaimedIdentifierForOPIdentifier);
+ results = opIdentifiers.Any() ? opIdentifiers : claimedIdentifiers;
+ }
+
+ return results;
+ }
+
+ /// <summary>
+ /// Checks whether a given OP Endpoint is permitted by the host relying party.
+ /// </summary>
+ /// <param name="endpoint">The OP endpoint.</param>
+ /// <returns><c>true</c> if the OP Endpoint is allowed; <c>false</c> otherwise.</returns>
+ protected internal bool FilterEndpoint(IProviderEndpoint endpoint) {
+ if (this.SecuritySettings.RejectAssertionsFromUntrustedProviders) {
+ if (!this.SecuritySettings.TrustedProviderEndpoints.Contains(endpoint.Uri)) {
+ Logger.OpenId.InfoFormat("Filtering out OP endpoint {0} because it is not on the exclusive trusted provider whitelist.", endpoint.Uri.AbsoluteUri);
+ return false;
+ }
+ }
+
+ if (endpoint.Version < Protocol.Lookup(this.SecuritySettings.MinimumRequiredOpenIdVersion).Version) {
+ Logger.OpenId.InfoFormat(
+ "Filtering out OP endpoint {0} because it implements OpenID {1} but this relying party requires OpenID {2} or later.",
+ endpoint.Uri.AbsoluteUri,
+ endpoint.Version,
+ Protocol.Lookup(this.SecuritySettings.MinimumRequiredOpenIdVersion).Version);
+ return false;
+ }
+
+ if (this.EndpointFilter != null) {
+ if (!this.EndpointFilter(endpoint)) {
+ Logger.OpenId.InfoFormat("Filtering out OP endpoint {0} because the host rejected it.", endpoint.Uri.AbsoluteUri);
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /// <summary>
+ /// Releases unmanaged and - optionally - managed resources
+ /// </summary>
+ /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
+ protected virtual void Dispose(bool disposing) {
+ if (disposing) {
+ if (this.nonVerifyingRelyingParty != null) {
+ this.nonVerifyingRelyingParty.Dispose();
+ this.nonVerifyingRelyingParty = null;
+ }
+
+ // Tear off the instance member as a local variable for thread safety.
+ IDisposable disposableChannel = this.channel as IDisposable;
+ if (disposableChannel != null) {
+ disposableChannel.Dispose();
+ }
+ }
+ }
+
+ /// <summary>
+ /// Invokes a method on a parent frame or window and closes the calling popup window if applicable.
+ /// </summary>
+ /// <param name="methodCall">The method to call on the parent window, including
+ /// parameters. (i.e. "callback('arg1', 2)"). No escaping is done by this method.</param>
+ /// <returns>The entire HTTP response to send to the popup window or iframe to perform the invocation.</returns>
+ private static OutgoingWebResponse InvokeParentPageScript(string methodCall) {
+ Contract.Requires<ArgumentException>(!string.IsNullOrEmpty(methodCall));
+
+ Logger.OpenId.DebugFormat("Sending Javascript callback: {0}", methodCall);
+ StringBuilder builder = new StringBuilder();
+ builder.AppendLine("<html><body><script type='text/javascript' language='javascript'><!--");
+ builder.AppendLine("//<![CDATA[");
+ builder.Append(@" var inPopup = !window.frameElement;
+ var objSrc = inPopup ? window.opener : window.frameElement;
+");
+
+ // 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) {{
+ try {{
+ objSrc.{0};
+ }} catch (ex) {{
+ alert(ex);
+ }} finally {{
+ window.self.close();
+ }}
+ }} else {{
+ objSrc.{0};
+ }}";
+ builder.AppendFormat(CultureInfo.InvariantCulture, htmlFormat, methodCall);
+ builder.AppendLine("//]]>--></script>");
+ builder.AppendLine("</body></html>");
+
+ var response = new OutgoingWebResponse();
+ response.Body = builder.ToString();
+ response.Headers.Add(HttpResponseHeader.ContentType, new ContentType("text/html").ToString());
+ return response;
+ }
+
+ /// <summary>
+ /// Called by derived classes when behaviors are added or removed.
+ /// </summary>
+ /// <param name="sender">The collection being modified.</param>
+ /// <param name="e">The <see cref="System.Collections.Specialized.NotifyCollectionChangedEventArgs"/> instance containing the event data.</param>
+ private void OnBehaviorsChanged(object sender, NotifyCollectionChangedEventArgs e) {
+ foreach (IRelyingPartyBehavior profile in e.NewItems) {
+ profile.ApplySecuritySettings(this.SecuritySettings);
+ Reporting.RecordFeatureUse(profile);
+ }
+ }
+
+#if CONTRACTS_FULL
+ /// <summary>
+ /// Verifies conditions that should be true for any valid state of this object.
+ /// </summary>
+ [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "Called by code contracts.")]
+ [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "Called by code contracts.")]
+ [ContractInvariantMethod]
+ private void ObjectInvariant() {
+ Contract.Invariant(this.SecuritySettings != null);
+ Contract.Invariant(this.Channel != null);
+ Contract.Invariant(this.EndpointOrder != null);
+ }
+#endif
+ }
+}
diff --git a/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/OpenIdRelyingPartyAjaxControlBase.cs b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/OpenIdRelyingPartyAjaxControlBase.cs
new file mode 100644
index 0000000..eaaba8c
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/OpenIdRelyingPartyAjaxControlBase.cs
@@ -0,0 +1,468 @@
+//-----------------------------------------------------------------------
+// <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.Globalization;
+ using System.Linq;
+ using System.Text;
+ using System.Web;
+ using System.Web.Script.Serialization;
+ using System.Web.UI;
+ using DotNetOpenAuth.Configuration;
+ using DotNetOpenAuth.Messaging;
+ using DotNetOpenAuth.OpenId.Extensions;
+
+ /// <summary>
+ /// A common base class for OpenID Relying Party controls.
+ /// </summary>
+ public 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 "dnoa.op_endpoint" string.
+ /// </summary>
+ internal const string OPEndpointParameterName = OpenIdUtilities.CustomParameterPrefix + "op_endpoint";
+
+ /// <summary>
+ /// The "dnoa.claimed_id" string.
+ /// </summary>
+ internal const string ClaimedIdParameterName = OpenIdUtilities.CustomParameterPrefix + "claimed_id";
+
+ /// <summary>
+ /// The name of the javascript field that stores the maximum time a positive assertion is
+ /// good for before it must be refreshed.
+ /// </summary>
+ internal const string MaxPositiveAssertionLifetimeJsName = "window.dnoa_internal.maxPositiveAssertionLifetime";
+
+ /// <summary>
+ /// The name of the javascript function that will initiate an asynchronous callback.
+ /// </summary>
+ protected internal const string CallbackJSFunctionAsync = "window.dnoa_internal.callbackAsync";
+
+ /// <summary>
+ /// The name of the javascript function that will initiate a synchronous callback.
+ /// </summary>
+ protected const string CallbackJSFunction = "window.dnoa_internal.callback";
+
+ #region Property viewstate keys
+
+ /// <summary>
+ /// The viewstate key to use for storing the value of a successful authentication.
+ /// </summary>
+ private const string AuthDataViewStateKey = "AuthData";
+
+ /// <summary>
+ /// The viewstate key to use for storing the value of the <see cref="AuthenticationResponse"/> property.
+ /// </summary>
+ private const string AuthenticationResponseViewStateKey = "AuthenticationResponse";
+
+ /// <summary>
+ /// The viewstate key to use for storing the value of the <see cref="AuthenticationProcessedAlready"/> property.
+ /// </summary>
+ private const string AuthenticationProcessedAlreadyViewStateKey = "AuthenticationProcessedAlready";
+
+ #endregion
+
+ /// <summary>
+ /// Default value of the <see cref="Popup"/> property.
+ /// </summary>
+ private const PopupBehavior PopupDefault = PopupBehavior.Always;
+
+ /// <summary>
+ /// Default value of <see cref="LogOnMode"/> property..
+ /// </summary>
+ private const LogOnSiteNotification LogOnModeDefault = LogOnSiteNotification.None;
+
+ /// <summary>
+ /// The authentication response that just came in.
+ /// </summary>
+ private IAuthenticationResponse authenticationResponse;
+
+ /// <summary>
+ /// Stores the result of an AJAX discovery request while it is waiting
+ /// to be picked up by ASP.NET on the way down to the user agent.
+ /// </summary>
+ private string discoveryResult;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="OpenIdRelyingPartyAjaxControlBase"/> class.
+ /// </summary>
+ protected OpenIdRelyingPartyAjaxControlBase() {
+ // The AJAX login style always uses popups (or invisible iframes).
+ base.Popup = PopupDefault;
+
+ // The expected use case for the AJAX login box is for comments... not logging in.
+ this.LogOnMode = LogOnModeDefault;
+ }
+
+ /// <summary>
+ /// Fired when a Provider sends back a positive assertion to this control,
+ /// but the authentication has not yet been verified.
+ /// </summary>
+ /// <remarks>
+ /// <b>No security critical decisions should be made within event handlers
+ /// for this event</b> as the authenticity of the assertion has not been
+ /// verified yet. All security related code should go in the event handler
+ /// for the <see cref="OpenIdRelyingPartyControlBase.LoggedIn"/> event.
+ /// </remarks>
+ [Description("Fired when a Provider sends back a positive assertion to this control, but the authentication has not yet been verified.")]
+ public event EventHandler<OpenIdEventArgs> UnconfirmedPositiveAssertion;
+
+ /// <summary>
+ /// Gets or sets a value indicating when to use a popup window to complete the login experience.
+ /// </summary>
+ /// <value>The default value is <see cref="PopupBehavior.Never"/>.</value>
+ [Bindable(false), Browsable(false), DefaultValue(PopupDefault)]
+ public override PopupBehavior Popup {
+ get { return base.Popup; }
+ set { ErrorUtilities.VerifySupported(value == base.Popup, OpenIdStrings.PropertyValueNotSupported); }
+ }
+
+ /// <summary>
+ /// Gets or sets the way a completed login is communicated to the rest of the web site.
+ /// </summary>
+ [Bindable(true), DefaultValue(LogOnModeDefault), Category(BehaviorCategory)]
+ [Description("The way a completed login is communicated to the rest of the web site.")]
+ public override LogOnSiteNotification LogOnMode { // override to set new DefaultValue
+ get { return base.LogOnMode; }
+ set { base.LogOnMode = value; }
+ }
+
+ /// <summary>
+ /// Gets or sets the <see cref="OpenIdRelyingParty"/> instance to use.
+ /// </summary>
+ /// <value>
+ /// The default value is an <see cref="OpenIdRelyingParty"/> instance initialized according to the web.config file.
+ /// </value>
+ /// <remarks>
+ /// A performance optimization would be to store off the
+ /// instance as a static member in your web site and set it
+ /// to this property in your <see cref="Control.Load">Page.Load</see>
+ /// event since instantiating these instances can be expensive on
+ /// heavily trafficked web pages.
+ /// </remarks>
+ public override OpenIdRelyingParty RelyingParty {
+ get {
+ return base.RelyingParty;
+ }
+
+ set {
+ // Make sure we get an AJAX-ready instance.
+ ErrorUtilities.VerifyArgument(value is OpenIdAjaxRelyingParty, OpenIdStrings.TypeMustImplementX, typeof(OpenIdAjaxRelyingParty).Name);
+ base.RelyingParty = value;
+ }
+ }
+
+ /// <summary>
+ /// Gets the completed authentication response.
+ /// </summary>
+ public IAuthenticationResponse AuthenticationResponse {
+ get {
+ if (this.authenticationResponse == null) {
+ // We will either validate a new response and return a live AuthenticationResponse
+ // or we will try to deserialize a previous IAuthenticationResponse (snapshot)
+ // from viewstate and return that.
+ IAuthenticationResponse viewstateResponse = this.ViewState[AuthenticationResponseViewStateKey] as IAuthenticationResponse;
+ string viewstateAuthData = this.ViewState[AuthDataViewStateKey] as string;
+ string formAuthData = this.Page.Request.Form[this.OpenIdAuthDataFormKey];
+
+ // First see if there is fresh auth data to be processed into a response.
+ if (!string.IsNullOrEmpty(formAuthData) && !string.Equals(viewstateAuthData, formAuthData, StringComparison.Ordinal)) {
+ this.ViewState[AuthDataViewStateKey] = formAuthData;
+
+ Uri authUri = new Uri(formAuthData);
+ HttpRequestInfo clientResponseInfo = new HttpRequestInfo {
+ UrlBeforeRewriting = authUri,
+ };
+
+ this.authenticationResponse = this.RelyingParty.GetResponse(clientResponseInfo);
+ Logger.Controls.DebugFormat(
+ "The {0} control checked for an authentication response and found: {1}",
+ this.ID,
+ this.authenticationResponse.Status);
+ this.AuthenticationProcessedAlready = false;
+
+ // Save out the authentication response to viewstate so we can find it on
+ // a subsequent postback.
+ this.ViewState[AuthenticationResponseViewStateKey] = new PositiveAuthenticationResponseSnapshot(this.authenticationResponse);
+ } else {
+ this.authenticationResponse = viewstateResponse;
+ }
+ }
+
+ return this.authenticationResponse;
+ }
+ }
+
+ /// <summary>
+ /// Gets the relying party as its AJAX type.
+ /// </summary>
+ protected OpenIdAjaxRelyingParty AjaxRelyingParty {
+ get { return (OpenIdAjaxRelyingParty)this.RelyingParty; }
+ }
+
+ /// <summary>
+ /// Gets the name of the open id auth data form key (for the value as stored at the user agent as a FORM field).
+ /// </summary>
+ /// <value>Usually a concatenation of the control's name and <c>"_openidAuthData"</c>.</value>
+ protected abstract string OpenIdAuthDataFormKey { get; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether an authentication in the page's view state
+ /// has already been processed and appropriate events fired.
+ /// </summary>
+ private bool AuthenticationProcessedAlready {
+ get { return (bool)(ViewState[AuthenticationProcessedAlreadyViewStateKey] ?? false); }
+ set { ViewState[AuthenticationProcessedAlreadyViewStateKey] = value; }
+ }
+
+ /// <summary>
+ /// Allows an OpenID extension to read data out of an unverified positive authentication assertion
+ /// and send it down to the client browser so that Javascript running on the page can perform
+ /// some preprocessing on the extension data.
+ /// </summary>
+ /// <typeparam name="T">The extension <i>response</i> type that will read data from the assertion.</typeparam>
+ /// <param name="propertyName">The property name on the openid_identifier input box object that will be used to store the extension data. For example: sreg</param>
+ /// <remarks>
+ /// This method should be called from the <see cref="UnconfirmedPositiveAssertion"/> event handler.
+ /// </remarks>
+ [SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter", Justification = "By design")]
+ public void RegisterClientScriptExtension<T>(string propertyName) where T : IClientScriptExtensionResponse {
+ Contract.Requires<ArgumentException>(!string.IsNullOrEmpty(propertyName));
+ this.RelyingParty.RegisterClientScriptExtension<T>(propertyName);
+ }
+
+ #region ICallbackEventHandler Members
+
+ /// <summary>
+ /// Returns the result of discovery on some Identifier passed to <see cref="ICallbackEventHandler.RaiseCallbackEvent"/>.
+ /// </summary>
+ /// <returns>The result of the callback.</returns>
+ /// <value>A whitespace delimited list of URLs that can be used to initiate authentication.</value>
+ string ICallbackEventHandler.GetCallbackResult() {
+ return this.GetCallbackResult();
+ }
+
+ /// <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>
+ [SuppressMessage("Microsoft.Design", "CA1030:UseEventsWhereAppropriate", Justification = "We want to preserve the signature of the interface.")]
+ void ICallbackEventHandler.RaiseCallbackEvent(string eventArgument) {
+ this.RaiseCallbackEvent(eventArgument);
+ }
+
+ #endregion
+
+ /// <summary>
+ /// Returns the results of a callback event that targets a control.
+ /// </summary>
+ /// <returns>The result of the callback.</returns>
+ [SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate", Justification = "We want to preserve the signature of the interface.")]
+ protected virtual string GetCallbackResult() {
+ this.Page.Response.ContentType = "text/javascript";
+ return this.discoveryResult;
+ }
+
+ /// <summary>
+ /// Processes a callback event that targets a control.
+ /// </summary>
+ /// <param name="eventArgument">A string that represents an event argument to pass to the event handler.</param>
+ [SuppressMessage("Microsoft.Design", "CA1030:UseEventsWhereAppropriate", Justification = "We want to preserve the signature of the interface.")]
+ protected virtual void RaiseCallbackEvent(string eventArgument) {
+ string userSuppliedIdentifier = eventArgument;
+
+ ErrorUtilities.VerifyNonZeroLength(userSuppliedIdentifier, "userSuppliedIdentifier");
+ Logger.OpenId.InfoFormat("AJAX discovery on {0} requested.", userSuppliedIdentifier);
+
+ this.Identifier = userSuppliedIdentifier;
+
+ var serializer = new JavaScriptSerializer();
+ IEnumerable<IAuthenticationRequest> requests = this.CreateRequests(this.Identifier);
+ this.discoveryResult = serializer.Serialize(this.AjaxRelyingParty.AsJsonDiscoveryResult(requests));
+ }
+
+ /// <summary>
+ /// Creates the relying party instance used to generate authentication requests.
+ /// </summary>
+ /// <param name="store">The store to pass to the relying party constructor.</param>
+ /// <returns>The instantiated relying party.</returns>
+ protected override OpenIdRelyingParty CreateRelyingParty(IOpenIdApplicationStore store) {
+ return new OpenIdAjaxRelyingParty(store);
+ }
+
+ /// <summary>
+ /// Pre-discovers an identifier and makes the results available to the
+ /// user agent for javascript as soon as the page loads.
+ /// </summary>
+ /// <param name="identifier">The identifier.</param>
+ protected void PreloadDiscovery(Identifier identifier) {
+ this.PreloadDiscovery(new[] { identifier });
+ }
+
+ /// <summary>
+ /// Pre-discovers a given set of identifiers and makes the results available to the
+ /// user agent for javascript as soon as the page loads.
+ /// </summary>
+ /// <param name="identifiers">The identifiers to perform discovery on.</param>
+ protected void PreloadDiscovery(IEnumerable<Identifier> identifiers) {
+ string script = this.AjaxRelyingParty.AsAjaxPreloadedDiscoveryResult(
+ identifiers.SelectMany(id => this.CreateRequests(id)));
+ this.Page.ClientScript.RegisterClientScriptBlock(typeof(OpenIdRelyingPartyAjaxControlBase), this.ClientID, script, true);
+ }
+
+ /// <summary>
+ /// Fires the <see cref="UnconfirmedPositiveAssertion"/> event.
+ /// </summary>
+ protected virtual void OnUnconfirmedPositiveAssertion() {
+ var unconfirmedPositiveAssertion = this.UnconfirmedPositiveAssertion;
+ if (unconfirmedPositiveAssertion != null) {
+ unconfirmedPositiveAssertion(this, null);
+ }
+ }
+
+ /// <summary>
+ /// Raises the <see cref="E:Load"/> event.
+ /// </summary>
+ /// <param name="e">The <see cref="System.EventArgs"/> instance containing the event data.</param>
+ protected override void OnLoad(EventArgs e) {
+ base.OnLoad(e);
+
+ // Our parent control ignores all OpenID messages included in a postback,
+ // but our AJAX controls hide an old OpenID message in a postback payload,
+ // so we deserialize it and process it when appropriate.
+ if (this.Page.IsPostBack) {
+ if (this.AuthenticationResponse != null && !this.AuthenticationProcessedAlready) {
+ // Only process messages targeted at this control.
+ // Note that Stateless mode causes no receiver to be indicated.
+ string receiver = this.AuthenticationResponse.GetUntrustedCallbackArgument(ReturnToReceivingControlId);
+ if (receiver == null || receiver == this.ClientID) {
+ this.ProcessResponse(this.AuthenticationResponse);
+ this.AuthenticationProcessedAlready = true;
+ }
+ }
+ }
+ }
+
+ /// <summary>
+ /// Called when the <see cref="Identifier"/> property is changed.
+ /// </summary>
+ protected override void OnIdentifierChanged() {
+ base.OnIdentifierChanged();
+
+ // Since the identifier changed, make sure we reset any cached authentication on the user agent.
+ this.ViewState.Remove(AuthDataViewStateKey);
+ }
+
+ /// <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.SetWebAppPathOnUserAgent();
+ this.Page.ClientScript.RegisterClientScriptResource(typeof(OpenIdRelyingPartyAjaxControlBase), EmbeddedAjaxJavascriptResource);
+
+ StringBuilder initScript = new StringBuilder();
+
+ initScript.AppendLine(CallbackJSFunctionAsync + " = " + this.GetJsCallbackConvenienceFunction(true));
+ initScript.AppendLine(CallbackJSFunction + " = " + this.GetJsCallbackConvenienceFunction(false));
+
+ // Positive assertions can last no longer than this library is willing to consider them valid,
+ // and when they come with OP private associations they last no longer than the OP is willing
+ // to consider them valid. We assume the OP will hold them valid for at least five minutes.
+ double assertionLifetimeInMilliseconds = Math.Min(TimeSpan.FromMinutes(5).TotalMilliseconds, Math.Min(OpenIdElement.Configuration.MaxAuthenticationTime.TotalMilliseconds, DotNetOpenAuthSection.Messaging.MaximumMessageLifetime.TotalMilliseconds));
+ initScript.AppendLine(MaxPositiveAssertionLifetimeJsName + " = " + assertionLifetimeInMilliseconds.ToString(CultureInfo.InvariantCulture) + ";");
+
+ // We register this callback code explicitly with a specific type rather than the derived-type of the control
+ // to ensure that this discovery callback function is only set ONCE for the HTML document.
+ this.Page.ClientScript.RegisterClientScriptBlock(typeof(OpenIdRelyingPartyControlBase), "initializer", initScript.ToString(), true);
+ }
+
+ /// <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) {
+ Contract.Assume(writer != null, "Missing contract.");
+ base.Render(writer);
+
+ // Emit a hidden field to let the javascript on the user agent know if an
+ // authentication has already successfully taken place.
+ string viewstateAuthData = this.ViewState[AuthDataViewStateKey] as string;
+ if (!string.IsNullOrEmpty(viewstateAuthData)) {
+ writer.AddAttribute(HtmlTextWriterAttribute.Name, this.OpenIdAuthDataFormKey);
+ writer.AddAttribute(HtmlTextWriterAttribute.Value, viewstateAuthData, true);
+ writer.AddAttribute(HtmlTextWriterAttribute.Type, "hidden");
+ writer.RenderBeginTag(HtmlTextWriterTag.Input);
+ writer.RenderEndTag();
+ }
+ }
+
+ /// <summary>
+ /// Notifies the user agent via an AJAX response of a completed authentication attempt.
+ /// </summary>
+ protected override void ScriptClosingPopupOrIFrame() {
+ Action<AuthenticationStatus> callback = status => {
+ if (status == AuthenticationStatus.Authenticated) {
+ this.OnUnconfirmedPositiveAssertion(); // event handler will fill the clientScriptExtensions collection.
+ }
+ };
+
+ OutgoingWebResponse response = this.RelyingParty.ProcessResponseFromPopup(
+ this.RelyingParty.Channel.GetRequestFromContext(),
+ callback);
+
+ response.Respond();
+ }
+
+ /// <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>
+ /// Sets the window.aspnetapppath variable on the user agent so that cookies can be set with the proper path.
+ /// </summary>
+ private void SetWebAppPathOnUserAgent() {
+ string script = "window.aspnetapppath = " + MessagingUtilities.GetSafeJavascriptValue(this.Page.Request.ApplicationPath) + ";";
+ this.Page.ClientScript.RegisterClientScriptBlock(typeof(OpenIdRelyingPartyAjaxControlBase), "webapppath", script, true);
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/OpenIdRelyingPartyAjaxControlBase.js b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/OpenIdRelyingPartyAjaxControlBase.js
new file mode 100644
index 0000000..4de5188
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/OpenIdRelyingPartyAjaxControlBase.js
@@ -0,0 +1,751 @@
+//-----------------------------------------------------------------------
+// <copyright file="OpenIdRelyingPartyAjaxControlBase.js" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// This file may be used and redistributed under the terms of the
+// Microsoft Public License (Ms-PL) http://opensource.org/licenses/ms-pl.html
+// </copyright>
+//-----------------------------------------------------------------------
+
+if (window.dnoa_internal === undefined) {
+ window.dnoa_internal = {};
+}
+
+/// <summary>Removes a given element from the array.</summary>
+/// <returns>True if the element was in the array, or false if it was not found.</returns>
+Array.prototype.remove = function(element) {
+ function elementToRemoveLast(a, b) {
+ if (a == element) { return 1; }
+ if (b == element) { return -1; }
+ return 0;
+ }
+ this.sort(elementToRemoveLast);
+ if (this[this.length - 1] == element) {
+ this.pop();
+ return true;
+ } else {
+ return false;
+ }
+};
+
+// Renders all the parameters in their string form, surrounded by parentheses.
+window.dnoa_internal.argsToString = function() {
+ result = "(";
+ for (var i = 0; i < arguments.length; i++) {
+ if (i > 0) { result += ', '; }
+ var arg = arguments[i];
+ if (typeof (arg) == 'string') {
+ arg = '"' + arg + '"';
+ } else if (arg === null) {
+ arg = '[null]';
+ } else if (arg === undefined) {
+ arg = '[undefined]';
+ }
+ result += arg.toString();
+ }
+ result += ')';
+ return result;
+};
+
+window.dnoa_internal.registerEvent = function(name) {
+ var filterOnApplicability = function(fn, domElement) {
+ /// <summary>Wraps a given function with a check so that the function only executes when a given element is still in the DOM.</summary>
+ return function() {
+ var args = Array.prototype.slice.call(arguments);
+ if (!domElement) {
+ // no element used as a basis of applicability indicates we always fire this callback.
+ fn.apply(null, args);
+ } else {
+ var elements = document.getElementsByTagName(domElement.tagName);
+ var isElementInDom = false;
+ for (var i = 0; i < elements.length; i++) {
+ if (elements[i] === domElement) {
+ isElementInDom = true;
+ break;
+ }
+ }
+ if (isElementInDom) {
+ fn.apply(null, args);
+ }
+ }
+ }
+ };
+
+ window.dnoa_internal[name + 'Listeners'] = [];
+ window.dnoa_internal['add' + name] = function(fn, whileDomElementApplicable) { window.dnoa_internal[name + 'Listeners'].push(filterOnApplicability(fn, whileDomElementApplicable)); };
+ window.dnoa_internal['remove' + name] = function(fn) { window.dnoa_internal[name + 'Listeners'].remove(fn); };
+ window.dnoa_internal['fire' + name] = function() {
+ var args = Array.prototype.slice.call(arguments);
+ trace('Firing event ' + name + window.dnoa_internal.argsToString.apply(null, args), 'blue');
+ var listeners = window.dnoa_internal[name + 'Listeners'];
+ for (var i = 0; i < listeners.length; i++) {
+ listeners[i].apply(null, args);
+ }
+ };
+};
+
+window.dnoa_internal.registerEvent('DiscoveryStarted'); // (identifier) - fired when a discovery callback is ACTUALLY made to the RP
+window.dnoa_internal.registerEvent('DiscoverySuccess'); // (identifier, discoveryResult, { fresh: true|false }) - fired after a discovery callback is returned from the RP successfully or a cached result is retrieved
+window.dnoa_internal.registerEvent('DiscoveryFailed'); // (identifier, message) - fired after a discovery callback fails
+window.dnoa_internal.registerEvent('AuthStarted'); // (discoveryResult, serviceEndpoint, { background: true|false })
+window.dnoa_internal.registerEvent('AuthFailed'); // (discoveryResult, serviceEndpoint, { background: true|false }) - fired for each individual ServiceEndpoint, and once at last with serviceEndpoint==null if all failed
+window.dnoa_internal.registerEvent('AuthSuccess'); // (discoveryResult, serviceEndpoint, extensionResponses, { background: true|false, deserialized: true|false })
+window.dnoa_internal.registerEvent('AuthCleared'); // (discoveryResult, serviceEndpoint)
+
+window.dnoa_internal.discoveryResults = []; // user supplied identifiers and discovery results
+window.dnoa_internal.discoveryInProgress = []; // identifiers currently being discovered and their callbacks
+
+// The possible authentication results
+window.dnoa_internal.authSuccess = 'auth-success';
+window.dnoa_internal.authRefused = 'auth-refused';
+window.dnoa_internal.timedOut = 'timed-out';
+
+/// <summary>Instantiates a new FrameManager.</summary>
+/// <param name="maxFrames">The maximum number of concurrent 'jobs' (authentication attempts).</param>
+window.dnoa_internal.FrameManager = function(maxFrames) {
+ this.queuedWork = [];
+ this.frames = [];
+ this.maxFrames = maxFrames;
+
+ /// <summary>Called to queue up some work that will use an iframe as soon as it is available.</summary>
+ /// <param name="job">
+ /// A delegate that must return { url: /*to point the iframe to*/, onCanceled: /* callback */ }
+ /// Its first parameter is the iframe created to service the request.
+ /// It will only be called when the work actually begins.
+ /// </param>
+ /// <param name="p1">Arbitrary additional parameter to pass to the job.</param>
+ this.enqueueWork = function(job, p1) {
+ // Assign an iframe to this task immediately if there is one available.
+ if (this.frames.length < this.maxFrames) {
+ this.createIFrame(job, p1);
+ } else {
+ this.queuedWork.unshift({ job: job, p1: p1 });
+ }
+ };
+
+ /// <summary>Clears the job queue and immediately closes all iframes.</summary>
+ this.cancelAllWork = function() {
+ trace('Canceling all open and pending iframes.');
+ while (this.queuedWork.pop()) { }
+ this.closeFrames();
+ };
+
+ /// <summary>An event fired when a frame is closing.</summary>
+ this.onJobCompleted = function() {
+ // If there is a job in the queue, go ahead and start it up.
+ if (jobDesc = this.queuedWork.pop()) {
+ this.createIFrame(jobDesc.job, jobDesc.p1);
+ }
+ };
+
+ this.createIFrame = function(job, p1) {
+ var iframe = document.createElement("iframe");
+ if (!window.openid_visible_iframe) {
+ iframe.setAttribute("width", 0);
+ iframe.setAttribute("height", 0);
+ iframe.setAttribute("style", "display: none");
+ }
+ var jobDescription = job(iframe, p1);
+ iframe.setAttribute("src", jobDescription.url);
+ iframe.onCanceled = jobDescription.onCanceled;
+ iframe.dnoa_internal = window.dnoa_internal;
+ document.body.insertBefore(iframe, document.body.firstChild);
+ this.frames.push(iframe);
+ return iframe;
+ };
+
+ this.closeFrames = function() {
+ if (this.frames.length === 0) { return false; }
+ for (var i = 0; i < this.frames.length; i++) {
+ this.frames[i].src = "about:blank"; // doesn't have to exist. Just stop its processing.
+ if (this.frames[i].parentNode) { this.frames[i].parentNode.removeChild(this.frames[i]); }
+ }
+ while (this.frames.length > 0) {
+ var frame = this.frames.pop();
+ if (frame.onCanceled) { frame.onCanceled(); }
+ }
+ return true;
+ };
+
+ this.closeFrame = function(frame) {
+ frame.src = "about:blank"; // doesn't have to exist. Just stop its processing.
+ if (frame.parentNode) { frame.parentNode.removeChild(frame); }
+ var removed = this.frames.remove(frame);
+ this.onJobCompleted();
+ return removed;
+ };
+};
+
+/// <summary>Instantiates an object that represents an OpenID Identifier.</summary>
+window.OpenIdIdentifier = function(identifier) {
+ if (!identifier || identifier.length === 0) {
+ throw 'Error: trying to create OpenIdIdentifier for null or empty string.';
+ }
+
+ /// <summary>Performs discovery on the identifier.</summary>
+ /// <param name="onDiscoverSuccess">A function(DiscoveryResult) callback to be called when discovery has completed successfully.</param>
+ /// <param name="onDiscoverFailure">A function callback to be called when discovery has completed in failure.</param>
+ this.discover = function(onDiscoverSuccess, onDiscoverFailure) {
+ /// <summary>Receives the results of a successful discovery (even if it yielded 0 results).</summary>
+ function discoverSuccessCallback(discoveryResult, identifier) {
+ trace('Discovery completed for: ' + identifier);
+
+ // Deserialize the JSON object and store the result if it was a successful discovery.
+ discoveryResult = eval('(' + discoveryResult + ')');
+
+ // Add behavior for later use.
+ discoveryResult = new window.dnoa_internal.DiscoveryResult(identifier, discoveryResult);
+ window.dnoa_internal.discoveryResults[identifier] = discoveryResult;
+
+ window.dnoa_internal.fireDiscoverySuccess(identifier, discoveryResult, { fresh: true });
+
+ // Clear our "in discovery" state and fire callbacks
+ var callbacks = window.dnoa_internal.discoveryInProgress[identifier];
+ window.dnoa_internal.discoveryInProgress[identifier] = null;
+
+ if (callbacks) {
+ for (var i = 0; i < callbacks.onSuccess.length; i++) {
+ if (callbacks.onSuccess[i]) {
+ callbacks.onSuccess[i](discoveryResult);
+ }
+ }
+ }
+ }
+
+ /// <summary>Receives the discovery failure notification.</summary>
+ function discoverFailureCallback(message, userSuppliedIdentifier) {
+ trace('Discovery failed for: ' + identifier);
+
+ // Clear our "in discovery" state and fire callbacks
+ var callbacks = window.dnoa_internal.discoveryInProgress[identifier];
+ window.dnoa_internal.discoveryInProgress[identifier] = null;
+
+ if (callbacks) {
+ for (var i = 0; i < callbacks.onSuccess.length; i++) {
+ if (callbacks.onFailure[i]) {
+ callbacks.onFailure[i](message);
+ }
+ }
+ }
+
+ window.dnoa_internal.fireDiscoveryFailed(identifier, message);
+ }
+
+ if (window.dnoa_internal.discoveryResults[identifier]) {
+ trace("We've already discovered " + identifier + " so we're using the cached version.");
+
+ // In this special case, we never fire the DiscoveryStarted event.
+ window.dnoa_internal.fireDiscoverySuccess(identifier, window.dnoa_internal.discoveryResults[identifier], { fresh: false });
+
+ if (onDiscoverSuccess) {
+ onDiscoverSuccess(window.dnoa_internal.discoveryResults[identifier]);
+ }
+
+ return;
+ }
+
+ window.dnoa_internal.fireDiscoveryStarted(identifier);
+
+ if (!window.dnoa_internal.discoveryInProgress[identifier]) {
+ trace('starting discovery on ' + identifier);
+ window.dnoa_internal.discoveryInProgress[identifier] = {
+ onSuccess: [onDiscoverSuccess],
+ onFailure: [onDiscoverFailure]
+ };
+ window.dnoa_internal.callbackAsync(identifier, discoverSuccessCallback, discoverFailureCallback);
+ } else {
+ trace('Discovery on ' + identifier + ' already started. Registering an additional callback.');
+ window.dnoa_internal.discoveryInProgress[identifier].onSuccess.push(onDiscoverSuccess);
+ window.dnoa_internal.discoveryInProgress[identifier].onFailure.push(onDiscoverFailure);
+ }
+ };
+
+ /// <summary>Performs discovery and immediately begins checkid_setup to authenticate the user using a given identifier.</summary>
+ this.login = function(onSuccess, onLoginFailure) {
+ this.discover(function(discoveryResult) {
+ if (discoveryResult) {
+ trace('Discovery succeeded and found ' + discoveryResult.length + ' OpenID service endpoints.');
+ if (discoveryResult.length > 0) {
+ discoveryResult[0].loginPopup(onSuccess, onLoginFailure);
+ } else {
+ trace("This doesn't look like an OpenID Identifier. Aborting login.");
+ if (onLoginFailure) {
+ onLoginFailure();
+ }
+ }
+ }
+ });
+ };
+
+ /// <summary>Performs discovery and immediately begins checkid_immediate on all discovered endpoints.</summary>
+ this.loginBackground = function(frameManager, onLoginSuccess, onLoginFailure, timeout, onLoginLastFailure) {
+ this.discover(function(discoveryResult) {
+ if (discoveryResult) {
+ trace('Discovery succeeded and found ' + discoveryResult.length + ' OpenID service endpoints.');
+ if (discoveryResult.length > 0) {
+ discoveryResult.loginBackground(frameManager, onLoginSuccess, onLoginFailure, onLoginLastFailure || onLoginFailure, timeout);
+ } else {
+ trace("This doesn't look like an OpenID Identifier. Aborting login.");
+ if (onLoginFailure) {
+ onLoginFailure();
+ }
+ }
+ }
+ });
+ };
+
+ this.toString = function() {
+ return identifier;
+ };
+};
+
+/// <summary>Invoked by RP web server when an authentication has completed.</summary>
+/// <remarks>The duty of this method is to distribute the notification to the appropriate tracking object.</remarks>
+window.dnoa_internal.processAuthorizationResult = function(resultUrl, extensionResponses) {
+ //trace('processAuthorizationResult ' + resultUrl);
+ var resultUri = new window.dnoa_internal.Uri(resultUrl);
+ trace('processing auth result with extensionResponses: ' + extensionResponses);
+ if (extensionResponses) {
+ extensionResponses = eval(extensionResponses);
+ }
+
+ // Find the tracking object responsible for this request.
+ var userSuppliedIdentifier = resultUri.getQueryArgValue('dnoa.userSuppliedIdentifier');
+ if (!userSuppliedIdentifier) {
+ throw 'processAuthorizationResult called but no userSuppliedIdentifier parameter was found. Exiting function.';
+ }
+ var discoveryResult = window.dnoa_internal.discoveryResults[userSuppliedIdentifier];
+ if (!discoveryResult) {
+ throw 'processAuthorizationResult called but no discovery result matching user supplied identifier ' + userSuppliedIdentifier + ' was found. Exiting function.';
+ }
+
+ var opEndpoint = resultUri.getQueryArgValue("openid.op_endpoint") ? resultUri.getQueryArgValue("openid.op_endpoint") : resultUri.getQueryArgValue("dnoa.op_endpoint");
+ var respondingEndpoint = discoveryResult.findByEndpoint(opEndpoint);
+ trace('Auth result for ' + respondingEndpoint.host + ' received.'); //: ' + resultUrl);
+
+ if (window.dnoa_internal.isAuthSuccessful(resultUri)) {
+ discoveryResult.successAuthData = resultUrl;
+ respondingEndpoint.onAuthSuccess(resultUri, extensionResponses);
+
+ var parsedPositiveAssertion = new window.dnoa_internal.PositiveAssertion(resultUri);
+ if (parsedPositiveAssertion.claimedIdentifier && parsedPositiveAssertion.claimedIdentifier != discoveryResult.claimedIdentifier) {
+ discoveryResult.claimedIdentifier = parsedPositiveAssertion.claimedIdentifier;
+ trace('Authenticated as ' + parsedPositiveAssertion.claimedIdentifier);
+ }
+ } else {
+ respondingEndpoint.onAuthFailed();
+ }
+};
+
+window.dnoa_internal.isAuthSuccessful = function(resultUri) {
+ if (window.dnoa_internal.isOpenID2Response(resultUri)) {
+ return resultUri.getQueryArgValue("openid.mode") == "id_res";
+ } else {
+ return resultUri.getQueryArgValue("openid.mode") == "id_res" && !resultUri.containsQueryArg("openid.user_setup_url");
+ }
+};
+
+window.dnoa_internal.isOpenID2Response = function(resultUri) {
+ return resultUri.containsQueryArg("openid.ns");
+};
+
+/// <summary>Instantiates an object that stores discovery results of some identifier.</summary>
+window.dnoa_internal.DiscoveryResult = function(identifier, discoveryInfo) {
+ var thisDiscoveryResult = this;
+
+ /// <summary>
+ /// Instantiates an object that describes an OpenID service endpoint and facilitates
+ /// initiating and tracking an authentication request.
+ /// </summary>
+ function ServiceEndpoint(requestInfo, userSuppliedIdentifier) {
+ this.immediate = requestInfo.immediate ? new window.dnoa_internal.Uri(requestInfo.immediate) : null;
+ this.setup = requestInfo.setup ? new window.dnoa_internal.Uri(requestInfo.setup) : null;
+ this.endpoint = new window.dnoa_internal.Uri(requestInfo.endpoint);
+ this.host = this.endpoint.getHost();
+ this.userSuppliedIdentifier = userSuppliedIdentifier;
+ var thisServiceEndpoint = this; // closure so that delegates have the right instance
+ this.loginPopup = function(onAuthSuccess, onAuthFailed) {
+ thisServiceEndpoint.abort(); // ensure no concurrent attempts
+ window.dnoa_internal.fireAuthStarted(thisDiscoveryResult, thisServiceEndpoint, { background: false });
+ thisDiscoveryResult.onAuthSuccess = onAuthSuccess;
+ thisDiscoveryResult.onAuthFailed = onAuthFailed;
+ var chromeHeight = 55; // estimated height of browser title bar and location bar
+ var bottomMargin = 45; // estimated bottom space on screen likely to include a task bar
+ var width = 1000;
+ var height = 600;
+ if (thisServiceEndpoint.setup.getQueryArgValue("openid.return_to").indexOf("dnoa.popupUISupported") >= 0) {
+ trace('This OP supports the UI extension. Using smaller window size.');
+ width = 500; // spec calls for 450px, but Yahoo needs 500px
+ height = 500;
+ } else {
+ trace("This OP doesn't appear to support the UI extension. Using larger window size.");
+ }
+
+ var left = (screen.width - width) / 2;
+ var top = (screen.height - bottomMargin - height - chromeHeight) / 2;
+ thisServiceEndpoint.popup = window.open(thisServiceEndpoint.setup, 'opLogin', 'status=0,toolbar=0,location=1,resizable=1,scrollbars=1,left=' + left + ',top=' + top + ',width=' + width + ',height=' + height);
+
+ // If the OP supports the UI extension it MAY close its own window
+ // for a negative assertion. We must be able to recover from that scenario.
+ var thisServiceEndpointLocal = thisServiceEndpoint;
+ thisServiceEndpoint.popupCloseChecker = window.setInterval(function() {
+ if (thisServiceEndpointLocal.popup) {
+ try {
+ if (thisServiceEndpointLocal.popup.closed) {
+ // The window closed, either because the user closed it, canceled at the OP,
+ // or approved at the OP and the popup window closed itself due to our script.
+ // If we were graying out the entire page while the child window was up,
+ // we would probably revert that here.
+ window.clearInterval(thisServiceEndpointLocal.popupCloseChecker);
+ thisServiceEndpointLocal.popup = null;
+
+ // The popup may have managed to inform us of the result already,
+ // so check whether the callback method was cleared already, which
+ // would indicate we've already processed this.
+ if (window.dnoa_internal.processAuthorizationResult) {
+ trace('User or OP canceled by closing the window.');
+ window.dnoa_internal.fireAuthFailed(thisDiscoveryResult, thisServiceEndpoint, { background: false });
+ if (thisDiscoveryResult.onAuthFailed) {
+ thisDiscoveryResult.onAuthFailed(thisDiscoveryResult, thisServiceEndpoint);
+ }
+ }
+ }
+ } catch (e) {
+ // This usually happens because the popup is currently displaying the OP's
+ // page from another domain, which makes the popup temporarily off limits to us.
+ // Just skip this interval and wait for the next callback.
+ }
+ } else {
+ // if there's no popup, there's no reason to keep this timer up.
+ window.clearInterval(thisServiceEndpointLocal.popupCloseChecker);
+ }
+ }, 250);
+ };
+
+ this.loginBackgroundJob = function(iframe, timeout) {
+ thisServiceEndpoint.abort(); // ensure no concurrent attempts
+ if (timeout) {
+ thisServiceEndpoint.timeout = setTimeout(function() { thisServiceEndpoint.onAuthenticationTimedOut(); }, timeout);
+ }
+ window.dnoa_internal.fireAuthStarted(thisDiscoveryResult, thisServiceEndpoint, { background: true });
+ trace('iframe hosting ' + thisServiceEndpoint.endpoint + ' now OPENING (timeout ' + timeout + ').');
+ //trace('initiating auth attempt with: ' + thisServiceEndpoint.immediate);
+ thisServiceEndpoint.iframe = iframe;
+ return {
+ url: thisServiceEndpoint.immediate.toString(),
+ onCanceled: function() {
+ thisServiceEndpoint.abort();
+ window.dnoa_internal.fireAuthFailed(thisDiscoveryResult, thisServiceEndpoint, { background: true });
+ }
+ };
+ };
+
+ this.busy = function() {
+ return thisServiceEndpoint.iframe || thisServiceEndpoint.popup;
+ };
+
+ this.completeAttempt = function(successful) {
+ if (!thisServiceEndpoint.busy()) { return false; }
+ window.clearInterval(thisServiceEndpoint.timeout);
+ var background = thisServiceEndpoint.iframe !== null;
+ if (thisServiceEndpoint.iframe) {
+ trace('iframe hosting ' + thisServiceEndpoint.endpoint + ' now CLOSING.');
+ thisDiscoveryResult.frameManager.closeFrame(thisServiceEndpoint.iframe);
+ thisServiceEndpoint.iframe = null;
+ }
+ if (thisServiceEndpoint.popup) {
+ thisServiceEndpoint.popup.close();
+ thisServiceEndpoint.popup = null;
+ }
+ if (thisServiceEndpoint.timeout) {
+ window.clearTimeout(thisServiceEndpoint.timeout);
+ thisServiceEndpoint.timeout = null;
+ }
+
+ if (!successful && !thisDiscoveryResult.busy() && !thisDiscoveryResult.findSuccessfulRequest()) {
+ // fire the failed event with NO service endpoint indicating the entire auth attempt has failed.
+ window.dnoa_internal.fireAuthFailed(thisDiscoveryResult, null, { background: background });
+ if (thisDiscoveryResult.onLastAttemptFailed) {
+ thisDiscoveryResult.onLastAttemptFailed(thisDiscoveryResult);
+ }
+ }
+
+ return true;
+ };
+
+ this.onAuthenticationTimedOut = function() {
+ var background = thisServiceEndpoint.iframe !== null;
+ if (thisServiceEndpoint.completeAttempt()) {
+ trace(thisServiceEndpoint.host + " timed out");
+ thisServiceEndpoint.result = window.dnoa_internal.timedOut;
+ }
+ window.dnoa_internal.fireAuthFailed(thisDiscoveryResult, thisServiceEndpoint, { background: background });
+ };
+
+ this.onAuthSuccess = function(authUri, extensionResponses) {
+ var background = thisServiceEndpoint.iframe !== null;
+ if (thisServiceEndpoint.completeAttempt(true)) {
+ trace(thisServiceEndpoint.host + " authenticated!");
+ thisServiceEndpoint.result = window.dnoa_internal.authSuccess;
+ thisServiceEndpoint.successReceived = new Date();
+ thisServiceEndpoint.claimedIdentifier = authUri.getQueryArgValue('openid.claimed_id');
+ thisServiceEndpoint.response = authUri;
+ thisServiceEndpoint.extensionResponses = extensionResponses;
+ thisDiscoveryResult.abortAll();
+ if (thisDiscoveryResult.onAuthSuccess) {
+ thisDiscoveryResult.onAuthSuccess(thisDiscoveryResult, thisServiceEndpoint, extensionResponses);
+ }
+ window.dnoa_internal.fireAuthSuccess(thisDiscoveryResult, thisServiceEndpoint, extensionResponses, { background: background });
+ }
+ };
+
+ this.onAuthFailed = function() {
+ var background = thisServiceEndpoint.iframe !== null;
+ if (thisServiceEndpoint.completeAttempt()) {
+ trace(thisServiceEndpoint.host + " failed authentication");
+ thisServiceEndpoint.result = window.dnoa_internal.authRefused;
+ window.dnoa_internal.fireAuthFailed(thisDiscoveryResult, thisServiceEndpoint, { background: background });
+ if (thisDiscoveryResult.onAuthFailed) {
+ thisDiscoveryResult.onAuthFailed(thisDiscoveryResult, thisServiceEndpoint);
+ }
+ }
+ };
+
+ this.abort = function() {
+ if (thisServiceEndpoint.completeAttempt()) {
+ trace(thisServiceEndpoint.host + " aborted");
+ // leave the result as whatever it was before.
+ }
+ };
+
+ this.clear = function() {
+ thisServiceEndpoint.result = null;
+ thisServiceEndpoint.extensionResponses = null;
+ thisServiceEndpoint.successReceived = null;
+ thisServiceEndpoint.claimedIdentifier = null;
+ thisServiceEndpoint.response = null;
+ if (this.onCleared) {
+ this.onCleared(thisServiceEndpoint, thisDiscoveryResult);
+ }
+ if (thisDiscoveryResult.onCleared) {
+ thisDiscoveryResult.onCleared(thisDiscoveryResult, thisServiceEndpoint);
+ }
+ window.dnoa_internal.fireAuthCleared(thisDiscoveryResult, thisServiceEndpoint);
+ };
+
+ this.toString = function() {
+ return "[ServiceEndpoint: " + thisServiceEndpoint.host + "]";
+ };
+ }
+
+ this.cloneWithOneServiceEndpoint = function(serviceEndpoint) {
+ var clone = window.dnoa_internal.clone(this);
+ clone.userSuppliedIdentifier = serviceEndpoint.claimedIdentifier;
+
+ // Erase all SEPs except the given one, and put it into first position.
+ clone.length = 1;
+ for (var i = 0; i < this.length; i++) {
+ if (clone[i].endpoint.toString() == serviceEndpoint.endpoint.toString()) {
+ var tmp = clone[i];
+ clone[i] = null;
+ clone[0] = tmp;
+ } else {
+ clone[i] = null;
+ }
+ }
+
+ return clone;
+ };
+
+ this.userSuppliedIdentifier = identifier;
+ this.error = discoveryInfo.error;
+
+ if (discoveryInfo) {
+ this.claimedIdentifier = discoveryInfo.claimedIdentifier; // The claimed identifier may be null if the user provided an OP Identifier.
+ this.length = discoveryInfo.requests.length;
+ for (var i = 0; i < discoveryInfo.requests.length; i++) {
+ this[i] = new ServiceEndpoint(discoveryInfo.requests[i], identifier);
+ }
+ } else {
+ this.length = 0;
+ }
+
+ if (this.length === 0) {
+ trace('Discovery completed, but yielded no service endpoints.');
+ } else {
+ trace('Discovered claimed identifier: ' + (this.claimedIdentifier ? this.claimedIdentifier : "(directed identity)"));
+ }
+
+ // Add extra tracking bits and behaviors.
+ this.findByEndpoint = function(opEndpoint) {
+ for (var i = 0; i < thisDiscoveryResult.length; i++) {
+ if (thisDiscoveryResult[i].endpoint == opEndpoint) {
+ return thisDiscoveryResult[i];
+ }
+ }
+ };
+
+ this.busy = function() {
+ for (var i = 0; i < thisDiscoveryResult.length; i++) {
+ if (thisDiscoveryResult[i].busy()) {
+ return true;
+ }
+ }
+ };
+
+ // Add extra tracking bits and behaviors.
+ this.findSuccessfulRequest = function() {
+ for (var i = 0; i < thisDiscoveryResult.length; i++) {
+ if (thisDiscoveryResult[i].result === window.dnoa_internal.authSuccess) {
+ return thisDiscoveryResult[i];
+ }
+ }
+ };
+
+ this.abortAll = function() {
+ if (thisDiscoveryResult.frameManager) {
+ // Abort all other asynchronous authentication attempts that may be in progress
+ // for this particular claimed identifier.
+ thisDiscoveryResult.frameManager.cancelAllWork();
+ for (var i = 0; i < thisDiscoveryResult.length; i++) {
+ thisDiscoveryResult[i].abort();
+ }
+ } else {
+ trace('abortAll called without a frameManager being previously set.');
+ }
+ };
+
+ /// <summary>Initiates an asynchronous checkid_immediate login attempt against all possible service endpoints for an Identifier.</summary>
+ /// <param name="frameManager">The work queue for authentication iframes.</param>
+ /// <param name="onAuthSuccess">Fired when an endpoint responds affirmatively.</param>
+ /// <param name="onAuthFailed">Fired when an endpoint responds negatively.</param>
+ /// <param name="onLastAuthFailed">Fired when all authentication attempts have responded negatively or timed out.</param>
+ /// <param name="timeout">Timeout for an individual service endpoint to respond before the iframe closes.</param>
+ this.loginBackground = function(frameManager, onAuthSuccess, onAuthFailed, onLastAuthFailed, timeout) {
+ if (!frameManager) {
+ throw "No frameManager specified.";
+ }
+ var priorSuccessRespondingEndpoint = thisDiscoveryResult.findSuccessfulRequest();
+ if (priorSuccessRespondingEndpoint) {
+ // In this particular case, we do not fire an AuthStarted event.
+ window.dnoa_internal.fireAuthSuccess(thisDiscoveryResult, priorSuccessRespondingEndpoint, priorSuccessRespondingEndpoint.extensionResponses, { background: true });
+ if (onAuthSuccess) {
+ onAuthSuccess(thisDiscoveryResult, priorSuccessRespondingEndpoint);
+ }
+ } else {
+ if (thisDiscoveryResult.busy()) {
+ trace('Warning: DiscoveryResult.loginBackground invoked while a login attempt is already in progress. Discarding second login request.', 'red');
+ return;
+ }
+ thisDiscoveryResult.frameManager = frameManager;
+ thisDiscoveryResult.onAuthSuccess = onAuthSuccess;
+ thisDiscoveryResult.onAuthFailed = onAuthFailed;
+ thisDiscoveryResult.onLastAttemptFailed = onLastAuthFailed;
+ // Notify listeners that general authentication is beginning. Individual ServiceEndpoints
+ // will fire their own events as each of them begin their iframe 'job'.
+ window.dnoa_internal.fireAuthStarted(thisDiscoveryResult, null, { background: true });
+ if (thisDiscoveryResult.length > 0) {
+ for (var i = 0; i < thisDiscoveryResult.length; i++) {
+ thisDiscoveryResult.frameManager.enqueueWork(thisDiscoveryResult[i].loginBackgroundJob, timeout);
+ }
+ }
+ }
+ };
+
+ this.toString = function() {
+ return "[DiscoveryResult: " + thisDiscoveryResult.userSuppliedIdentifier + "]";
+ };
+};
+
+/// <summary>
+/// Called in a page had an AJAX control that had already obtained a positive assertion
+/// when a postback occurred, and now that control wants to restore its 'authenticated' state.
+/// </summary>
+/// <param name="positiveAssertion">The string form of the URI that contains the positive assertion.</param>
+window.dnoa_internal.deserializePreviousAuthentication = function(positiveAssertion) {
+ if (!positiveAssertion || positiveAssertion.length === 0) {
+ return;
+ }
+
+ trace('Revitalizing an old positive assertion from a prior postback.');
+
+ // The control ensures that we ALWAYS have an OpenID 2.0-style claimed_id attribute, even against
+ // 1.0 Providers via the return_to URL mechanism.
+ var parsedPositiveAssertion = new window.dnoa_internal.PositiveAssertion(positiveAssertion);
+
+ // We weren't given a full discovery history, but we can spoof this much from the
+ // authentication assertion.
+ trace('Deserialized claimed_id: ' + parsedPositiveAssertion.claimedIdentifier + ' and endpoint: ' + parsedPositiveAssertion.endpoint);
+ var discoveryInfo = {
+ claimedIdentifier: parsedPositiveAssertion.claimedIdentifier,
+ requests: [{ endpoint: parsedPositiveAssertion.endpoint}]
+ };
+
+ discoveryResult = new window.dnoa_internal.DiscoveryResult(parsedPositiveAssertion.userSuppliedIdentifier, discoveryInfo);
+ window.dnoa_internal.discoveryResults[parsedPositiveAssertion.userSuppliedIdentifier] = discoveryResult;
+ discoveryResult[0].result = window.dnoa_internal.authSuccess;
+ discoveryResult.successAuthData = positiveAssertion;
+
+ // restore old state from before postback
+ window.dnoa_internal.fireAuthSuccess(discoveryResult, discoveryResult[0], null, { background: true, deserialized: true });
+};
+
+window.dnoa_internal.PositiveAssertion = function(uri) {
+ uri = new window.dnoa_internal.Uri(uri.toString());
+ this.endpoint = new window.dnoa_internal.Uri(uri.getQueryArgValue("dnoa.op_endpoint"));
+ this.userSuppliedIdentifier = uri.getQueryArgValue('dnoa.userSuppliedIdentifier');
+ this.claimedIdentifier = uri.getQueryArgValue('openid.claimed_id');
+ if (!this.claimedIdentifier) {
+ this.claimedIdentifier = uri.getQueryArgValue('dnoa.claimed_id');
+ }
+ this.toString = function() { return uri.toString(); };
+};
+
+window.dnoa_internal.clone = function(obj) {
+ if (obj === null || typeof (obj) != 'object' || !isNaN(obj)) { // !isNaN catches Date objects
+ return obj;
+ }
+
+ var temp = {};
+ for (var key in obj) {
+ temp[key] = window.dnoa_internal.clone(obj[key]);
+ }
+
+ // Copy over some built-in methods that were not included in the above loop,
+ // but nevertheless may have been overridden.
+ temp.toString = window.dnoa_internal.clone(obj.toString);
+
+ return temp;
+};
+
+// Deserialized the preloaded discovery results
+window.dnoa_internal.loadPreloadedDiscoveryResults = function(preloadedDiscoveryResults) {
+ trace('found ' + preloadedDiscoveryResults.length + ' preloaded discovery results.');
+ for (var i = 0; i < preloadedDiscoveryResults.length; i++) {
+ var result = preloadedDiscoveryResults[i];
+ if (!window.dnoa_internal.discoveryResults[result.userSuppliedIdentifier]) {
+ window.dnoa_internal.discoveryResults[result.userSuppliedIdentifier] = new window.dnoa_internal.DiscoveryResult(result.userSuppliedIdentifier, result.discoveryResult);
+ trace('Preloaded discovery on: ' + window.dnoa_internal.discoveryResults[result.userSuppliedIdentifier].userSuppliedIdentifier);
+ } else {
+ trace('Skipped preloaded discovery on: ' + window.dnoa_internal.discoveryResults[result.userSuppliedIdentifier].userSuppliedIdentifier + ' because we have a cached discovery result on it.');
+ }
+ }
+};
+
+window.dnoa_internal.clearExpiredPositiveAssertions = function() {
+ for (identifier in window.dnoa_internal.discoveryResults) {
+ var discoveryResult = window.dnoa_internal.discoveryResults[identifier];
+ if (typeof (discoveryResult) != 'object') { continue; } // skip functions
+ for (var i = 0; i < discoveryResult.length; i++) {
+ if (discoveryResult[i] && discoveryResult[i].result === window.dnoa_internal.authSuccess) {
+ if (new Date() - discoveryResult[i].successReceived > window.dnoa_internal.maxPositiveAssertionLifetime) {
+ // This positive assertion is too old, and may eventually be rejected by DNOA during verification.
+ // Let's clear out the positive assertion so it can be renewed.
+ trace('Clearing out expired positive assertion from ' + discoveryResult[i].host);
+ discoveryResult[i].clear();
+ }
+ }
+ }
+ }
+};
+
+window.setInterval(window.dnoa_internal.clearExpiredPositiveAssertions, 1000);
diff --git a/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/OpenIdRelyingPartyControlBase.cs b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/OpenIdRelyingPartyControlBase.cs
new file mode 100644
index 0000000..16ea839
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/OpenIdRelyingPartyControlBase.cs
@@ -0,0 +1,1054 @@
+//-----------------------------------------------------------------------
+// <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.Collections.ObjectModel;
+ 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;
+ using DotNetOpenAuth.OpenId.Messages;
+
+ /// <summary>
+ /// Methods of indicating to the rest of the web site that the user has logged in.
+ /// </summary>
+ [SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly", MessageId = "OnSite", Justification = "Two words intended.")]
+ public enum LogOnSiteNotification {
+ /// <summary>
+ /// The rest of the web site is unaware that the user just completed an OpenID login.
+ /// </summary>
+ None,
+
+ /// <summary>
+ /// After the <see cref="OpenIdRelyingPartyControlBase.LoggedIn"/> event is fired
+ /// the control automatically calls <see cref="System.Web.Security.FormsAuthentication.RedirectFromLoginPage(string, bool)"/>
+ /// with the <see cref="IAuthenticationResponse.ClaimedIdentifier"/> as the username
+ /// unless the <see cref="OpenIdRelyingPartyControlBase.LoggedIn"/> event handler sets
+ /// <see cref="OpenIdEventArgs.Cancel"/> property to true.
+ /// </summary>
+ FormsAuthentication,
+ }
+
+ /// <summary>
+ /// How an OpenID user session should be persisted across visits.
+ /// </summary>
+ public enum LogOnPersistence {
+ /// <summary>
+ /// The user should only be logged in as long as the browser window remains open.
+ /// Nothing is persisted to help the user on a return visit. Public kiosk mode.
+ /// </summary>
+ Session,
+
+ /// <summary>
+ /// The user should only be logged in as long as the browser window remains open.
+ /// The OpenID Identifier is persisted to help expedite re-authentication when
+ /// the user visits the next time.
+ /// </summary>
+ SessionAndPersistentIdentifier,
+
+ /// <summary>
+ /// The user is issued a persistent authentication ticket so that no login is
+ /// necessary on their return visit.
+ /// </summary>
+ PersistentAuthentication,
+ }
+
+ /// <summary>
+ /// A common base class for OpenID Relying Party controls.
+ /// </summary>
+ [DefaultProperty("Identifier"), ValidationProperty("Identifier")]
+ [ParseChildren(true), PersistChildren(false)]
+ public abstract class OpenIdRelyingPartyControlBase : Control, IPostBackEventHandler, IDisposable {
+ /// <summary>
+ /// The manifest resource name of the javascript file to include on the hosting page.
+ /// </summary>
+ internal const string EmbeddedJavascriptResource = Util.DefaultNamespace + ".OpenId.RelyingParty.OpenIdRelyingPartyControlBase.js";
+
+ /// <summary>
+ /// The cookie used to persist the Identifier the user logged in with.
+ /// </summary>
+ internal const string PersistentIdentifierCookieName = OpenIdUtilities.CustomParameterPrefix + "OpenIDIdentifier";
+
+ /// <summary>
+ /// The callback parameter name to use to store which control initiated the auth request.
+ /// </summary>
+ internal const string ReturnToReceivingControlId = OpenIdUtilities.CustomParameterPrefix + "receiver";
+
+ #region Protected internal callback parameter names
+
+ /// <summary>
+ /// The callback parameter to use for recognizing when the callback is in a popup window or hidden iframe.
+ /// </summary>
+ protected internal const string UIPopupCallbackKey = OpenIdUtilities.CustomParameterPrefix + "uipopup";
+
+ /// <summary>
+ /// The parameter name to include in the formulated auth request so that javascript can know whether
+ /// the OP advertises support for the UI extension.
+ /// </summary>
+ protected internal const string PopupUISupportedJSHint = OpenIdUtilities.CustomParameterPrefix + "popupUISupported";
+
+ #endregion
+
+ #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";
+
+ /// <summary>
+ /// The "OpenID" category for properties and events.
+ /// </summary>
+ protected const string OpenIdCategory = "OpenID";
+
+ #endregion
+
+ #region Private 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 the parent window.
+ /// </summary>
+ private const string UIPopupCallbackParentKey = OpenIdUtilities.CustomParameterPrefix + "uipopupParent";
+
+ #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 LogOnPersistence UsePersistentCookieDefault = LogOnPersistence.Session;
+
+ /// <summary>
+ /// Default value of <see cref="LogOnMode"/>.
+ /// </summary>
+ private const LogOnSiteNotification LogOnModeDefault = LogOnSiteNotification.FormsAuthentication;
+
+ /// <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 storing the value of the <see cref="Extensions"/> property.
+ /// </summary>
+ private const string ExtensionsViewStateKey = "Extensions";
+
+ /// <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="LogOnMode"/> property.
+ /// </summary>
+ private const string LogOnModeViewStateKey = "LogOnMode";
+
+ /// <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
+
+ /// <summary>
+ /// The lifetime of the cookie used to persist the Identifier the user logged in with.
+ /// </summary>
+ private static readonly TimeSpan PersistentIdentifierTimeToLiveDefault = TimeSpan.FromDays(14);
+
+ /// <summary>
+ /// Backing field for the <see cref="RelyingParty"/> property.
+ /// </summary>
+ private OpenIdRelyingParty relyingParty;
+
+ /// <summary>
+ /// A value indicating whether the <see cref="relyingParty"/> field contains
+ /// an instance that we own and should Dispose.
+ /// </summary>
+ private bool relyingPartyOwned;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="OpenIdRelyingPartyControlBase"/> class.
+ /// </summary>
+ protected OpenIdRelyingPartyControlBase() {
+ Reporting.RecordFeatureUse(this);
+ }
+
+ #region Events
+
+ /// <summary>
+ /// Fired when the user has typed in their identifier, discovery was successful
+ /// and a login attempt is about to begin.
+ /// </summary>
+ [Description("Fired when the user has typed in their identifier, discovery was successful and a login attempt is about to begin."), Category(OpenIdCategory)]
+ public event EventHandler<OpenIdEventArgs> LoggingIn;
+
+ /// <summary>
+ /// Fired upon completion of a successful login.
+ /// </summary>
+ [Description("Fired upon completion of a successful login."), Category(OpenIdCategory)]
+ public event EventHandler<OpenIdEventArgs> LoggedIn;
+
+ /// <summary>
+ /// Fired when a login attempt fails.
+ /// </summary>
+ [Description("Fired when a login attempt fails."), Category(OpenIdCategory)]
+ 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."), Category(OpenIdCategory)]
+ public event EventHandler<OpenIdEventArgs> Canceled;
+
+ /// <summary>
+ /// Occurs when the <see cref="Identifier"/> property is changed.
+ /// </summary>
+ protected event EventHandler IdentifierChanged;
+
+ #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 virtual OpenIdRelyingParty RelyingParty {
+ get {
+ if (this.relyingParty == null) {
+ this.relyingParty = this.CreateRelyingParty();
+ this.ConfigureRelyingParty(this.relyingParty);
+ this.relyingPartyOwned = true;
+ }
+ return this.relyingParty;
+ }
+
+ set {
+ if (this.relyingPartyOwned && this.relyingParty != null) {
+ this.relyingParty.Dispose();
+ }
+
+ this.relyingParty = value;
+ this.relyingPartyOwned = false;
+ }
+ }
+
+ /// <summary>
+ /// Gets the collection of extension requests this selector should include in generated requests.
+ /// </summary>
+ [PersistenceMode(PersistenceMode.InnerProperty)]
+ public Collection<IOpenIdMessageExtension> Extensions {
+ get {
+ if (this.ViewState[ExtensionsViewStateKey] == null) {
+ var extensions = new Collection<IOpenIdMessageExtension>();
+ this.ViewState[ExtensionsViewStateKey] = extensions;
+ return extensions;
+ } else {
+ return (Collection<IOpenIdMessageExtension>)this.ViewState[ExtensionsViewStateKey];
+ }
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether stateless mode is used.
+ /// </summary>
+ [Bindable(true), DefaultValue(StatelessDefault), Category(OpenIdCategory)]
+ [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(OpenIdCategory)]
+ [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 {
+ Contract.Requires<ArgumentException>(!string.IsNullOrEmpty(value));
+
+ 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(OpenIdCategory)]
+ [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 LogOnPersistence UsePersistentCookie {
+ get { return (LogOnPersistence)(this.ViewState[UsePersistentCookieViewStateKey] ?? UsePersistentCookieDefault); }
+ set { this.ViewState[UsePersistentCookieViewStateKey] = value; }
+ }
+
+ /// <summary>
+ /// Gets or sets the way a completed login is communicated to the rest of the web site.
+ /// </summary>
+ [Bindable(true), DefaultValue(LogOnModeDefault), Category(BehaviorCategory)]
+ [Description("The way a completed login is communicated to the rest of the web site.")]
+ public virtual LogOnSiteNotification LogOnMode {
+ get { return (LogOnSiteNotification)(this.ViewState[LogOnModeViewStateKey] ?? LogOnModeDefault); }
+ set { this.ViewState[LogOnModeViewStateKey] = 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(OpenIdCategory)]
+ [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 Identifier that will be used to initiate login.
+ /// </summary>
+ [Bindable(true), Category(OpenIdCategory)]
+ [Description("The OpenID Identifier that this button will use to initiate login.")]
+ [TypeConverter(typeof(IdentifierConverter))]
+ public virtual Identifier Identifier {
+ get {
+ return (Identifier)ViewState[IdentifierViewStateKey];
+ }
+
+ set {
+ ViewState[IdentifierViewStateKey] = value;
+ this.OnIdentifierChanged();
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets the default association preference to set on authentication requests.
+ /// </summary>
+ internal AssociationPreference AssociationPreference { get; set; }
+
+ /// <summary>
+ /// Gets ancestor controls, starting with the immediate parent, and progressing to more distant ancestors.
+ /// </summary>
+ protected IEnumerable<Control> ParentControls {
+ get {
+ Control parent = this;
+ while ((parent = parent.Parent) != null) {
+ yield return parent;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Gets a value indicating whether this control is a child control of a composite OpenID control.
+ /// </summary>
+ /// <value>
+ /// <c>true</c> if this instance is embedded in parent OpenID control; otherwise, <c>false</c>.
+ /// </value>
+ protected bool IsEmbeddedInParentOpenIdControl {
+ get { return this.ParentControls.OfType<OpenIdRelyingPartyControlBase>().Any(); }
+ }
+
+ /// <summary>
+ /// Clears any cookie set by this control to help the user on a returning visit next time.
+ /// </summary>
+ public static void LogOff() {
+ HttpContext.Current.Response.SetCookie(CreateIdentifierPersistingCookie(null));
+ }
+
+ /// <summary>
+ /// Immediately redirects to the OpenID Provider to verify the Identifier
+ /// provided in the text box.
+ /// </summary>
+ public void LogOn() {
+ IAuthenticationRequest request = this.CreateRequests().FirstOrDefault();
+ ErrorUtilities.VerifyProtocol(request != null, OpenIdStrings.OpenIdEndpointNotFound);
+ this.LogOn(request);
+ }
+
+ /// <summary>
+ /// Immediately redirects to the OpenID Provider to verify the Identifier
+ /// provided in the text box.
+ /// </summary>
+ /// <param name="request">The request.</param>
+ public void LogOn(IAuthenticationRequest request) {
+ Contract.Requires<ArgumentNullException>(request != null);
+
+ if (this.IsPopupAppropriate(request)) {
+ this.ScriptPopupWindow(request);
+ } else {
+ request.RedirectToProvider();
+ }
+ }
+
+ #region IPostBackEventHandler Members
+
+ /// <summary>
+ /// When implemented by a class, enables a server control to process an event raised when a form is posted to the server.
+ /// </summary>
+ /// <param name="eventArgument">A <see cref="T:System.String"/> that represents an optional event argument to be passed to the event handler.</param>
+ void IPostBackEventHandler.RaisePostBackEvent(string eventArgument) {
+ this.RaisePostBackEvent(eventArgument);
+ }
+
+ #endregion
+
+ /// <summary>
+ /// Enables a server control to perform final clean up before it is released from memory.
+ /// </summary>
+ [SuppressMessage("Microsoft.Usage", "CA2202:Do not dispose objects multiple times", Justification = "Unavoidable because base class does not expose a protected virtual Dispose(bool) method."), SuppressMessage("Microsoft.Design", "CA1063:ImplementIDisposableCorrectly", Justification = "Base class doesn't implement virtual Dispose(bool), so we must call its Dispose() method.")]
+ public sealed override void Dispose() {
+ this.Dispose(true);
+ base.Dispose();
+ GC.SuppressFinalize(this);
+ }
+
+ /// <summary>
+ /// Creates the authentication requests for a given user-supplied Identifier.
+ /// </summary>
+ /// <param name="identifier">The identifier to create a request for.</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>
+ protected internal virtual IEnumerable<IAuthenticationRequest> CreateRequests(Identifier identifier) {
+ Contract.Requires<ArgumentNullException>(identifier != null);
+
+ // If this control is actually a member of another OpenID RP control,
+ // delegate creation of requests to the parent control.
+ var parentOwner = this.ParentControls.OfType<OpenIdRelyingPartyControlBase>().FirstOrDefault();
+ if (parentOwner != null) {
+ return parentOwner.CreateRequests(identifier);
+ } else {
+ // Delegate to a private method to keep 'yield return' and Code Contract separate.
+ return this.CreateRequestsCore(identifier);
+ }
+ }
+
+ /// <summary>
+ /// Releases unmanaged and - optionally - managed resources
+ /// </summary>
+ /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
+ protected virtual void Dispose(bool disposing) {
+ if (disposing) {
+ if (this.relyingPartyOwned && this.relyingParty != null) {
+ this.relyingParty.Dispose();
+ this.relyingParty = null;
+ }
+ }
+ }
+
+ /// <summary>
+ /// When implemented by a class, enables a server control to process an event raised when a form is posted to the server.
+ /// </summary>
+ /// <param name="eventArgument">A <see cref="T:System.String"/> that represents an optional event argument to be passed to the event handler.</param>
+ [SuppressMessage("Microsoft.Design", "CA1030:UseEventsWhereAppropriate", Justification = "Predefined signature.")]
+ protected virtual void RaisePostBackEvent(string eventArgument) {
+ }
+
+ /// <summary>
+ /// Creates the authentication requests for the value set in the <see cref="Identifier"/> property.
+ /// </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 IEnumerable<IAuthenticationRequest> CreateRequests() {
+ Contract.Requires<InvalidOperationException>(this.Identifier != null, OpenIdStrings.NoIdentifierSet);
+ return this.CreateRequests(this.Identifier);
+ }
+
+ /// <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;
+ }
+
+ if (this.Identifier == null) {
+ this.TryPresetIdentifierWithCookie();
+ }
+
+ // Take an unreliable sneek peek to see if we're in a popup and an OpenID
+ // assertion is coming in. We shouldn't process assertions in a popup window.
+ if (this.Page.Request.QueryString[UIPopupCallbackKey] == "1" && this.Page.Request.QueryString[UIPopupCallbackParentKey] == null) {
+ // We're in a popup window. We need to close it and pass the
+ // message back to the parent window for processing.
+ this.ScriptClosingPopupOrIFrame();
+ 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, and
+ // we want to handle that, but only if there isn't a parent control that
+ // will be handling that.
+ string receiver = this.Page.Request.QueryString[ReturnToReceivingControlId] ?? this.Page.Request.Form[ReturnToReceivingControlId];
+ if (receiver == this.ClientID || (receiver == null && !this.IsEmbeddedInParentOpenIdControl)) {
+ var response = this.RelyingParty.GetResponse();
+ Logger.Controls.DebugFormat(
+ "The {0} control checked for an authentication response and found: {1}",
+ this.ID,
+ response != null ? response.Status.ToString() : "nothing");
+ this.ProcessResponse(response);
+ }
+ }
+
+ /// <summary>
+ /// Notifies the user agent via an AJAX response of a completed authentication attempt.
+ /// </summary>
+ protected virtual void ScriptClosingPopupOrIFrame() {
+ this.RelyingParty.ProcessResponseFromPopup();
+ }
+
+ /// <summary>
+ /// Called when the <see cref="Identifier"/> property is changed.
+ /// </summary>
+ protected virtual void OnIdentifierChanged() {
+ var identifierChanged = this.IdentifierChanged;
+ if (identifierChanged != null) {
+ identifierChanged(this, EventArgs.Empty);
+ }
+ }
+
+ /// <summary>
+ /// Processes the response.
+ /// </summary>
+ /// <param name="response">The response.</param>
+ protected virtual void ProcessResponse(IAuthenticationResponse response) {
+ if (response == null) {
+ return;
+ }
+ string persistentString = response.GetUntrustedCallbackArgument(UsePersistentCookieCallbackKey);
+ if (persistentString != null) {
+ this.UsePersistentCookie = (LogOnPersistence)Enum.Parse(typeof(LogOnPersistence), persistentString);
+ }
+
+ switch (response.Status) {
+ case AuthenticationStatus.Authenticated:
+ this.OnLoggedIn(response);
+ break;
+ case AuthenticationStatus.Canceled:
+ this.OnCanceled(response);
+ break;
+ case AuthenticationStatus.Failed:
+ this.OnFailed(response);
+ break;
+ case AuthenticationStatus.SetupRequired:
+ case AuthenticationStatus.ExtensionsOnly:
+ default:
+ // The NotApplicable (extension-only assertion) is NOT one that we support
+ // in this control because that scenario is primarily interesting to RPs
+ // that are asking a specific OP, and it is not user-initiated as this textbox
+ // is designed for.
+ throw new InvalidOperationException(MessagingStrings.UnexpectedMessageReceivedOfMany);
+ }
+ }
+
+ /// <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<ArgumentNullException>(response != null);
+ Contract.Requires<ArgumentException>(response.Status == AuthenticationStatus.Authenticated);
+
+ var loggedIn = this.LoggedIn;
+ OpenIdEventArgs args = new OpenIdEventArgs(response);
+ if (loggedIn != null) {
+ loggedIn(this, args);
+ }
+
+ if (!args.Cancel) {
+ if (this.UsePersistentCookie == LogOnPersistence.SessionAndPersistentIdentifier) {
+ Page.Response.SetCookie(CreateIdentifierPersistingCookie(response));
+ }
+
+ switch (this.LogOnMode) {
+ case LogOnSiteNotification.FormsAuthentication:
+ FormsAuthentication.RedirectFromLoginPage(response.ClaimedIdentifier, this.UsePersistentCookie == LogOnPersistence.PersistentAuthentication);
+ break;
+ case LogOnSiteNotification.None:
+ default:
+ break;
+ }
+ }
+ }
+
+ /// <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<ArgumentNullException>(request != null);
+
+ 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<ArgumentNullException>(response != null);
+ Contract.Requires<ArgumentException>(response.Status == AuthenticationStatus.Canceled);
+
+ 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<ArgumentNullException>(response != null);
+ Contract.Requires<ArgumentException>(response.Status == AuthenticationStatus.Failed);
+
+ 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 OpenIdRelyingParty CreateRelyingParty() {
+ IOpenIdApplicationStore store = this.Stateless ? null : OpenIdElement.Configuration.RelyingParty.ApplicationStore.CreateInstance(OpenIdRelyingParty.HttpApplicationStore);
+ return this.CreateRelyingParty(store);
+ }
+
+ /// <summary>
+ /// Creates the relying party instance used to generate authentication requests.
+ /// </summary>
+ /// <param name="store">The store to pass to the relying party constructor.</param>
+ /// <returns>The instantiated relying party.</returns>
+ protected virtual OpenIdRelyingParty CreateRelyingParty(IOpenIdApplicationStore store) {
+ return new OpenIdRelyingParty(store);
+ }
+
+ /// <summary>
+ /// Configures the relying party.
+ /// </summary>
+ /// <param name="relyingParty">The relying party.</param>
+ [SuppressMessage("Microsoft.Maintainability", "CA1500:VariableNamesShouldNotMatchFieldNames", MessageId = "relyingParty", Justification = "This makes it possible for overrides to see the value before it is set on a field.")]
+ protected virtual void ConfigureRelyingParty(OpenIdRelyingParty relyingParty) {
+ Contract.Requires<ArgumentNullException>(relyingParty != null);
+
+ // Only set RequireSsl to true, as we don't want to override
+ // a .config setting of true with false.
+ if (this.RequireSsl) {
+ relyingParty.SecuritySettings.RequireSsl = true;
+ }
+ }
+
+ /// <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<ArgumentNullException>(request != null);
+
+ switch (this.Popup) {
+ case PopupBehavior.Never:
+ return false;
+ case PopupBehavior.Always:
+ return true;
+ case PopupBehavior.IfProviderSupported:
+ return request.DiscoveryResult.IsExtensionSupported<UIRequest>();
+ default:
+ throw ErrorUtilities.ThrowInternal("Unexpected value for Popup property.");
+ }
+ }
+
+ /// <summary>
+ /// Adds attributes to an HTML &lt;A&gt; tag that will be written by the caller using
+ /// <see cref="HtmlTextWriter.RenderBeginTag(HtmlTextWriterTag)"/> after this method.
+ /// </summary>
+ /// <param name="writer">The HTML writer.</param>
+ /// <param name="request">The outgoing authentication request.</param>
+ /// <param name="windowStatus">The text to try to display in the status bar on mouse hover.</param>
+ protected void RenderOpenIdMessageTransmissionAsAnchorAttributes(HtmlTextWriter writer, IAuthenticationRequest request, string windowStatus) {
+ Contract.Requires<ArgumentNullException>(writer != null);
+ Contract.Requires<ArgumentNullException>(request != null);
+
+ // 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>
+ /// Creates the identifier-persisting cookie, either for saving or deleting.
+ /// </summary>
+ /// <param name="response">The positive authentication response; or <c>null</c> to clear the cookie.</param>
+ /// <returns>An persistent cookie.</returns>
+ private static HttpCookie CreateIdentifierPersistingCookie(IAuthenticationResponse response) {
+ HttpCookie cookie = new HttpCookie(PersistentIdentifierCookieName);
+ bool clearingCookie = false;
+
+ // We'll try to store whatever it was the user originally typed in, but fallback
+ // to the final claimed_id.
+ if (response != null && response.Status == AuthenticationStatus.Authenticated) {
+ var positiveResponse = (PositiveAuthenticationResponse)response;
+
+ // We must escape the value because XRIs start with =, and any leading '=' gets dropped (by ASP.NET?)
+ cookie.Value = Uri.EscapeDataString(positiveResponse.Endpoint.UserSuppliedIdentifier ?? response.ClaimedIdentifier);
+ } else {
+ clearingCookie = true;
+ cookie.Value = string.Empty;
+ if (HttpContext.Current.Request.Browser["supportsEmptyStringInCookieValue"] == "false") {
+ cookie.Value = "NoCookie";
+ }
+ }
+
+ if (clearingCookie) {
+ // mark the cookie has having already expired to cause the user agent to delete
+ // the old persisted cookie.
+ cookie.Expires = DateTime.Now.Subtract(TimeSpan.FromDays(1));
+ } else {
+ // Make the cookie persistent by setting an expiration date
+ cookie.Expires = DateTime.Now + PersistentIdentifierTimeToLiveDefault;
+ }
+
+ return cookie;
+ }
+
+ /// <summary>
+ /// Creates the authentication requests for a given user-supplied Identifier.
+ /// </summary>
+ /// <param name="identifier">The identifier to create a request for.</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(Identifier identifier) {
+ ErrorUtilities.VerifyArgumentNotNull(identifier, "identifier"); // NO CODE CONTRACTS! (yield return used here)
+ 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;
+ if (this.ReturnToUrl != null) {
+ string returnToResolvedPath = this.ResolveUrl(this.ReturnToUrl);
+ returnToApproximation = new Uri(this.RelyingParty.Channel.GetRequestFromContext().UrlBeforeRewriting, returnToResolvedPath);
+ } else {
+ returnToApproximation = 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(identifier, typedRealm);
+ } else {
+ // Since the user actually gave us a return_to value,
+ // the "approximation" is exactly what we want.
+ requests = this.RelyingParty.CreateRequests(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 ourselves in return_to that we're in a popup.
+ req.SetUntrustedCallbackArgument(UIPopupCallbackKey, "1");
+
+ if (req.DiscoveryResult.IsExtensionSupported<UIRequest>()) {
+ // Inform the OP that we'll be using a popup window consistent with the UI extension.
+ // But beware that the extension MAY have already been added if we're using
+ // the OpenIdAjaxRelyingParty class.
+ if (!((AuthenticationRequest)req).Extensions.OfType<UIRequest>().Any()) {
+ req.AddExtension(new UIRequest());
+ }
+
+ // Provide a hint for the client javascript about whether the OP supports the UI extension.
+ // This is so the window can be made the correct size for the extension.
+ // If the OP doesn't advertise support for the extension, the javascript will use
+ // a bigger popup window.
+ req.SetUntrustedCallbackArgument(PopupUISupportedJSHint, "1");
+ }
+ }
+
+ // Add the extensions injected into the control.
+ foreach (var extension in this.Extensions) {
+ req.AddExtension(extension);
+ }
+
+ // Add state that needs to survive across the redirect, but at this point
+ // only save those properties that are not expected to be changed by a
+ // LoggingIn event handler.
+ req.SetUntrustedCallbackArgument(ReturnToReceivingControlId, this.ClientID);
+
+ // Apply the control's association preference to this auth request, but only if
+ // it is less demanding (greater ordinal value) than the existing one.
+ // That way, we protect against retrying an association that was already attempted.
+ var authReq = ((AuthenticationRequest)req);
+ if (authReq.AssociationPreference < this.AssociationPreference) {
+ authReq.AssociationPreference = this.AssociationPreference;
+ }
+
+ if (this.OnLoggingIn(req)) {
+ // We save this property after firing OnLoggingIn so that the host page can
+ // change its value and have that value saved.
+ req.SetUntrustedCallbackArgument(UsePersistentCookieCallbackKey, this.UsePersistentCookie.ToString());
+
+ yield return req;
+ }
+ }
+ }
+
+ /// <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<ArgumentNullException>(request != null);
+
+ 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<ArgumentNullException>(request != null);
+ Contract.Requires<InvalidOperationException>(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 = {};");
+ 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>
+ /// Tries to preset the <see cref="Identifier"/> property based on a persistent
+ /// cookie on the browser.
+ /// </summary>
+ /// <returns>
+ /// A value indicating whether the <see cref="Identifier"/> property was
+ /// successfully preset to some non-empty value.
+ /// </returns>
+ private bool TryPresetIdentifierWithCookie() {
+ HttpCookie cookie = this.Page.Request.Cookies[PersistentIdentifierCookieName];
+ if (cookie != null) {
+ this.Identifier = Uri.UnescapeDataString(cookie.Value);
+ return true;
+ }
+
+ return false;
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/OpenIdRelyingPartyControlBase.js b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/OpenIdRelyingPartyControlBase.js
new file mode 100644
index 0000000..58b283d
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/OpenIdRelyingPartyControlBase.js
@@ -0,0 +1,172 @@
+//-----------------------------------------------------------------------
+// <copyright file="OpenIdRelyingPartyControlBase.js" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// This file may be used and redistributed under the terms of the
+// Microsoft Public License (Ms-PL) http://opensource.org/licenses/ms-pl.html
+// </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, color) {
+ 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");
+ if (color) { el.style.color = color; }
+ el.appendChild(document.createTextNode(msg));
+ window.openid_tracediv.appendChild(el);
+ //alert(msg);
+ }
+};
+
+if (window.dnoa_internal === undefined) {
+ window.dnoa_internal = {};
+}
+
+/// <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 = [];
+
+ var queryBeginsAt = this.originalUri.indexOf('?');
+ if (queryBeginsAt >= 0) {
+ this.queryString = this.originalUri.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/OpenId/RelyingParty/OpenIdSelector.cs b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/OpenIdSelector.cs
new file mode 100644
index 0000000..ae1037b
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/OpenIdSelector.cs
@@ -0,0 +1,455 @@
+//-----------------------------------------------------------------------
+// <copyright file="OpenIdSelector.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+[assembly: System.Web.UI.WebResource(DotNetOpenAuth.OpenId.RelyingParty.OpenIdSelector.EmbeddedScriptResourceName, "text/javascript")]
+[assembly: System.Web.UI.WebResource(DotNetOpenAuth.OpenId.RelyingParty.OpenIdSelector.EmbeddedStylesheetResourceName, "text/css")]
+
+namespace DotNetOpenAuth.OpenId.RelyingParty {
+ using System;
+ using System.Collections.Generic;
+ using System.Collections.ObjectModel;
+ using System.ComponentModel;
+ using System.Diagnostics.Contracts;
+ using System.Globalization;
+ using System.IdentityModel.Claims;
+ using System.Linq;
+ using System.Text;
+ using System.Web;
+ using System.Web.UI;
+ using System.Web.UI.HtmlControls;
+ using System.Web.UI.WebControls;
+ using DotNetOpenAuth.ComponentModel;
+ ////using DotNetOpenAuth.InfoCard;
+ using DotNetOpenAuth.Messaging;
+
+ /// <summary>
+ /// An ASP.NET control that provides a user-friendly way of logging into a web site using OpenID.
+ /// </summary>
+ [ToolboxData("<{0}:OpenIdSelector runat=\"server\"></{0}:OpenIdSelector>")]
+ public class OpenIdSelector : OpenIdRelyingPartyAjaxControlBase {
+ /// <summary>
+ /// The name of the manifest stream containing the OpenIdButtonPanel.js file.
+ /// </summary>
+ internal const string EmbeddedScriptResourceName = Util.DefaultNamespace + ".OpenId.RelyingParty.OpenIdSelector.js";
+
+ /// <summary>
+ /// The name of the manifest stream containing the OpenIdButtonPanel.css file.
+ /// </summary>
+ internal const string EmbeddedStylesheetResourceName = Util.DefaultNamespace + ".OpenId.RelyingParty.OpenIdSelector.css";
+
+ /// <summary>
+ /// The substring to append to the end of the id or name of this control to form the
+ /// unique name of the hidden field that will carry the positive assertion on postback.
+ /// </summary>
+ private const string AuthDataFormKeySuffix = "_openidAuthData";
+
+ #region ViewState keys
+
+ /// <summary>
+ /// The viewstate key to use for storing the value of the <see cref="Buttons"/> property.
+ /// </summary>
+ private const string ButtonsViewStateKey = "Buttons";
+
+ /// <summary>
+ /// The viewstate key to use for storing the value of the <see cref="AuthenticatedAsToolTip"/> property.
+ /// </summary>
+ private const string AuthenticatedAsToolTipViewStateKey = "AuthenticatedAsToolTip";
+
+ #endregion
+
+ #region Property defaults
+
+ /// <summary>
+ /// The default value for the <see cref="AuthenticatedAsToolTip"/> property.
+ /// </summary>
+ private const string AuthenticatedAsToolTipDefault = "We recognize you!";
+
+ #endregion
+
+ /// <summary>
+ /// The OpenIdAjaxTextBox that remains hidden until the user clicks the OpenID button.
+ /// </summary>
+ private OpenIdAjaxTextBox textBox;
+
+ /// <summary>
+ /// The hidden field that will transmit the positive assertion to the RP.
+ /// </summary>
+ private HiddenField positiveAssertionField;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="OpenIdSelector"/> class.
+ /// </summary>
+ public OpenIdSelector() {
+ }
+
+ /////// <summary>
+ /////// Occurs when an InfoCard has been submitted and decoded.
+ /////// </summary>
+ ////public event EventHandler<ReceivedTokenEventArgs> ReceivedToken;
+
+ /////// <summary>
+ /////// Occurs when [token processing error].
+ /////// </summary>
+ ////public event EventHandler<TokenProcessingErrorEventArgs> TokenProcessingError;
+
+ /// <summary>
+ /// Gets the text box where applicable.
+ /// </summary>
+ public OpenIdAjaxTextBox TextBox {
+ get {
+ this.EnsureChildControlsAreCreatedSafe();
+ return this.textBox;
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets the maximum number of OpenID Providers to simultaneously try to authenticate with.
+ /// </summary>
+ [Browsable(true), DefaultValue(OpenIdAjaxTextBox.ThrottleDefault), Category(BehaviorCategory)]
+ [Description("The maximum number of OpenID Providers to simultaneously try to authenticate with.")]
+ public int Throttle {
+ get {
+ this.EnsureChildControlsAreCreatedSafe();
+ return this.textBox.Throttle;
+ }
+
+ set {
+ this.EnsureChildControlsAreCreatedSafe();
+ this.textBox.Throttle = value;
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets the time duration for the AJAX control to wait for an OP to respond before reporting failure to the user.
+ /// </summary>
+ [Browsable(true), DefaultValue(typeof(TimeSpan), "00:00:08"), Category(BehaviorCategory)]
+ [Description("The time duration for the AJAX control to wait for an OP to respond before reporting failure to the user.")]
+ public TimeSpan Timeout {
+ get {
+ this.EnsureChildControlsAreCreatedSafe();
+ return this.textBox.Timeout;
+ }
+
+ set {
+ this.EnsureChildControlsAreCreatedSafe();
+ this.textBox.Timeout = value;
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets the tool tip text that appears on the green checkmark when authentication succeeds.
+ /// </summary>
+ [Bindable(true), DefaultValue(AuthenticatedAsToolTipDefault), Localizable(true), Category(AppearanceCategory)]
+ [Description("The tool tip text that appears on the green checkmark when authentication succeeds.")]
+ public string AuthenticatedAsToolTip {
+ get { return (string)(this.ViewState[AuthenticatedAsToolTipViewStateKey] ?? AuthenticatedAsToolTipDefault); }
+ set { this.ViewState[AuthenticatedAsToolTipViewStateKey] = value ?? string.Empty; }
+ }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether the Yahoo! User Interface Library (YUI)
+ /// will be downloaded in order to provide a login split button.
+ /// </summary>
+ /// <value>
+ /// <c>true</c> to use a split button; otherwise, <c>false</c> to use a standard HTML button
+ /// or a split button by downloading the YUI library yourself on the hosting web page.
+ /// </value>
+ /// <remarks>
+ /// The split button brings in about 180KB of YUI javascript dependencies.
+ /// </remarks>
+ [Bindable(true), DefaultValue(OpenIdAjaxTextBox.DownloadYahooUILibraryDefault), Category(BehaviorCategory)]
+ [Description("Whether a split button will be used for the \"log in\" when the user provides an identifier that delegates to more than one Provider.")]
+ public bool DownloadYahooUILibrary {
+ get {
+ this.EnsureChildControlsAreCreatedSafe();
+ return this.textBox.DownloadYahooUILibrary;
+ }
+
+ set {
+ this.EnsureChildControlsAreCreatedSafe();
+ this.textBox.DownloadYahooUILibrary = value;
+ }
+ }
+
+ /// <summary>
+ /// Gets the collection of buttons this selector should render to the browser.
+ /// </summary>
+ [PersistenceMode(PersistenceMode.InnerProperty)]
+ public Collection<SelectorButton> Buttons {
+ get {
+ if (this.ViewState[ButtonsViewStateKey] == null) {
+ var providers = new Collection<SelectorButton>();
+ this.ViewState[ButtonsViewStateKey] = providers;
+ return providers;
+ } else {
+ return (Collection<SelectorButton>)this.ViewState[ButtonsViewStateKey];
+ }
+ }
+ }
+
+ /// <summary>
+ /// Gets a <see cref="T:System.Web.UI.ControlCollection"/> object that represents the child controls for a specified server control in the UI hierarchy.
+ /// </summary>
+ /// <returns>
+ /// The collection of child controls for the specified server control.
+ /// </returns>
+ public override ControlCollection Controls {
+ get {
+ this.EnsureChildControls();
+ return base.Controls;
+ }
+ }
+
+ /// <summary>
+ /// Gets the name of the open id auth data form key (for the value as stored at the user agent as a FORM field).
+ /// </summary>
+ /// <value>
+ /// Usually a concatenation of the control's name and <c>"_openidAuthData"</c>.
+ /// </value>
+ protected override string OpenIdAuthDataFormKey {
+ get { return this.UniqueID + AuthDataFormKeySuffix; }
+ }
+
+ /// <summary>
+ /// Gets a value indicating whether some button in the selector will want
+ /// to display the <see cref="OpenIdAjaxTextBox"/> control.
+ /// </summary>
+ protected virtual bool OpenIdTextBoxVisible {
+ get { return this.Buttons.OfType<SelectorOpenIdButton>().Any(); }
+ }
+
+ /// <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 override void Dispose(bool disposing) {
+ if (disposing) {
+ foreach (var button in this.Buttons.OfType<IDisposable>()) {
+ button.Dispose();
+ }
+ }
+
+ base.Dispose(disposing);
+ }
+
+ /// <summary>
+ /// Called by the ASP.NET page framework to notify server controls that use composition-based implementation to create any child controls they contain in preparation for posting back or rendering.
+ /// </summary>
+ protected override void CreateChildControls() {
+ this.EnsureChildControlsAreCreatedSafe();
+
+ base.CreateChildControls();
+
+ // Now do the ID specific work.
+ this.EnsureID();
+ ErrorUtilities.VerifyInternal(!string.IsNullOrEmpty(this.UniqueID), "Control.EnsureID() failed to give us a unique ID. Try setting an ID on the OpenIdSelector control. But please also file this bug with the project owners.");
+
+ this.Controls.Add(this.textBox);
+
+ this.positiveAssertionField.ID = this.ID + AuthDataFormKeySuffix;
+ this.Controls.Add(this.positiveAssertionField);
+ }
+
+ /// <summary>
+ /// Ensures that the child controls have been built, but doesn't set control
+ /// properties that require executing <see cref="Control.EnsureID"/> in order to avoid
+ /// certain initialization order problems.
+ /// </summary>
+ /// <remarks>
+ /// We don't just call EnsureChildControls() and then set the property on
+ /// this.textBox itself because (apparently) setting this property in the ASPX
+ /// page and thus calling this EnsureID() via EnsureChildControls() this early
+ /// results in no ID.
+ /// </remarks>
+ protected virtual void EnsureChildControlsAreCreatedSafe() {
+ // If we've already created the child controls, this method is a no-op.
+ if (this.textBox != null) {
+ return;
+ }
+
+ ////var selectorButton = this.Buttons.OfType<SelectorInfoCardButton>().FirstOrDefault();
+ ////if (selectorButton != null) {
+ //// var selector = selectorButton.InfoCardSelector;
+ //// selector.ClaimsRequested.Add(new ClaimType { Name = ClaimTypes.PPID });
+ //// selector.ImageSize = InfoCardImageSize.Size60x42;
+ //// selector.ReceivedToken += this.InfoCardSelector_ReceivedToken;
+ //// selector.TokenProcessingError += this.InfoCardSelector_TokenProcessingError;
+ //// this.Controls.Add(selector);
+ ////}
+
+ this.textBox = new OpenIdAjaxTextBox();
+ this.textBox.ID = "openid_identifier";
+ this.textBox.HookFormSubmit = false;
+ this.textBox.ShowLogOnPostBackButton = true;
+
+ this.positiveAssertionField = new HiddenField();
+ }
+
+ /// <summary>
+ /// Raises the <see cref="E:System.Web.UI.Control.Init"/> event.
+ /// </summary>
+ /// <param name="e">An <see cref="T:System.EventArgs"/> object that contains the event data.</param>
+ protected override void OnInit(EventArgs e) {
+ base.OnInit(e);
+
+ // We force child control creation here so that they can get postback events.
+ EnsureChildControls();
+ }
+
+ /// <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.EnsureValidButtons();
+
+ var css = new HtmlLink();
+ try {
+ css.Href = this.Page.ClientScript.GetWebResourceUrl(this.GetType(), EmbeddedStylesheetResourceName);
+ css.Attributes["rel"] = "stylesheet";
+ css.Attributes["type"] = "text/css";
+ ErrorUtilities.VerifyHost(this.Page.Header != null, OpenIdStrings.HeadTagMustIncludeRunatServer);
+ this.Page.Header.Controls.AddAt(0, css); // insert at top so host page can override
+ } catch {
+ css.Dispose();
+ throw;
+ }
+
+ // Import the .js file where most of the code is.
+ this.Page.ClientScript.RegisterClientScriptResource(typeof(OpenIdSelector), EmbeddedScriptResourceName);
+
+ // Provide javascript with a way to post the login assertion.
+ const string PostLoginAssertionMethodName = "postLoginAssertion";
+ const string PositiveAssertionParameterName = "positiveAssertion";
+ const string ScriptFormat = @"window.{2} = function({0}) {{
+ $('#{3}')[0].setAttribute('value', {0});
+ {1};
+}};";
+ string script = string.Format(
+ CultureInfo.InvariantCulture,
+ ScriptFormat,
+ PositiveAssertionParameterName,
+ this.Page.ClientScript.GetPostBackEventReference(this, null, false),
+ PostLoginAssertionMethodName,
+ this.positiveAssertionField.ClientID);
+ this.Page.ClientScript.RegisterClientScriptBlock(this.GetType(), "Postback", script, true);
+
+ this.PreloadDiscovery(this.Buttons.OfType<SelectorProviderButton>().Select(op => op.OPIdentifier).Where(id => id != null));
+ this.textBox.Visible = this.OpenIdTextBoxVisible;
+ }
+
+ /// <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) {
+ Contract.Assume(writer != null, "Missing contract");
+ writer.AddAttribute(HtmlTextWriterAttribute.Class, "OpenIdProviders");
+ writer.RenderBeginTag(HtmlTextWriterTag.Ul);
+
+ foreach (var button in this.Buttons) {
+ button.RenderLeadingAttributes(writer);
+
+ writer.RenderBeginTag(HtmlTextWriterTag.Li);
+
+ writer.AddAttribute(HtmlTextWriterAttribute.Href, "#");
+ writer.RenderBeginTag(HtmlTextWriterTag.A);
+
+ writer.RenderBeginTag(HtmlTextWriterTag.Div);
+ writer.RenderBeginTag(HtmlTextWriterTag.Div);
+
+ button.RenderButtonContent(writer, this);
+
+ writer.RenderEndTag(); // </div>
+
+ writer.AddAttribute(HtmlTextWriterAttribute.Class, "ui-widget-overlay");
+ writer.RenderBeginTag(HtmlTextWriterTag.Div);
+ writer.RenderEndTag();
+
+ writer.RenderEndTag(); // </div>
+ writer.RenderEndTag(); // </a>
+ writer.RenderEndTag(); // </li>
+ }
+
+ writer.RenderEndTag(); // </ul>
+
+ if (this.textBox.Visible) {
+ writer.AddStyleAttribute(HtmlTextWriterStyle.Display, "none");
+ writer.AddAttribute(HtmlTextWriterAttribute.Id, "OpenIDForm");
+ writer.RenderBeginTag(HtmlTextWriterTag.Div);
+
+ this.textBox.RenderControl(writer);
+
+ writer.RenderEndTag(); // </div>
+ }
+
+ this.positiveAssertionField.RenderControl(writer);
+ }
+
+ /////// <summary>
+ /////// Fires the <see cref="ReceivedToken"/> event.
+ /////// </summary>
+ /////// <param name="e">The token, if it was decrypted.</param>
+ ////protected virtual void OnReceivedToken(ReceivedTokenEventArgs e) {
+ //// Contract.Requires(e != null);
+ //// ErrorUtilities.VerifyArgumentNotNull(e, "e");
+
+ //// var receivedInfoCard = this.ReceivedToken;
+ //// if (receivedInfoCard != null) {
+ //// receivedInfoCard(this, e);
+ //// }
+ ////}
+
+ /////// <summary>
+ /////// Raises the <see cref="E:TokenProcessingError"/> event.
+ /////// </summary>
+ /////// <param name="e">The <see cref="DotNetOpenAuth.InfoCard.TokenProcessingErrorEventArgs"/> instance containing the event data.</param>
+ ////protected virtual void OnTokenProcessingError(TokenProcessingErrorEventArgs e) {
+ //// Contract.Requires(e != null);
+ //// ErrorUtilities.VerifyArgumentNotNull(e, "e");
+
+ //// var tokenProcessingError = this.TokenProcessingError;
+ //// if (tokenProcessingError != null) {
+ //// tokenProcessingError(this, e);
+ //// }
+ ////}
+
+ /////// <summary>
+ /////// Handles the ReceivedToken event of the infoCardSelector control.
+ /////// </summary>
+ /////// <param name="sender">The source of the event.</param>
+ /////// <param name="e">The <see cref="DotNetOpenAuth.InfoCard.ReceivedTokenEventArgs"/> instance containing the event data.</param>
+ ////private void InfoCardSelector_ReceivedToken(object sender, ReceivedTokenEventArgs e) {
+ //// this.Page.Response.SetCookie(new HttpCookie("openid_identifier", "infocard") {
+ //// Path = this.Page.Request.ApplicationPath,
+ //// });
+ //// this.OnReceivedToken(e);
+ ////}
+
+ /////// <summary>
+ /////// Handles the TokenProcessingError event of the infoCardSelector control.
+ /////// </summary>
+ /////// <param name="sender">The source of the event.</param>
+ /////// <param name="e">The <see cref="DotNetOpenAuth.InfoCard.TokenProcessingErrorEventArgs"/> instance containing the event data.</param>
+ ////private void InfoCardSelector_TokenProcessingError(object sender, TokenProcessingErrorEventArgs e) {
+ //// this.OnTokenProcessingError(e);
+ ////}
+
+ /// <summary>
+ /// Ensures the <see cref="Buttons"/> collection has a valid set of buttons.
+ /// </summary>
+ private void EnsureValidButtons() {
+ foreach (var button in this.Buttons) {
+ button.EnsureValid();
+ }
+
+ // Also make sure that there are appropriate numbers of each type of button.
+ // TODO: code here
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/OpenIdSelector.css b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/OpenIdSelector.css
new file mode 100644
index 0000000..e7eafc7
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/OpenIdSelector.css
@@ -0,0 +1,109 @@
+ul.OpenIdProviders
+{
+ padding: 0;
+ margin: 0px 0px 0px 0px;
+ list-style-type: none;
+ text-align: center;
+}
+
+ul.OpenIdProviders li
+{
+ background-color: White;
+ display: inline-block;
+ border: 1px solid #DDD;
+ margin: 0px 2px 4px 2px;
+ height: 50px;
+ width: 100px;
+ text-align: center;
+ vertical-align: middle;
+}
+
+ul.OpenIdProviders li div
+{
+ margin: 0;
+ padding: 0;
+ height: 50px;
+ width: 100px;
+ text-align: center;
+ display: table;
+ position: relative;
+ overflow: hidden;
+}
+
+ul.OpenIdProviders li div div
+{
+ margin: 0;
+ padding: 0;
+ top: 50%;
+ display: table-cell;
+ vertical-align: middle;
+ position: static;
+}
+
+ul.OpenIdProviders li img
+{
+}
+
+ul.OpenIdProviders li a img
+{
+ border-width: 0;
+}
+
+ul.OpenIdProviders li img.loginSuccess
+{
+ position: absolute;
+ right: 0;
+ bottom: 0;
+ display: none;
+}
+
+ul.OpenIdProviders li.loginSuccess img.loginSuccess
+{
+ display: inline;
+}
+
+ul.OpenIdProviders li a
+{
+ display: block; /* Chrome needs this for proper position of grayed out overlay */
+ position: relative;
+}
+
+ul.OpenIdProviders li div.ui-widget-overlay
+{
+ display: none;
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ left: 0;
+ bottom: 0;
+}
+
+ul.OpenIdProviders li.grayedOut div.ui-widget-overlay
+{
+ display: block;
+}
+
+ul.OpenIdProviders li.focused
+{
+ border: solid 2px yellow;
+}
+
+ul.OpenIdProviders li.infocard
+{
+ display: none; /* default to hiding InfoCard until the user agent determines it's supported */
+ cursor: pointer;
+}
+
+#openid_identifier
+{
+ width: 298px;
+}
+
+#OpenIDForm
+{
+ text-align: center;
+}
+
+#openid_login_button
+{
+}
diff --git a/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/OpenIdSelector.js b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/OpenIdSelector.js
new file mode 100644
index 0000000..297ea23
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/OpenIdSelector.js
@@ -0,0 +1,196 @@
+//-----------------------------------------------------------------------
+// <copyright file="OpenIdSelector.js" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// This file may be used and redistributed under the terms of the
+// Microsoft Public License (Ms-PL) http://opensource.org/licenses/ms-pl.html
+// </copyright>
+//-----------------------------------------------------------------------
+
+$(function() {
+ var hint = $.cookie('openid_identifier') || '';
+
+ var ajaxbox = document.getElementsByName('openid_identifier')[0];
+ if (ajaxbox && hint != 'infocard') {
+ ajaxbox.setValue(hint);
+ }
+
+ if (document.infoCard && document.infoCard.isSupported()) {
+ $('ul.OpenIdProviders li.infocard')[0].style.display = 'inline-block';
+ }
+
+ if (hint.length > 0) {
+ var ops = $('ul.OpenIdProviders li');
+ ops.addClass('grayedOut');
+ var matchFound = false;
+ ops.each(function(i, li) {
+ if (li.id == hint || (hint == 'infocard' && $(li).hasClass('infocard'))) {
+ $(li)
+ .removeClass('grayedOut')
+ .addClass('focused');
+ matchFound = true;
+ }
+ });
+ if (!matchFound) {
+ if (ajaxbox) {
+ $('#OpenIDButton')
+ .removeClass('grayedOut')
+ .addClass('focused');
+ $('#OpenIDForm').show('slow', function() {
+ ajaxbox.focus();
+ });
+ } else {
+ // No OP button matched the last identifier, and there is no text box,
+ // so just un-gray all buttons.
+ ops.removeClass('grayedOut');
+ }
+ }
+ }
+
+ function showLoginSuccess(userSuppliedIdentifier, success) {
+ var li = document.getElementById(userSuppliedIdentifier);
+ if (li) {
+ if (success) {
+ $(li).addClass('loginSuccess');
+ } else {
+ $(li).removeClass('loginSuccess');
+ }
+ }
+ }
+
+ window.dnoa_internal.addAuthSuccess(function(discoveryResult, serviceEndpoint, extensionResponses, state) {
+ showLoginSuccess(discoveryResult.userSuppliedIdentifier, true);
+ });
+
+ window.dnoa_internal.addAuthCleared(function(discoveryResult, serviceEndpoint) {
+ showLoginSuccess(discoveryResult.userSuppliedIdentifier, false);
+
+ // If this is an OP button, renew the positive assertion.
+ var li = document.getElementById(discoveryResult.userSuppliedIdentifier);
+ if (li) {
+ li.loginBackground();
+ }
+ });
+
+ if (ajaxbox) {
+ ajaxbox.onStateChanged = function(state) {
+ if (state == "authenticated") {
+ showLoginSuccess('OpenIDButton', true);
+ } else {
+ showLoginSuccess('OpenIDButton', false); // hide checkmark
+ }
+ };
+ }
+
+ function checkidSetup(identifier, timerBased) {
+ var openid = new window.OpenIdIdentifier(identifier);
+ if (!openid) { throw 'checkidSetup called without an identifier.'; }
+ openid.login(function(discoveryResult, respondingEndpoint, extensionResponses) {
+ doLogin(discoveryResult, respondingEndpoint);
+ });
+ }
+
+ // Sends the positive assertion we've collected to the server and actually logs the user into the RP.
+ function doLogin(discoveryResult, respondingEndpoint) {
+ var retain = true; //!$('#NotMyComputer')[0].selected;
+ $.cookie('openid_identifier', retain ? discoveryResult.userSuppliedIdentifier : null, { path: window.aspnetapppath });
+ window.postLoginAssertion(respondingEndpoint.response.toString(), window.parent.location.href);
+ }
+
+ if (ajaxbox) {
+ // take over how the text box does postbacks.
+ ajaxbox.dnoi_internal.postback = doLogin;
+ }
+
+ // This FrameManager will be used for background logins for the OP buttons
+ // and the last used identifier. It is NOT the frame manager used by the
+ // OpenIdAjaxTextBox, as it has its own.
+ var backgroundTimeout = 3000;
+
+ $(document).ready(function() {
+ var ops = $('ul.OpenIdProviders li');
+ ops.each(function(i, li) {
+ if ($(li).hasClass('OPButton')) {
+ li.authenticationIFrames = new window.dnoa_internal.FrameManager(1/*throttle*/);
+ var openid = new window.OpenIdIdentifier(li.id);
+ var authFrames = li.authenticationIFrames;
+ if ($(li).hasClass('NoAsyncAuth')) {
+ li.loginBackground = function() { };
+ } else {
+ li.loginBackground = function() {
+ openid.loginBackground(authFrames, null, null, backgroundTimeout);
+ };
+ }
+ li.loginBackground();
+ }
+ });
+ });
+
+ $('ul.OpenIdProviders li').click(function() {
+ var lastFocus = $('.focused')[0];
+ if (lastFocus != $(this)[0]) {
+ $('ul.OpenIdProviders li').removeClass('focused');
+ $(this).addClass('focused');
+ }
+
+ // Make sure we're not graying out any OPs if the user clicked on a gray button.
+ var wasGrayedOut = false;
+ if ($(this).hasClass('grayedOut')) {
+ wasGrayedOut = true;
+ $('ul.OpenIdProviders li').removeClass('grayedOut');
+ }
+
+ // Be sure to hide the openid_identifier text box unless the OpenID button is selected.
+ if ($(this)[0] != $('#OpenIDButton')[0] && $('#OpenIDForm').is(':visible')) {
+ $('#OpenIDForm').hide('slow');
+ }
+
+ var relevantUserSuppliedIdentifier = null;
+ // Don't immediately login if the user clicked OpenID and he can't see the identifier box.
+ if ($(this)[0].id != 'OpenIDButton') {
+ relevantUserSuppliedIdentifier = $(this)[0].id;
+ } else if (ajaxbox && $('#OpenIDForm').is(':visible')) {
+ relevantUserSuppliedIdentifier = ajaxbox.value;
+ }
+
+ var discoveryResult = window.dnoa_internal.discoveryResults[relevantUserSuppliedIdentifier];
+ var respondingEndpoint = discoveryResult ? discoveryResult.findSuccessfulRequest() : null;
+
+ // If the user clicked on a button that has the "we're ready to log you in immediately",
+ // then log them in!
+ if (respondingEndpoint) {
+ doLogin(discoveryResult, respondingEndpoint);
+ } else if ($(this).hasClass('OPButton')) {
+ checkidSetup($(this)[0].id);
+ } else if ($(this).hasClass('infocard') && wasGrayedOut) {
+ // we need to forward the click onto the InfoCard image so it is handled, since our
+ // gray overlaying div captured the click event.
+ $('img', this)[0].click();
+ }
+ });
+ if (ajaxbox) {
+ $('#OpenIDButton').click(function() {
+ // Be careful to only try to select the text box once it is available.
+ if ($('#OpenIDForm').is(':hidden')) {
+ $('#OpenIDForm').show('slow', function() {
+ ajaxbox.focus();
+ });
+ } else {
+ ajaxbox.focus();
+ }
+ });
+
+ $(ajaxbox.form).keydown(function(e) {
+ if (e.keyCode == $.ui.keyCode.ENTER) {
+ // we do NOT want to submit the form on ENTER.
+ e.preventDefault();
+ }
+ });
+ }
+
+ // Make popup window close on escape (the dialog style is already taken care of)
+ $(document).keydown(function(e) {
+ if (e.keyCode == $.ui.keyCode.ESCAPE) {
+ window.close();
+ }
+ });
+}); \ No newline at end of file
diff --git a/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/OpenIdTextBox.cs b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/OpenIdTextBox.cs
new file mode 100644
index 0000000..335b435
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/OpenIdTextBox.cs
@@ -0,0 +1,708 @@
+//-----------------------------------------------------------------------
+// <copyright file="OpenIdTextBox.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+[assembly: System.Web.UI.WebResource(DotNetOpenAuth.OpenId.RelyingParty.OpenIdTextBox.EmbeddedLogoResourceName, "image/png")]
+
+#pragma warning disable 0809 // marking inherited, unsupported properties as obsolete to discourage their use
+
+namespace DotNetOpenAuth.OpenId.RelyingParty {
+ using System;
+ using System.Collections.Generic;
+ using System.Collections.Specialized;
+ using System.ComponentModel;
+ using System.Diagnostics;
+ using System.Diagnostics.CodeAnalysis;
+ using System.Diagnostics.Contracts;
+ using System.Drawing.Design;
+ using System.Globalization;
+ using System.Net;
+ using System.Text;
+ using System.Text.RegularExpressions;
+ using System.Web;
+ using System.Web.Security;
+ using System.Web.UI;
+ using System.Web.UI.WebControls;
+ using DotNetOpenAuth.Configuration;
+ using DotNetOpenAuth.Messaging;
+ using DotNetOpenAuth.OpenId.Extensions.SimpleRegistration;
+ using DotNetOpenAuth.OpenId.Extensions.UI;
+
+ /// <summary>
+ /// An ASP.NET control that provides a minimal text box that is OpenID-aware.
+ /// </summary>
+ /// <remarks>
+ /// This control offers greater UI flexibility than the <see cref="OpenIdLogin"/>
+ /// control, but requires more work to be done by the hosting web site to
+ /// assemble a complete login experience.
+ /// </remarks>
+ [DefaultProperty("Text"), ValidationProperty("Text")]
+ [ToolboxData("<{0}:OpenIdTextBox runat=\"server\" />")]
+ public class OpenIdTextBox : OpenIdRelyingPartyControlBase, IEditableTextControl, ITextControl, IPostBackDataHandler {
+ /// <summary>
+ /// The name of the manifest stream containing the
+ /// OpenID logo that is placed inside the text box.
+ /// </summary>
+ internal const string EmbeddedLogoResourceName = Util.DefaultNamespace + ".OpenId.RelyingParty.openid_login.png";
+
+ /// <summary>
+ /// Default value for <see cref="TabIndex"/> property.
+ /// </summary>
+ protected const short TabIndexDefault = 0;
+
+ #region Property category constants
+
+ /// <summary>
+ /// The "Simple Registration" category for properties.
+ /// </summary>
+ private const string ProfileCategory = "Simple Registration";
+
+ #endregion
+
+ #region Property viewstate keys
+
+ /// <summary>
+ /// The viewstate key to use for the <see cref="RequestEmail"/> property.
+ /// </summary>
+ private const string RequestEmailViewStateKey = "RequestEmail";
+
+ /// <summary>
+ /// The viewstate key to use for the <see cref="RequestNickname"/> property.
+ /// </summary>
+ private const string RequestNicknameViewStateKey = "RequestNickname";
+
+ /// <summary>
+ /// The viewstate key to use for the <see cref="RequestPostalCode"/> property.
+ /// </summary>
+ private const string RequestPostalCodeViewStateKey = "RequestPostalCode";
+
+ /// <summary>
+ /// The viewstate key to use for the <see cref="RequestCountry"/> property.
+ /// </summary>
+ private const string RequestCountryViewStateKey = "RequestCountry";
+
+ /// <summary>
+ /// The viewstate key to use for the <see cref="RequestLanguage"/> property.
+ /// </summary>
+ private const string RequestLanguageViewStateKey = "RequestLanguage";
+
+ /// <summary>
+ /// The viewstate key to use for the <see cref="RequestTimeZone"/> property.
+ /// </summary>
+ private const string RequestTimeZoneViewStateKey = "RequestTimeZone";
+
+ /// <summary>
+ /// The viewstate key to use for the <see cref="EnableRequestProfile"/> property.
+ /// </summary>
+ private const string EnableRequestProfileViewStateKey = "EnableRequestProfile";
+
+ /// <summary>
+ /// The viewstate key to use for the <see cref="PolicyUrl"/> property.
+ /// </summary>
+ private const string PolicyUrlViewStateKey = "PolicyUrl";
+
+ /// <summary>
+ /// The viewstate key to use for the <see cref="RequestFullName"/> property.
+ /// </summary>
+ private const string RequestFullNameViewStateKey = "RequestFullName";
+
+ /// <summary>
+ /// The viewstate key to use for the <see cref="PresetBorder"/> property.
+ /// </summary>
+ private const string PresetBorderViewStateKey = "PresetBorder";
+
+ /// <summary>
+ /// The viewstate key to use for the <see cref="ShowLogo"/> property.
+ /// </summary>
+ private const string ShowLogoViewStateKey = "ShowLogo";
+
+ /// <summary>
+ /// The viewstate key to use for the <see cref="RequestGender"/> property.
+ /// </summary>
+ private const string RequestGenderViewStateKey = "RequestGender";
+
+ /// <summary>
+ /// The viewstate key to use for the <see cref="RequestBirthDate"/> property.
+ /// </summary>
+ private const string RequestBirthDateViewStateKey = "RequestBirthDate";
+
+ /// <summary>
+ /// The viewstate key to use for the <see cref="CssClass"/> property.
+ /// </summary>
+ private const string CssClassViewStateKey = "CssClass";
+
+ /// <summary>
+ /// The viewstate key to use for the <see cref="MaxLength"/> property.
+ /// </summary>
+ private const string MaxLengthViewStateKey = "MaxLength";
+
+ /// <summary>
+ /// The viewstate key to use for the <see cref="Columns"/> property.
+ /// </summary>
+ private const string ColumnsViewStateKey = "Columns";
+
+ /// <summary>
+ /// The viewstate key to use for the <see cref="TabIndex"/> property.
+ /// </summary>
+ private const string TabIndexViewStateKey = "TabIndex";
+
+ /// <summary>
+ /// The viewstate key to use for the <see cref="Enabled"/> property.
+ /// </summary>
+ private const string EnabledViewStateKey = "Enabled";
+
+ /// <summary>
+ /// The viewstate key to use for the <see cref="Name"/> property.
+ /// </summary>
+ private const string NameViewStateKey = "Name";
+
+ /// <summary>
+ /// The viewstate key to use for the <see cref="Text"/> property.
+ /// </summary>
+ private const string TextViewStateKey = "Text";
+
+ #endregion
+
+ #region Property defaults
+
+ /// <summary>
+ /// The default value for the <see cref="Columns"/> property.
+ /// </summary>
+ private const int ColumnsDefault = 40;
+
+ /// <summary>
+ /// The default value for the <see cref="MaxLength"/> property.
+ /// </summary>
+ private const int MaxLengthDefault = 40;
+
+ /// <summary>
+ /// The default value for the <see cref="Name"/> property.
+ /// </summary>
+ private const string NameDefault = "openid_identifier";
+
+ /// <summary>
+ /// The default value for the <see cref="EnableRequestProfile"/> property.
+ /// </summary>
+ private const bool EnableRequestProfileDefault = true;
+
+ /// <summary>
+ /// The default value for the <see cref="ShowLogo"/> property.
+ /// </summary>
+ private const bool ShowLogoDefault = true;
+
+ /// <summary>
+ /// The default value for the <see cref="PresetBorder"/> property.
+ /// </summary>
+ private const bool PresetBorderDefault = true;
+
+ /// <summary>
+ /// The default value for the <see cref="PolicyUrl"/> property.
+ /// </summary>
+ private const string PolicyUrlDefault = "";
+
+ /// <summary>
+ /// The default value for the <see cref="CssClass"/> property.
+ /// </summary>
+ private const string CssClassDefault = "openid";
+
+ /// <summary>
+ /// The default value for the <see cref="Text"/> property.
+ /// </summary>
+ private const string TextDefault = "";
+
+ /// <summary>
+ /// The default value for the <see cref="RequestEmail"/> property.
+ /// </summary>
+ private const DemandLevel RequestEmailDefault = DemandLevel.NoRequest;
+
+ /// <summary>
+ /// The default value for the <see cref="RequestPostalCode"/> property.
+ /// </summary>
+ private const DemandLevel RequestPostalCodeDefault = DemandLevel.NoRequest;
+
+ /// <summary>
+ /// The default value for the <see cref="RequestCountry"/> property.
+ /// </summary>
+ private const DemandLevel RequestCountryDefault = DemandLevel.NoRequest;
+
+ /// <summary>
+ /// The default value for the <see cref="RequestLanguage"/> property.
+ /// </summary>
+ private const DemandLevel RequestLanguageDefault = DemandLevel.NoRequest;
+
+ /// <summary>
+ /// The default value for the <see cref="RequestTimeZone"/> property.
+ /// </summary>
+ private const DemandLevel RequestTimeZoneDefault = DemandLevel.NoRequest;
+
+ /// <summary>
+ /// The default value for the <see cref="RequestNickname"/> property.
+ /// </summary>
+ private const DemandLevel RequestNicknameDefault = DemandLevel.NoRequest;
+
+ /// <summary>
+ /// The default value for the <see cref="RequestFullName"/> property.
+ /// </summary>
+ private const DemandLevel RequestFullNameDefault = DemandLevel.NoRequest;
+
+ /// <summary>
+ /// The default value for the <see cref="RequestBirthDate"/> property.
+ /// </summary>
+ private const DemandLevel RequestBirthDateDefault = DemandLevel.NoRequest;
+
+ /// <summary>
+ /// The default value for the <see cref="RequestGender"/> property.
+ /// </summary>
+ private const DemandLevel RequestGenderDefault = DemandLevel.NoRequest;
+
+ #endregion
+
+ /// <summary>
+ /// An empty sreg request, used to compare with others to see if they too are empty.
+ /// </summary>
+ private static readonly ClaimsRequest EmptyClaimsRequest = new ClaimsRequest();
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="OpenIdTextBox"/> class.
+ /// </summary>
+ public OpenIdTextBox() {
+ }
+
+ #region IEditableTextControl Members
+
+ /// <summary>
+ /// Occurs when the content of the text changes between posts to the server.
+ /// </summary>
+ public event EventHandler TextChanged;
+
+ #endregion
+
+ #region Properties
+
+ /// <summary>
+ /// Gets or sets the content of the text box.
+ /// </summary>
+ [Bindable(true), DefaultValue(""), Category(AppearanceCategory)]
+ [Description("The content of the text box.")]
+ public string Text {
+ get {
+ return this.Identifier != null ? this.Identifier.OriginalString : (this.ViewState[TextViewStateKey] as string ?? string.Empty);
+ }
+
+ set {
+ // Try to store it as a validated identifier,
+ // but failing that at least store the text.
+ Identifier id;
+ if (Identifier.TryParse(value, out id)) {
+ this.Identifier = id;
+ } else {
+ // Be sure to set the viewstate AFTER setting the Identifier,
+ // since setting the Identifier clears the viewstate in OnIdentifierChanged.
+ this.Identifier = null;
+ this.ViewState[TextViewStateKey] = value;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets the form name to use for this input field.
+ /// </summary>
+ [Bindable(true), DefaultValue(NameDefault), Category(BehaviorCategory)]
+ [Description("The form name of this input field.")]
+ public string Name {
+ get { return (string)(this.ViewState[NameViewStateKey] ?? NameDefault); }
+ set { this.ViewState[NameViewStateKey] = value; }
+ }
+
+ /// <summary>
+ /// Gets or sets the CSS class assigned to the text box.
+ /// </summary>
+ [Bindable(true), DefaultValue(CssClassDefault), Category(AppearanceCategory)]
+ [Description("The CSS class assigned to the text box.")]
+ public string CssClass {
+ get { return (string)this.ViewState[CssClassViewStateKey]; }
+ set { this.ViewState[CssClassViewStateKey] = value; }
+ }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether to show the OpenID logo in the text box.
+ /// </summary>
+ [Bindable(true), DefaultValue(ShowLogoDefault), Category(AppearanceCategory)]
+ [Description("The visibility of the OpenID logo in the text box.")]
+ public bool ShowLogo {
+ get { return (bool)(this.ViewState[ShowLogoViewStateKey] ?? ShowLogoDefault); }
+ set { this.ViewState[ShowLogoViewStateKey] = value; }
+ }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether to use inline styling to force a solid gray border.
+ /// </summary>
+ [Bindable(true), DefaultValue(PresetBorderDefault), Category(AppearanceCategory)]
+ [Description("Whether to use inline styling to force a solid gray border.")]
+ public bool PresetBorder {
+ get { return (bool)(this.ViewState[PresetBorderViewStateKey] ?? PresetBorderDefault); }
+ set { this.ViewState[PresetBorderViewStateKey] = value; }
+ }
+
+ /// <summary>
+ /// Gets or sets the width of the text box in characters.
+ /// </summary>
+ [Bindable(true), DefaultValue(ColumnsDefault), Category(AppearanceCategory)]
+ [Description("The width of the text box in characters.")]
+ public int Columns {
+ get { return (int)(this.ViewState[ColumnsViewStateKey] ?? ColumnsDefault); }
+ set { this.ViewState[ColumnsViewStateKey] = value; }
+ }
+
+ /// <summary>
+ /// Gets or sets the maximum number of characters the browser should allow
+ /// </summary>
+ [Bindable(true), DefaultValue(MaxLengthDefault), Category(AppearanceCategory)]
+ [Description("The maximum number of characters the browser should allow.")]
+ public int MaxLength {
+ get { return (int)(this.ViewState[MaxLengthViewStateKey] ?? MaxLengthDefault); }
+ set { this.ViewState[MaxLengthViewStateKey] = value; }
+ }
+
+ /// <summary>
+ /// Gets or sets the tab index of the Web server control.
+ /// </summary>
+ /// <value></value>
+ /// <returns>
+ /// The tab index of the Web server control. The default is 0, which indicates that this property is not set.
+ /// </returns>
+ /// <exception cref="T:System.ArgumentOutOfRangeException">
+ /// The specified tab index is not between -32768 and 32767.
+ /// </exception>
+ [Bindable(true), DefaultValue(TabIndexDefault), Category(BehaviorCategory)]
+ [Description("The tab index of the text box control.")]
+ public virtual short TabIndex {
+ get { return (short)(this.ViewState[TabIndexViewStateKey] ?? TabIndexDefault); }
+ set { this.ViewState[TabIndexViewStateKey] = value; }
+ }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this <see cref="OpenIdTextBox"/> is enabled
+ /// in the browser for editing and will respond to incoming OpenID messages.
+ /// </summary>
+ /// <value><c>true</c> if enabled; otherwise, <c>false</c>.</value>
+ [Bindable(true), DefaultValue(true), Category(BehaviorCategory)]
+ [Description("Whether the control is editable in the browser and will respond to OpenID messages.")]
+ public bool Enabled {
+ get { return (bool)(this.ViewState[EnabledViewStateKey] ?? true); }
+ set { this.ViewState[EnabledViewStateKey] = value; }
+ }
+
+ /// <summary>
+ /// Gets or sets your level of interest in receiving the user's nickname from the Provider.
+ /// </summary>
+ [Bindable(true), DefaultValue(RequestNicknameDefault), Category(ProfileCategory)]
+ [Description("Your level of interest in receiving the user's nickname from the Provider.")]
+ public DemandLevel RequestNickname {
+ get { return (DemandLevel)(ViewState[RequestNicknameViewStateKey] ?? RequestNicknameDefault); }
+ set { ViewState[RequestNicknameViewStateKey] = value; }
+ }
+
+ /// <summary>
+ /// Gets or sets your level of interest in receiving the user's email address from the Provider.
+ /// </summary>
+ [Bindable(true), DefaultValue(RequestEmailDefault), Category(ProfileCategory)]
+ [Description("Your level of interest in receiving the user's email address from the Provider.")]
+ public DemandLevel RequestEmail {
+ get { return (DemandLevel)(ViewState[RequestEmailViewStateKey] ?? RequestEmailDefault); }
+ set { ViewState[RequestEmailViewStateKey] = value; }
+ }
+
+ /// <summary>
+ /// Gets or sets your level of interest in receiving the user's full name from the Provider.
+ /// </summary>
+ [Bindable(true), DefaultValue(RequestFullNameDefault), Category(ProfileCategory)]
+ [Description("Your level of interest in receiving the user's full name from the Provider")]
+ public DemandLevel RequestFullName {
+ get { return (DemandLevel)(ViewState[RequestFullNameViewStateKey] ?? RequestFullNameDefault); }
+ set { ViewState[RequestFullNameViewStateKey] = value; }
+ }
+
+ /// <summary>
+ /// Gets or sets your level of interest in receiving the user's birthdate from the Provider.
+ /// </summary>
+ [Bindable(true), DefaultValue(RequestBirthDateDefault), Category(ProfileCategory)]
+ [Description("Your level of interest in receiving the user's birthdate from the Provider.")]
+ public DemandLevel RequestBirthDate {
+ get { return (DemandLevel)(ViewState[RequestBirthDateViewStateKey] ?? RequestBirthDateDefault); }
+ set { ViewState[RequestBirthDateViewStateKey] = value; }
+ }
+
+ /// <summary>
+ /// Gets or sets your level of interest in receiving the user's gender from the Provider.
+ /// </summary>
+ [Bindable(true), DefaultValue(RequestGenderDefault), Category(ProfileCategory)]
+ [Description("Your level of interest in receiving the user's gender from the Provider.")]
+ public DemandLevel RequestGender {
+ get { return (DemandLevel)(ViewState[RequestGenderViewStateKey] ?? RequestGenderDefault); }
+ set { ViewState[RequestGenderViewStateKey] = value; }
+ }
+
+ /// <summary>
+ /// Gets or sets your level of interest in receiving the user's postal code from the Provider.
+ /// </summary>
+ [Bindable(true), DefaultValue(RequestPostalCodeDefault), Category(ProfileCategory)]
+ [Description("Your level of interest in receiving the user's postal code from the Provider.")]
+ public DemandLevel RequestPostalCode {
+ get { return (DemandLevel)(ViewState[RequestPostalCodeViewStateKey] ?? RequestPostalCodeDefault); }
+ set { ViewState[RequestPostalCodeViewStateKey] = value; }
+ }
+
+ /// <summary>
+ /// Gets or sets your level of interest in receiving the user's country from the Provider.
+ /// </summary>
+ [Bindable(true)]
+ [Category(ProfileCategory)]
+ [DefaultValue(RequestCountryDefault)]
+ [Description("Your level of interest in receiving the user's country from the Provider.")]
+ public DemandLevel RequestCountry {
+ get { return (DemandLevel)(ViewState[RequestCountryViewStateKey] ?? RequestCountryDefault); }
+ set { ViewState[RequestCountryViewStateKey] = value; }
+ }
+
+ /// <summary>
+ /// Gets or sets your level of interest in receiving the user's preferred language from the Provider.
+ /// </summary>
+ [Bindable(true), DefaultValue(RequestLanguageDefault), Category(ProfileCategory)]
+ [Description("Your level of interest in receiving the user's preferred language from the Provider.")]
+ public DemandLevel RequestLanguage {
+ get { return (DemandLevel)(ViewState[RequestLanguageViewStateKey] ?? RequestLanguageDefault); }
+ set { ViewState[RequestLanguageViewStateKey] = value; }
+ }
+
+ /// <summary>
+ /// Gets or sets your level of interest in receiving the user's time zone from the Provider.
+ /// </summary>
+ [Bindable(true), DefaultValue(RequestTimeZoneDefault), Category(ProfileCategory)]
+ [Description("Your level of interest in receiving the user's time zone from the Provider.")]
+ public DemandLevel RequestTimeZone {
+ get { return (DemandLevel)(ViewState[RequestTimeZoneViewStateKey] ?? RequestTimeZoneDefault); }
+ set { ViewState[RequestTimeZoneViewStateKey] = value; }
+ }
+
+ /// <summary>
+ /// Gets or sets the URL to your privacy policy page that describes how
+ /// claims will be used and/or shared.
+ /// </summary>
+ [SuppressMessage("Microsoft.Design", "CA1056:UriPropertiesShouldNotBeStrings", Justification = "Bindable property must be simple type")]
+ [Bindable(true), DefaultValue(PolicyUrlDefault), Category(ProfileCategory)]
+ [Description("The URL to your privacy policy page that describes how claims will be used and/or shared.")]
+ [UrlProperty, Editor("System.Web.UI.Design.UrlEditor, System.Design, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a", typeof(UITypeEditor))]
+ public string PolicyUrl {
+ get {
+ return (string)ViewState[PolicyUrlViewStateKey] ?? PolicyUrlDefault;
+ }
+
+ set {
+ UriUtil.ValidateResolvableUrl(Page, DesignMode, value);
+ ViewState[PolicyUrlViewStateKey] = value;
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether to use OpenID extensions
+ /// to retrieve profile data of the authenticating user.
+ /// </summary>
+ [Bindable(true), DefaultValue(EnableRequestProfileDefault), Category(ProfileCategory)]
+ [Description("Turns the entire Simple Registration extension on or off.")]
+ public bool EnableRequestProfile {
+ get { return (bool)(ViewState[EnableRequestProfileViewStateKey] ?? EnableRequestProfileDefault); }
+ set { ViewState[EnableRequestProfileViewStateKey] = value; }
+ }
+
+ #endregion
+
+ #region IPostBackDataHandler Members
+
+ /// <summary>
+ /// When implemented by a class, processes postback data for an ASP.NET server control.
+ /// </summary>
+ /// <param name="postDataKey">The key identifier for the control.</param>
+ /// <param name="postCollection">The collection of all incoming name values.</param>
+ /// <returns>
+ /// true if the server control's state changes as a result of the postback; otherwise, false.
+ /// </returns>
+ bool IPostBackDataHandler.LoadPostData(string postDataKey, NameValueCollection postCollection) {
+ return this.LoadPostData(postDataKey, postCollection);
+ }
+
+ /// <summary>
+ /// When implemented by a class, signals the server control to notify the ASP.NET application that the state of the control has changed.
+ /// </summary>
+ void IPostBackDataHandler.RaisePostDataChangedEvent() {
+ this.RaisePostDataChangedEvent();
+ }
+
+ #endregion
+
+ /// <summary>
+ /// Creates the authentication requests for a given user-supplied Identifier.
+ /// </summary>
+ /// <param name="identifier">The identifier to create a request for.</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>
+ protected internal override IEnumerable<IAuthenticationRequest> CreateRequests(Identifier identifier) {
+ ErrorUtilities.VerifyArgumentNotNull(identifier, "identifier");
+
+ // 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(identifier));
+ }
+
+ /// <summary>
+ /// Checks for incoming OpenID authentication responses and fires appropriate events.
+ /// </summary>
+ /// <param name="e">The <see cref="T:System.EventArgs"/> object that contains the event data.</param>
+ protected override void OnLoad(EventArgs e) {
+ if (!this.Enabled) {
+ return;
+ }
+
+ this.Page.RegisterRequiresPostBack(this);
+ base.OnLoad(e);
+ }
+
+ /// <summary>
+ /// Called when the <see cref="Identifier"/> property is changed.
+ /// </summary>
+ protected override void OnIdentifierChanged() {
+ this.ViewState.Remove(TextViewStateKey);
+ base.OnIdentifierChanged();
+ }
+
+ /// <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) {
+ Contract.Assume(writer != null, "Missing contract.");
+
+ if (this.ShowLogo) {
+ string logoUrl = Page.ClientScript.GetWebResourceUrl(
+ typeof(OpenIdTextBox), EmbeddedLogoResourceName);
+ writer.AddStyleAttribute(
+ HtmlTextWriterStyle.BackgroundImage,
+ string.Format(CultureInfo.InvariantCulture, "url({0})", HttpUtility.HtmlEncode(logoUrl)));
+ writer.AddStyleAttribute("background-repeat", "no-repeat");
+ writer.AddStyleAttribute("background-position", "0 50%");
+ writer.AddStyleAttribute(HtmlTextWriterStyle.PaddingLeft, "18px");
+ }
+
+ if (this.PresetBorder) {
+ writer.AddStyleAttribute(HtmlTextWriterStyle.BorderStyle, "solid");
+ writer.AddStyleAttribute(HtmlTextWriterStyle.BorderWidth, "1px");
+ writer.AddStyleAttribute(HtmlTextWriterStyle.BorderColor, "lightgray");
+ }
+
+ if (!string.IsNullOrEmpty(this.CssClass)) {
+ writer.AddAttribute(HtmlTextWriterAttribute.Class, this.CssClass);
+ }
+
+ writer.AddAttribute(HtmlTextWriterAttribute.Id, this.ClientID);
+ writer.AddAttribute(HtmlTextWriterAttribute.Name, HttpUtility.HtmlEncode(this.Name));
+ writer.AddAttribute(HtmlTextWriterAttribute.Type, "text");
+ writer.AddAttribute(HtmlTextWriterAttribute.Size, this.Columns.ToString(CultureInfo.InvariantCulture));
+ writer.AddAttribute(HtmlTextWriterAttribute.Value, HttpUtility.HtmlEncode(this.Text));
+ writer.AddAttribute(HtmlTextWriterAttribute.Tabindex, this.TabIndex.ToString(CultureInfo.CurrentCulture));
+
+ writer.RenderBeginTag(HtmlTextWriterTag.Input);
+ writer.RenderEndTag();
+ }
+
+ /// <summary>
+ /// When implemented by a class, processes postback data for an ASP.NET server control.
+ /// </summary>
+ /// <param name="postDataKey">The key identifier for the control.</param>
+ /// <param name="postCollection">The collection of all incoming name values.</param>
+ /// <returns>
+ /// true if the server control's state changes as a result of the postback; otherwise, false.
+ /// </returns>
+ protected virtual bool LoadPostData(string postDataKey, NameValueCollection postCollection) {
+ Contract.Assume(postCollection != null, "Missing contract");
+
+ // If the control was temporarily hidden, it won't be in the Form data,
+ // and we'll just implicitly keep the last Text setting.
+ if (postCollection[this.Name] != null) {
+ this.Text = postCollection[this.Name];
+ return true;
+ }
+
+ return false;
+ }
+
+ /// <summary>
+ /// When implemented by a class, signals the server control to notify the ASP.NET application that the state of the control has changed.
+ /// </summary>
+ [SuppressMessage("Microsoft.Design", "CA1030:UseEventsWhereAppropriate", Justification = "Preserve signature of interface we're implementing.")]
+ protected virtual void RaisePostDataChangedEvent() {
+ this.OnTextChanged();
+ }
+
+ /// <summary>
+ /// Called on a postback when the Text property has changed.
+ /// </summary>
+ protected virtual void OnTextChanged() {
+ EventHandler textChanged = this.TextChanged;
+ if (textChanged != null) {
+ textChanged(this, EventArgs.Empty);
+ }
+ }
+
+ /// <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);
+
+ foreach (var request in requests) {
+ if (this.EnableRequestProfile) {
+ this.AddProfileArgs(request);
+ }
+
+ yield return request;
+ }
+ }
+
+ /// <summary>
+ /// Adds extensions to a given authentication request to ask the Provider
+ /// for user profile data.
+ /// </summary>
+ /// <param name="request">The authentication request to add the extensions to.</param>
+ private void AddProfileArgs(IAuthenticationRequest request) {
+ Contract.Requires<ArgumentNullException>(request != null);
+
+ var sreg = new ClaimsRequest() {
+ Nickname = this.RequestNickname,
+ Email = this.RequestEmail,
+ FullName = this.RequestFullName,
+ BirthDate = this.RequestBirthDate,
+ Gender = this.RequestGender,
+ PostalCode = this.RequestPostalCode,
+ Country = this.RequestCountry,
+ Language = this.RequestLanguage,
+ TimeZone = this.RequestTimeZone,
+ PolicyUrl = string.IsNullOrEmpty(this.PolicyUrl) ?
+ null : new Uri(this.RelyingParty.Channel.GetRequestFromContext().UrlBeforeRewriting, this.Page.ResolveUrl(this.PolicyUrl)),
+ };
+
+ // Only actually add the extension request if fields are actually being requested.
+ if (!sreg.Equals(EmptyClaimsRequest)) {
+ request.AddExtension(sreg);
+ }
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/PopupBehavior.cs b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/PopupBehavior.cs
new file mode 100644
index 0000000..e84f4f5
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/PopupBehavior.cs
@@ -0,0 +1,31 @@
+//-----------------------------------------------------------------------
+// <copyright file="PopupBehavior.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OpenId.RelyingParty {
+ /// <summary>
+ /// Several ways that the relying party can direct the user to the Provider
+ /// to complete authentication.
+ /// </summary>
+ public enum PopupBehavior {
+ /// <summary>
+ /// A full browser window redirect will be used to send the
+ /// user to the Provider.
+ /// </summary>
+ Never,
+
+ /// <summary>
+ /// A popup window will be used to send the user to the Provider.
+ /// </summary>
+ Always,
+
+ /// <summary>
+ /// A popup window will be used to send the user to the Provider
+ /// if the Provider advertises support for the popup UI extension;
+ /// otherwise a standard redirect is used.
+ /// </summary>
+ IfProviderSupported,
+ }
+}
diff --git a/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/PositiveAnonymousResponse.cs b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/PositiveAnonymousResponse.cs
new file mode 100644
index 0000000..fc334b0
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/PositiveAnonymousResponse.cs
@@ -0,0 +1,347 @@
+//-----------------------------------------------------------------------
+// <copyright file="PositiveAnonymousResponse.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OpenId.RelyingParty {
+ using System;
+ using System.Collections.Generic;
+ using System.Diagnostics.Contracts;
+ using System.Linq;
+ using System.Web;
+ using DotNetOpenAuth.Messaging;
+ using DotNetOpenAuth.OpenId.Messages;
+
+ /// <summary>
+ /// Wraps an extension-only response from the OP in an <see cref="IAuthenticationResponse"/> instance
+ /// for public consumption by the host web site.
+ /// </summary>
+ internal class PositiveAnonymousResponse : IAuthenticationResponse {
+ /// <summary>
+ /// Backin field for the <see cref="Response"/> property.
+ /// </summary>
+ private readonly IndirectSignedResponse response;
+
+ /// <summary>
+ /// Information about the OP endpoint that issued this assertion.
+ /// </summary>
+ private readonly IProviderEndpoint provider;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="PositiveAnonymousResponse"/> class.
+ /// </summary>
+ /// <param name="response">The response message.</param>
+ protected internal PositiveAnonymousResponse(IndirectSignedResponse response) {
+ Contract.Requires<ArgumentNullException>(response != null);
+
+ this.response = response;
+ if (response.ProviderEndpoint != null && response.Version != null) {
+ this.provider = new ProviderEndpointDescription(response.ProviderEndpoint, response.Version);
+ }
+
+ // Derived types of this are responsible to log an appropriate message for themselves.
+ if (Logger.OpenId.IsInfoEnabled && this.GetType() == typeof(PositiveAnonymousResponse)) {
+ Logger.OpenId.Info("Received anonymous (identity-less) positive assertion.");
+ }
+
+ if (response.ProviderEndpoint != null) {
+ Reporting.RecordEventOccurrence(this, response.ProviderEndpoint.AbsoluteUri);
+ }
+ }
+
+ #region IAuthenticationResponse Properties
+
+ /// <summary>
+ /// Gets the Identifier that the end user claims to own. For use with user database storage and lookup.
+ /// May be null for some failed authentications (i.e. failed directed identity authentications).
+ /// </summary>
+ /// <remarks>
+ /// <para>
+ /// This is the secure identifier that should be used for database storage and lookup.
+ /// It is not always friendly (i.e. =Arnott becomes =!9B72.7DD1.50A9.5CCD), but it protects
+ /// user identities against spoofing and other attacks.
+ /// </para>
+ /// <para>
+ /// For user-friendly identifiers to display, use the
+ /// <see cref="FriendlyIdentifierForDisplay"/> property.
+ /// </para>
+ /// </remarks>
+ public virtual Identifier ClaimedIdentifier {
+ get { return null; }
+ }
+
+ /// <summary>
+ /// Gets a user-friendly OpenID Identifier for display purposes ONLY.
+ /// </summary>
+ /// <value></value>
+ /// <remarks>
+ /// <para>
+ /// This <i>should</i> be put through <see cref="HttpUtility.HtmlEncode(string)"/> before
+ /// sending to a browser to secure against javascript injection attacks.
+ /// </para>
+ /// <para>
+ /// This property retains some aspects of the user-supplied identifier that get lost
+ /// in the <see cref="ClaimedIdentifier"/>. For example, XRIs used as user-supplied
+ /// identifiers (i.e. =Arnott) become unfriendly unique strings (i.e. =!9B72.7DD1.50A9.5CCD).
+ /// For display purposes, such as text on a web page that says "You're logged in as ...",
+ /// this property serves to provide the =Arnott string, or whatever else is the most friendly
+ /// string close to what the user originally typed in.
+ /// </para>
+ /// <para>
+ /// If the user-supplied identifier is a URI, this property will be the URI after all
+ /// redirects, and with the protocol and fragment trimmed off.
+ /// If the user-supplied identifier is an XRI, this property will be the original XRI.
+ /// If the user-supplied identifier is an OpenID Provider identifier (i.e. yahoo.com),
+ /// this property will be the Claimed Identifier, with the protocol stripped if it is a URI.
+ /// </para>
+ /// <para>
+ /// It is <b>very</b> important that this property <i>never</i> be used for database storage
+ /// or lookup to avoid identity spoofing and other security risks. For database storage
+ /// and lookup please use the <see cref="ClaimedIdentifier"/> property.
+ /// </para>
+ /// </remarks>
+ public virtual string FriendlyIdentifierForDisplay {
+ get { return null; }
+ }
+
+ /// <summary>
+ /// Gets the detailed success or failure status of the authentication attempt.
+ /// </summary>
+ public virtual AuthenticationStatus Status {
+ get { return AuthenticationStatus.ExtensionsOnly; }
+ }
+
+ /// <summary>
+ /// Gets information about the OpenId Provider, as advertised by the
+ /// OpenID discovery documents found at the <see cref="ClaimedIdentifier"/>
+ /// location.
+ /// </summary>
+ /// <value>
+ /// The Provider endpoint that issued the positive assertion;
+ /// or <c>null</c> if information about the Provider is unavailable.
+ /// </value>
+ public IProviderEndpoint Provider {
+ get { return this.provider; }
+ }
+
+ /// <summary>
+ /// Gets the details regarding a failed authentication attempt, if available.
+ /// This will be set if and only if <see cref="Status"/> is <see cref="AuthenticationStatus.Failed"/>.
+ /// </summary>
+ /// <value></value>
+ public Exception Exception {
+ get { return null; }
+ }
+
+ #endregion
+
+ /// <summary>
+ /// Gets a value indicating whether trusted callback arguments are available.
+ /// </summary>
+ /// <remarks>
+ /// We use this internally to avoid logging a warning during a standard snapshot creation.
+ /// </remarks>
+ internal bool TrustedCallbackArgumentsAvailable {
+ get { return this.response.ReturnToParametersSignatureValidated; }
+ }
+
+ /// <summary>
+ /// Gets the positive extension-only message the Relying Party received that this instance wraps.
+ /// </summary>
+ protected internal IndirectSignedResponse Response {
+ get { return this.response; }
+ }
+
+ #region IAuthenticationResponse methods
+
+ /// <summary>
+ /// Gets a callback argument's value that was previously added using
+ /// <see cref="IAuthenticationRequest.AddCallbackArguments(string, string)"/>.
+ /// </summary>
+ /// <param name="key">The name of the parameter whose value is sought.</param>
+ /// <returns>
+ /// The value of the argument, or null if the named parameter could not be found.
+ /// </returns>
+ /// <remarks>
+ /// Callback parameters are only available if they are complete and untampered with
+ /// since the original request message (as proven by a signature).
+ /// If the relying party is operating in stateless mode <c>null</c> is always
+ /// returned since the callback arguments could not be signed to protect against
+ /// tampering.
+ /// </remarks>
+ public string GetCallbackArgument(string key) {
+ if (this.response.ReturnToParametersSignatureValidated) {
+ return this.GetUntrustedCallbackArgument(key);
+ } else {
+ Logger.OpenId.WarnFormat(OpenIdStrings.CallbackArgumentsRequireSecretStore, typeof(IRelyingPartyAssociationStore).Name, typeof(OpenIdRelyingParty).Name);
+ return null;
+ }
+ }
+
+ /// <summary>
+ /// Gets a callback argument's value that was previously added using
+ /// <see cref="IAuthenticationRequest.AddCallbackArguments(string, string)"/>.
+ /// </summary>
+ /// <param name="key">The name of the parameter whose value is sought.</param>
+ /// <returns>
+ /// The value of the argument, or null if the named parameter could not be found.
+ /// </returns>
+ /// <remarks>
+ /// Callback parameters are only available even if the RP is in stateless mode,
+ /// or the callback parameters are otherwise unverifiable as untampered with.
+ /// Therefore, use this method only when the callback argument is not to be
+ /// used to make a security-sensitive decision.
+ /// </remarks>
+ public string GetUntrustedCallbackArgument(string key) {
+ return this.response.GetReturnToArgument(key);
+ }
+
+ /// <summary>
+ /// Gets all the callback arguments that were previously added using
+ /// <see cref="IAuthenticationRequest.AddCallbackArguments(string, string)"/> or as a natural part
+ /// of the return_to URL.
+ /// </summary>
+ /// <returns>A name-value dictionary. Never null.</returns>
+ /// <remarks>
+ /// Callback parameters are only available if they are complete and untampered with
+ /// since the original request message (as proven by a signature).
+ /// If the relying party is operating in stateless mode an empty dictionary is always
+ /// returned since the callback arguments could not be signed to protect against
+ /// tampering.
+ /// </remarks>
+ public IDictionary<string, string> GetCallbackArguments() {
+ if (this.response.ReturnToParametersSignatureValidated) {
+ return this.GetUntrustedCallbackArguments();
+ } else {
+ Logger.OpenId.WarnFormat(OpenIdStrings.CallbackArgumentsRequireSecretStore, typeof(IRelyingPartyAssociationStore).Name, typeof(OpenIdRelyingParty).Name);
+ return EmptyDictionary<string, string>.Instance;
+ }
+ }
+
+ /// <summary>
+ /// Gets all the callback arguments that were previously added using
+ /// <see cref="IAuthenticationRequest.AddCallbackArguments(string, string)"/> or as a natural part
+ /// of the return_to URL.
+ /// </summary>
+ /// <returns>A name-value dictionary. Never null.</returns>
+ /// <remarks>
+ /// Callback parameters are only available if they are complete and untampered with
+ /// since the original request message (as proven by a signature).
+ /// If the relying party is operating in stateless mode an empty dictionary is always
+ /// returned since the callback arguments could not be signed to protect against
+ /// tampering.
+ /// </remarks>
+ public IDictionary<string, string> GetUntrustedCallbackArguments() {
+ var args = new Dictionary<string, string>();
+
+ // Return all the return_to arguments, except for the OpenID-supporting ones.
+ // The only arguments that should be returned here are the ones that the host
+ // web site adds explicitly.
+ foreach (string key in this.response.GetReturnToParameterNames().Where(key => !OpenIdRelyingParty.IsOpenIdSupportingParameter(key))) {
+ args[key] = this.response.GetReturnToArgument(key);
+ }
+
+ return args;
+ }
+
+ /// <summary>
+ /// Tries to get an OpenID extension that may be present in the response.
+ /// </summary>
+ /// <typeparam name="T">The type of extension to look for in the response message.</typeparam>
+ /// <returns>
+ /// The extension, if it is found. Null otherwise.
+ /// </returns>
+ /// <remarks>
+ /// <para>Extensions are returned only if the Provider signed them.
+ /// Relying parties that do not care if the values were modified in
+ /// transit should use the <see cref="GetUntrustedExtension&lt;T&gt;"/> method
+ /// in order to allow the Provider to not sign the extension. </para>
+ /// <para>Unsigned extensions are completely unreliable and should be
+ /// used only to prefill user forms since the user or any other third
+ /// party may have tampered with the data carried by the extension.</para>
+ /// <para>Signed extensions are only reliable if the relying party
+ /// trusts the OpenID Provider that signed them. Signing does not mean
+ /// the relying party can trust the values -- it only means that the values
+ /// have not been tampered with since the Provider sent the message.</para>
+ /// </remarks>
+ public T GetExtension<T>() where T : IOpenIdMessageExtension {
+ return this.response.SignedExtensions.OfType<T>().FirstOrDefault();
+ }
+
+ /// <summary>
+ /// Tries to get an OpenID extension that may be present in the response.
+ /// </summary>
+ /// <param name="extensionType">Type of the extension to look for in the response.</param>
+ /// <returns>
+ /// The extension, if it is found. Null otherwise.
+ /// </returns>
+ /// <remarks>
+ /// <para>Extensions are returned only if the Provider signed them.
+ /// Relying parties that do not care if the values were modified in
+ /// transit should use the <see cref="GetUntrustedExtension"/> method
+ /// in order to allow the Provider to not sign the extension. </para>
+ /// <para>Unsigned extensions are completely unreliable and should be
+ /// used only to prefill user forms since the user or any other third
+ /// party may have tampered with the data carried by the extension.</para>
+ /// <para>Signed extensions are only reliable if the relying party
+ /// trusts the OpenID Provider that signed them. Signing does not mean
+ /// the relying party can trust the values -- it only means that the values
+ /// have not been tampered with since the Provider sent the message.</para>
+ /// </remarks>
+ public IOpenIdMessageExtension GetExtension(Type extensionType) {
+ return this.response.SignedExtensions.OfType<IOpenIdMessageExtension>().Where(ext => extensionType.IsInstanceOfType(ext)).FirstOrDefault();
+ }
+
+ /// <summary>
+ /// Tries to get an OpenID extension that may be present in the response, without
+ /// requiring it to be signed by the Provider.
+ /// </summary>
+ /// <typeparam name="T">The type of extension to look for in the response message.</typeparam>
+ /// <returns>
+ /// The extension, if it is found. Null otherwise.
+ /// </returns>
+ /// <remarks>
+ /// <para>Extensions are returned whether they are signed or not.
+ /// Use the <see cref="GetExtension&lt;T&gt;"/> method to retrieve
+ /// extension responses only if they are signed by the Provider to
+ /// protect against tampering. </para>
+ /// <para>Unsigned extensions are completely unreliable and should be
+ /// used only to prefill user forms since the user or any other third
+ /// party may have tampered with the data carried by the extension.</para>
+ /// <para>Signed extensions are only reliable if the relying party
+ /// trusts the OpenID Provider that signed them. Signing does not mean
+ /// the relying party can trust the values -- it only means that the values
+ /// have not been tampered with since the Provider sent the message.</para>
+ /// </remarks>
+ public T GetUntrustedExtension<T>() where T : IOpenIdMessageExtension {
+ return this.response.Extensions.OfType<T>().FirstOrDefault();
+ }
+
+ /// <summary>
+ /// Tries to get an OpenID extension that may be present in the response.
+ /// </summary>
+ /// <param name="extensionType">Type of the extension to look for in the response.</param>
+ /// <returns>
+ /// The extension, if it is found. Null otherwise.
+ /// </returns>
+ /// <remarks>
+ /// <para>Extensions are returned whether they are signed or not.
+ /// Use the <see cref="GetExtension"/> method to retrieve
+ /// extension responses only if they are signed by the Provider to
+ /// protect against tampering. </para>
+ /// <para>Unsigned extensions are completely unreliable and should be
+ /// used only to prefill user forms since the user or any other third
+ /// party may have tampered with the data carried by the extension.</para>
+ /// <para>Signed extensions are only reliable if the relying party
+ /// trusts the OpenID Provider that signed them. Signing does not mean
+ /// the relying party can trust the values -- it only means that the values
+ /// have not been tampered with since the Provider sent the message.</para>
+ /// </remarks>
+ public IOpenIdMessageExtension GetUntrustedExtension(Type extensionType) {
+ return this.response.Extensions.OfType<IOpenIdMessageExtension>().Where(ext => extensionType.IsInstanceOfType(ext)).FirstOrDefault();
+ }
+
+ #endregion
+ }
+}
diff --git a/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/PositiveAuthenticationResponse.cs b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/PositiveAuthenticationResponse.cs
new file mode 100644
index 0000000..3e2298c
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/PositiveAuthenticationResponse.cs
@@ -0,0 +1,174 @@
+//-----------------------------------------------------------------------
+// <copyright file="PositiveAuthenticationResponse.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OpenId.RelyingParty {
+ using System;
+ using System.Diagnostics;
+ using System.Diagnostics.Contracts;
+ using System.Linq;
+ using System.Web;
+ using DotNetOpenAuth.Messaging;
+ using DotNetOpenAuth.OpenId.Messages;
+
+ /// <summary>
+ /// Wraps a positive assertion response in an <see cref="IAuthenticationResponse"/> instance
+ /// for public consumption by the host web site.
+ /// </summary>
+ [DebuggerDisplay("Status: {Status}, ClaimedIdentifier: {ClaimedIdentifier}")]
+ internal class PositiveAuthenticationResponse : PositiveAnonymousResponse {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="PositiveAuthenticationResponse"/> class.
+ /// </summary>
+ /// <param name="response">The positive assertion response that was just received by the Relying Party.</param>
+ /// <param name="relyingParty">The relying party.</param>
+ internal PositiveAuthenticationResponse(PositiveAssertionResponse response, OpenIdRelyingParty relyingParty)
+ : base(response) {
+ Contract.Requires<ArgumentNullException>(relyingParty != null);
+
+ this.Endpoint = IdentifierDiscoveryResult.CreateForClaimedIdentifier(
+ this.Response.ClaimedIdentifier,
+ this.Response.GetReturnToArgument(AuthenticationRequest.UserSuppliedIdentifierParameterName),
+ this.Response.LocalIdentifier,
+ new ProviderEndpointDescription(this.Response.ProviderEndpoint, this.Response.Version),
+ null,
+ null);
+
+ this.VerifyDiscoveryMatchesAssertion(relyingParty);
+
+ Logger.OpenId.InfoFormat("Received identity assertion for {0} via {1}.", this.Response.ClaimedIdentifier, this.Provider.Uri);
+ }
+
+ #region IAuthenticationResponse Properties
+
+ /// <summary>
+ /// Gets the Identifier that the end user claims to own. For use with user database storage and lookup.
+ /// May be null for some failed authentications (i.e. failed directed identity authentications).
+ /// </summary>
+ /// <value></value>
+ /// <remarks>
+ /// <para>
+ /// This is the secure identifier that should be used for database storage and lookup.
+ /// It is not always friendly (i.e. =Arnott becomes =!9B72.7DD1.50A9.5CCD), but it protects
+ /// user identities against spoofing and other attacks.
+ /// </para>
+ /// <para>
+ /// For user-friendly identifiers to display, use the
+ /// <see cref="FriendlyIdentifierForDisplay"/> property.
+ /// </para>
+ /// </remarks>
+ public override Identifier ClaimedIdentifier {
+ get { return this.Endpoint.ClaimedIdentifier; }
+ }
+
+ /// <summary>
+ /// Gets a user-friendly OpenID Identifier for display purposes ONLY.
+ /// </summary>
+ /// <remarks>
+ /// <para>
+ /// This <i>should</i> be put through <see cref="HttpUtility.HtmlEncode(string)"/> before
+ /// sending to a browser to secure against javascript injection attacks.
+ /// </para>
+ /// <para>
+ /// This property retains some aspects of the user-supplied identifier that get lost
+ /// in the <see cref="ClaimedIdentifier"/>. For example, XRIs used as user-supplied
+ /// identifiers (i.e. =Arnott) become unfriendly unique strings (i.e. =!9B72.7DD1.50A9.5CCD).
+ /// For display purposes, such as text on a web page that says "You're logged in as ...",
+ /// this property serves to provide the =Arnott string, or whatever else is the most friendly
+ /// string close to what the user originally typed in.
+ /// </para>
+ /// <para>
+ /// If the user-supplied identifier is a URI, this property will be the URI after all
+ /// redirects, and with the protocol and fragment trimmed off.
+ /// If the user-supplied identifier is an XRI, this property will be the original XRI.
+ /// If the user-supplied identifier is an OpenID Provider identifier (i.e. yahoo.com),
+ /// this property will be the Claimed Identifier, with the protocol stripped if it is a URI.
+ /// </para>
+ /// <para>
+ /// It is <b>very</b> important that this property <i>never</i> be used for database storage
+ /// or lookup to avoid identity spoofing and other security risks. For database storage
+ /// and lookup please use the <see cref="ClaimedIdentifier"/> property.
+ /// </para>
+ /// </remarks>
+ public override string FriendlyIdentifierForDisplay {
+ get { return this.Endpoint.FriendlyIdentifierForDisplay; }
+ }
+
+ /// <summary>
+ /// Gets the detailed success or failure status of the authentication attempt.
+ /// </summary>
+ public override AuthenticationStatus Status {
+ get { return AuthenticationStatus.Authenticated; }
+ }
+
+ #endregion
+
+ /// <summary>
+ /// Gets the OpenID service endpoint reconstructed from the assertion message.
+ /// </summary>
+ /// <remarks>
+ /// This information is straight from the Provider, and therefore must not
+ /// be trusted until verified as matching the discovery information for
+ /// the claimed identifier to avoid a Provider asserting an Identifier
+ /// for which it has no authority.
+ /// </remarks>
+ internal IdentifierDiscoveryResult Endpoint { get; private set; }
+
+ /// <summary>
+ /// Gets the positive assertion response message.
+ /// </summary>
+ protected internal new PositiveAssertionResponse Response {
+ get { return (PositiveAssertionResponse)base.Response; }
+ }
+
+ /// <summary>
+ /// Verifies that the positive assertion data matches the results of
+ /// discovery on the Claimed Identifier.
+ /// </summary>
+ /// <param name="relyingParty">The relying party.</param>
+ /// <exception cref="ProtocolException">
+ /// Thrown when the Provider is asserting that a user controls an Identifier
+ /// when discovery on that Identifier contradicts what the Provider says.
+ /// This would be an indication of either a misconfigured Provider or
+ /// an attempt by someone to spoof another user's identity with a rogue Provider.
+ /// </exception>
+ private void VerifyDiscoveryMatchesAssertion(OpenIdRelyingParty relyingParty) {
+ Logger.OpenId.Debug("Verifying assertion matches identifier discovery results...");
+
+ // Ensure that we abide by the RP's rules regarding RequireSsl for this discovery step.
+ Identifier claimedId = this.Response.ClaimedIdentifier;
+ if (relyingParty.SecuritySettings.RequireSsl) {
+ if (!claimedId.TryRequireSsl(out claimedId)) {
+ Logger.OpenId.ErrorFormat("This site is configured to accept only SSL-protected OpenIDs, but {0} was asserted and must be rejected.", this.Response.ClaimedIdentifier);
+ ErrorUtilities.ThrowProtocol(OpenIdStrings.RequireSslNotSatisfiedByAssertedClaimedId, this.Response.ClaimedIdentifier);
+ }
+ }
+
+ // Check whether this particular identifier presents a problem with HTTP discovery
+ // due to limitations in the .NET Uri class.
+ UriIdentifier claimedIdUri = claimedId as UriIdentifier;
+ if (claimedIdUri != null && claimedIdUri.ProblematicNormalization) {
+ ErrorUtilities.VerifyProtocol(relyingParty.SecuritySettings.AllowApproximateIdentifierDiscovery, OpenIdStrings.ClaimedIdentifierDefiesDotNetNormalization);
+ Logger.OpenId.WarnFormat("Positive assertion for claimed identifier {0} cannot be precisely verified under partial trust hosting due to .NET limitation. An approximate verification will be attempted.", claimedId);
+ }
+
+ // While it LOOKS like we're performing discovery over HTTP again
+ // Yadis.IdentifierDiscoveryCachePolicy is set to HttpRequestCacheLevel.CacheIfAvailable
+ // which means that the .NET runtime is caching our discoveries for us. This turns out
+ // to be very fast and keeps our code clean and easily verifiable as correct and secure.
+ // CAUTION: if this discovery is ever made to be skipped based on previous discovery
+ // data that was saved to the return_to URL, be careful to verify that that information
+ // is signed by the RP before it's considered reliable. In 1.x stateless mode, this RP
+ // doesn't (and can't) sign its own return_to URL, so its cached discovery information
+ // is merely a hint that must be verified by performing discovery again here.
+ var discoveryResults = relyingParty.Discover(claimedId);
+ ErrorUtilities.VerifyProtocol(
+ discoveryResults.Contains(this.Endpoint),
+ OpenIdStrings.IssuedAssertionFailsIdentifierDiscovery,
+ this.Endpoint,
+ discoveryResults.ToStringDeferred(true));
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/PositiveAuthenticationResponseSnapshot.cs b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/PositiveAuthenticationResponseSnapshot.cs
new file mode 100644
index 0000000..80b424a
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/PositiveAuthenticationResponseSnapshot.cs
@@ -0,0 +1,304 @@
+//-----------------------------------------------------------------------
+// <copyright file="PositiveAuthenticationResponseSnapshot.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OpenId.RelyingParty {
+ using System;
+ using System.Collections.Generic;
+ using System.Diagnostics.Contracts;
+ using System.Text;
+ using System.Web;
+ using DotNetOpenAuth.Messaging;
+ using DotNetOpenAuth.OpenId.Messages;
+
+ /// <summary>
+ /// A serializable snapshot of a verified authentication message.
+ /// </summary>
+ [Serializable]
+ internal class PositiveAuthenticationResponseSnapshot : IAuthenticationResponse {
+ /// <summary>
+ /// The callback arguments that came with the authentication response.
+ /// </summary>
+ private IDictionary<string, string> callbackArguments;
+
+ /// <summary>
+ /// The untrusted callback arguments that came with the authentication response.
+ /// </summary>
+ private IDictionary<string, string> untrustedCallbackArguments;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="PositiveAuthenticationResponseSnapshot"/> class.
+ /// </summary>
+ /// <param name="copyFrom">The authentication response to copy from.</param>
+ internal PositiveAuthenticationResponseSnapshot(IAuthenticationResponse copyFrom) {
+ Contract.Requires<ArgumentNullException>(copyFrom != null);
+
+ this.ClaimedIdentifier = copyFrom.ClaimedIdentifier;
+ this.FriendlyIdentifierForDisplay = copyFrom.FriendlyIdentifierForDisplay;
+ this.Status = copyFrom.Status;
+ this.Provider = copyFrom.Provider;
+ this.untrustedCallbackArguments = copyFrom.GetUntrustedCallbackArguments();
+
+ // Do this special check to avoid logging a warning for trying to clone a dictionary.
+ var anonResponse = copyFrom as PositiveAnonymousResponse;
+ if (anonResponse == null || anonResponse.TrustedCallbackArgumentsAvailable) {
+ this.callbackArguments = copyFrom.GetCallbackArguments();
+ } else {
+ this.callbackArguments = EmptyDictionary<string, string>.Instance;
+ }
+ }
+
+ #region IAuthenticationResponse Members
+
+ /// <summary>
+ /// Gets the Identifier that the end user claims to own. For use with user database storage and lookup.
+ /// May be null for some failed authentications (i.e. failed directed identity authentications).
+ /// </summary>
+ /// <value></value>
+ /// <remarks>
+ /// <para>
+ /// This is the secure identifier that should be used for database storage and lookup.
+ /// It is not always friendly (i.e. =Arnott becomes =!9B72.7DD1.50A9.5CCD), but it protects
+ /// user identities against spoofing and other attacks.
+ /// </para>
+ /// <para>
+ /// For user-friendly identifiers to display, use the
+ /// <see cref="FriendlyIdentifierForDisplay"/> property.
+ /// </para>
+ /// </remarks>
+ public Identifier ClaimedIdentifier { get; private set; }
+
+ /// <summary>
+ /// Gets a user-friendly OpenID Identifier for display purposes ONLY.
+ /// </summary>
+ /// <value></value>
+ /// <remarks>
+ /// <para>
+ /// This <i>should</i> be put through <see cref="HttpUtility.HtmlEncode(string)"/> before
+ /// sending to a browser to secure against javascript injection attacks.
+ /// </para>
+ /// <para>
+ /// This property retains some aspects of the user-supplied identifier that get lost
+ /// in the <see cref="ClaimedIdentifier"/>. For example, XRIs used as user-supplied
+ /// identifiers (i.e. =Arnott) become unfriendly unique strings (i.e. =!9B72.7DD1.50A9.5CCD).
+ /// For display purposes, such as text on a web page that says "You're logged in as ...",
+ /// this property serves to provide the =Arnott string, or whatever else is the most friendly
+ /// string close to what the user originally typed in.
+ /// </para>
+ /// <para>
+ /// If the user-supplied identifier is a URI, this property will be the URI after all
+ /// redirects, and with the protocol and fragment trimmed off.
+ /// If the user-supplied identifier is an XRI, this property will be the original XRI.
+ /// If the user-supplied identifier is an OpenID Provider identifier (i.e. yahoo.com),
+ /// this property will be the Claimed Identifier, with the protocol stripped if it is a URI.
+ /// </para>
+ /// <para>
+ /// It is <b>very</b> important that this property <i>never</i> be used for database storage
+ /// or lookup to avoid identity spoofing and other security risks. For database storage
+ /// and lookup please use the <see cref="ClaimedIdentifier"/> property.
+ /// </para>
+ /// </remarks>
+ public string FriendlyIdentifierForDisplay { get; private set; }
+
+ /// <summary>
+ /// Gets the detailed success or failure status of the authentication attempt.
+ /// </summary>
+ /// <value></value>
+ public AuthenticationStatus Status { get; private set; }
+
+ /// <summary>
+ /// Gets information about the OpenId Provider, as advertised by the
+ /// OpenID discovery documents found at the <see cref="ClaimedIdentifier"/>
+ /// location.
+ /// </summary>
+ /// <value>
+ /// The Provider endpoint that issued the positive assertion;
+ /// or <c>null</c> if information about the Provider is unavailable.
+ /// </value>
+ public IProviderEndpoint Provider { get; private set; }
+
+ /// <summary>
+ /// Gets the details regarding a failed authentication attempt, if available.
+ /// This will be set if and only if <see cref="Status"/> is <see cref="AuthenticationStatus.Failed"/>.
+ /// </summary>
+ /// <value></value>
+ public Exception Exception {
+ get { throw new NotSupportedException(OpenIdStrings.NotSupportedByAuthenticationSnapshot); }
+ }
+
+ /// <summary>
+ /// Tries to get an OpenID extension that may be present in the response.
+ /// </summary>
+ /// <typeparam name="T">The type of extension to look for in the response message.</typeparam>
+ /// <returns>
+ /// The extension, if it is found. Null otherwise.
+ /// </returns>
+ /// <remarks>
+ /// <para>Extensions are returned only if the Provider signed them.
+ /// Relying parties that do not care if the values were modified in
+ /// transit should use the <see cref="GetUntrustedExtension&lt;T&gt;"/> method
+ /// in order to allow the Provider to not sign the extension. </para>
+ /// <para>Unsigned extensions are completely unreliable and should be
+ /// used only to prefill user forms since the user or any other third
+ /// party may have tampered with the data carried by the extension.</para>
+ /// <para>Signed extensions are only reliable if the relying party
+ /// trusts the OpenID Provider that signed them. Signing does not mean
+ /// the relying party can trust the values -- it only means that the values
+ /// have not been tampered with since the Provider sent the message.</para>
+ /// </remarks>
+ public T GetExtension<T>() where T : IOpenIdMessageExtension {
+ throw new NotSupportedException(OpenIdStrings.NotSupportedByAuthenticationSnapshot);
+ }
+
+ /// <summary>
+ /// Tries to get an OpenID extension that may be present in the response.
+ /// </summary>
+ /// <param name="extensionType">Type of the extension to look for in the response.</param>
+ /// <returns>
+ /// The extension, if it is found. Null otherwise.
+ /// </returns>
+ /// <remarks>
+ /// <para>Extensions are returned only if the Provider signed them.
+ /// Relying parties that do not care if the values were modified in
+ /// transit should use the <see cref="GetUntrustedExtension"/> method
+ /// in order to allow the Provider to not sign the extension. </para>
+ /// <para>Unsigned extensions are completely unreliable and should be
+ /// used only to prefill user forms since the user or any other third
+ /// party may have tampered with the data carried by the extension.</para>
+ /// <para>Signed extensions are only reliable if the relying party
+ /// trusts the OpenID Provider that signed them. Signing does not mean
+ /// the relying party can trust the values -- it only means that the values
+ /// have not been tampered with since the Provider sent the message.</para>
+ /// </remarks>
+ public IOpenIdMessageExtension GetExtension(Type extensionType) {
+ throw new NotSupportedException(OpenIdStrings.NotSupportedByAuthenticationSnapshot);
+ }
+
+ /// <summary>
+ /// Tries to get an OpenID extension that may be present in the response, without
+ /// requiring it to be signed by the Provider.
+ /// </summary>
+ /// <typeparam name="T">The type of extension to look for in the response message.</typeparam>
+ /// <returns>
+ /// The extension, if it is found. Null otherwise.
+ /// </returns>
+ /// <remarks>
+ /// <para>Extensions are returned whether they are signed or not.
+ /// Use the <see cref="GetExtension&lt;T&gt;"/> method to retrieve
+ /// extension responses only if they are signed by the Provider to
+ /// protect against tampering. </para>
+ /// <para>Unsigned extensions are completely unreliable and should be
+ /// used only to prefill user forms since the user or any other third
+ /// party may have tampered with the data carried by the extension.</para>
+ /// <para>Signed extensions are only reliable if the relying party
+ /// trusts the OpenID Provider that signed them. Signing does not mean
+ /// the relying party can trust the values -- it only means that the values
+ /// have not been tampered with since the Provider sent the message.</para>
+ /// </remarks>
+ public T GetUntrustedExtension<T>() where T : IOpenIdMessageExtension {
+ throw new NotSupportedException(OpenIdStrings.NotSupportedByAuthenticationSnapshot);
+ }
+
+ /// <summary>
+ /// Tries to get an OpenID extension that may be present in the response.
+ /// </summary>
+ /// <param name="extensionType">Type of the extension to look for in the response.</param>
+ /// <returns>
+ /// The extension, if it is found. Null otherwise.
+ /// </returns>
+ /// <remarks>
+ /// <para>Extensions are returned whether they are signed or not.
+ /// Use the <see cref="GetExtension"/> method to retrieve
+ /// extension responses only if they are signed by the Provider to
+ /// protect against tampering. </para>
+ /// <para>Unsigned extensions are completely unreliable and should be
+ /// used only to prefill user forms since the user or any other third
+ /// party may have tampered with the data carried by the extension.</para>
+ /// <para>Signed extensions are only reliable if the relying party
+ /// trusts the OpenID Provider that signed them. Signing does not mean
+ /// the relying party can trust the values -- it only means that the values
+ /// have not been tampered with since the Provider sent the message.</para>
+ /// </remarks>
+ public IOpenIdMessageExtension GetUntrustedExtension(Type extensionType) {
+ throw new NotSupportedException(OpenIdStrings.NotSupportedByAuthenticationSnapshot);
+ }
+
+ /// <summary>
+ /// Gets all the callback arguments that were previously added using
+ /// <see cref="IAuthenticationRequest.AddCallbackArguments(string, string)"/> or as a natural part
+ /// of the return_to URL.
+ /// </summary>
+ /// <returns>A name-value dictionary. Never null.</returns>
+ /// <remarks>
+ /// <para>This MAY return any argument on the querystring that came with the authentication response,
+ /// which may include parameters not explicitly added using
+ /// <see cref="IAuthenticationRequest.AddCallbackArguments(string, string)"/>.</para>
+ /// <para>Note that these values are NOT protected against tampering in transit.</para>
+ /// </remarks>
+ public IDictionary<string, string> GetCallbackArguments() {
+ // Return a copy so that the caller cannot change the contents.
+ return new Dictionary<string, string>(this.callbackArguments);
+ }
+
+ /// <summary>
+ /// Gets all the callback arguments that were previously added using
+ /// <see cref="IAuthenticationRequest.AddCallbackArguments(string, string)"/> or as a natural part
+ /// of the return_to URL.
+ /// </summary>
+ /// <returns>A name-value dictionary. Never null.</returns>
+ /// <remarks>
+ /// Callback parameters are only available even if the RP is in stateless mode,
+ /// or the callback parameters are otherwise unverifiable as untampered with.
+ /// Therefore, use this method only when the callback argument is not to be
+ /// used to make a security-sensitive decision.
+ /// </remarks>
+ public IDictionary<string, string> GetUntrustedCallbackArguments() {
+ // Return a copy so that the caller cannot change the contents.
+ return new Dictionary<string, string>(this.untrustedCallbackArguments);
+ }
+
+ /// <summary>
+ /// Gets a callback argument's value that was previously added using
+ /// <see cref="IAuthenticationRequest.AddCallbackArguments(string, string)"/>.
+ /// </summary>
+ /// <param name="key">The name of the parameter whose value is sought.</param>
+ /// <returns>
+ /// The value of the argument, or null if the named parameter could not be found.
+ /// </returns>
+ /// <remarks>
+ /// <para>This may return any argument on the querystring that came with the authentication response,
+ /// which may include parameters not explicitly added using
+ /// <see cref="IAuthenticationRequest.AddCallbackArguments(string, string)"/>.</para>
+ /// <para>Note that these values are NOT protected against tampering in transit.</para>
+ /// </remarks>
+ public string GetCallbackArgument(string key) {
+ string value;
+ this.callbackArguments.TryGetValue(key, out value);
+ return value;
+ }
+
+ /// <summary>
+ /// Gets a callback argument's value that was previously added using
+ /// <see cref="IAuthenticationRequest.AddCallbackArguments(string, string)"/>.
+ /// </summary>
+ /// <param name="key">The name of the parameter whose value is sought.</param>
+ /// <returns>
+ /// The value of the argument, or null if the named parameter could not be found.
+ /// </returns>
+ /// <remarks>
+ /// Callback parameters are only available even if the RP is in stateless mode,
+ /// or the callback parameters are otherwise unverifiable as untampered with.
+ /// Therefore, use this method only when the callback argument is not to be
+ /// used to make a security-sensitive decision.
+ /// </remarks>
+ public string GetUntrustedCallbackArgument(string key) {
+ string value;
+ this.untrustedCallbackArguments.TryGetValue(key, out value);
+ return value;
+ }
+
+ #endregion
+ }
+}
diff --git a/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/RelyingPartySecuritySettings.cs b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/RelyingPartySecuritySettings.cs
new file mode 100644
index 0000000..fc6d4c7
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/RelyingPartySecuritySettings.cs
@@ -0,0 +1,187 @@
+//-----------------------------------------------------------------------
+// <copyright file="RelyingPartySecuritySettings.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OpenId.RelyingParty {
+ using System;
+ using System.Collections.Generic;
+ using System.Collections.ObjectModel;
+ using System.Diagnostics.CodeAnalysis;
+ using System.Diagnostics.Contracts;
+ using System.Linq;
+ using DotNetOpenAuth.Messaging;
+
+ /// <summary>
+ /// Security settings that are applicable to relying parties.
+ /// </summary>
+ public sealed class RelyingPartySecuritySettings : SecuritySettings {
+ /// <summary>
+ /// The default value for the <see cref="ProtectDownlevelReplayAttacks"/> property.
+ /// </summary>
+ internal const bool ProtectDownlevelReplayAttacksDefault = true;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="RelyingPartySecuritySettings"/> class.
+ /// </summary>
+ internal RelyingPartySecuritySettings()
+ : base(false) {
+ this.PrivateSecretMaximumAge = TimeSpan.FromDays(7);
+ this.ProtectDownlevelReplayAttacks = ProtectDownlevelReplayAttacksDefault;
+ this.AllowApproximateIdentifierDiscovery = true;
+ this.TrustedProviderEndpoints = new HashSet<Uri>();
+ }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether the entire pipeline from Identifier discovery to
+ /// Provider redirect is guaranteed to be encrypted using HTTPS for authentication to succeed.
+ /// </summary>
+ /// <remarks>
+ /// <para>Setting this property to true is appropriate for RPs with highly sensitive
+ /// personal information behind the authentication (money management, health records, etc.)</para>
+ /// <para>When set to true, some behavioral changes and additional restrictions are placed:</para>
+ /// <list>
+ /// <item>User-supplied identifiers lacking a scheme are prepended with
+ /// HTTPS:// rather than the standard HTTP:// automatically.</item>
+ /// <item>User-supplied identifiers are not allowed to use HTTP for the scheme.</item>
+ /// <item>All redirects during discovery on the user-supplied identifier must be HTTPS.</item>
+ /// <item>Any XRDS file found by discovery on the User-supplied identifier must be protected using HTTPS.</item>
+ /// <item>Only Provider endpoints found at HTTPS URLs will be considered.</item>
+ /// <item>If the discovered identifier is an OP Identifier (directed identity), the
+ /// Claimed Identifier eventually asserted by the Provider must be an HTTPS identifier.</item>
+ /// <item>In the case of an unsolicited assertion, the asserted Identifier, discovery on it and
+ /// the asserting provider endpoint must all be secured by HTTPS.</item>
+ /// </list>
+ /// <para>Although the first redirect from this relying party to the Provider is required
+ /// to use HTTPS, any additional redirects within the Provider cannot be protected and MAY
+ /// revert the user's connection to HTTP, based on individual Provider implementation.
+ /// There is nothing that the RP can do to detect or prevent this.</para>
+ /// <para>
+ /// A <see cref="ProtocolException"/> is thrown during discovery or authentication when a secure pipeline cannot be established.
+ /// </para>
+ /// </remarks>
+ public bool RequireSsl { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether only OP Identifiers will be discoverable
+ /// when creating authentication requests.
+ /// </summary>
+ public bool RequireDirectedIdentity { get; set; }
+
+ /// <summary>
+ /// Gets or sets the oldest version of OpenID the remote party is allowed to implement.
+ /// </summary>
+ /// <value>Defaults to <see cref="ProtocolVersion.V10"/></value>
+ public ProtocolVersion MinimumRequiredOpenIdVersion { get; set; }
+
+ /// <summary>
+ /// Gets or sets the maximum allowable age of the secret a Relying Party
+ /// uses to its return_to URLs and nonces with 1.0 Providers.
+ /// </summary>
+ /// <value>The default value is 7 days.</value>
+ public TimeSpan PrivateSecretMaximumAge { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether all unsolicited assertions should be ignored.
+ /// </summary>
+ /// <value>The default value is <c>false</c>.</value>
+ public bool RejectUnsolicitedAssertions { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether delegating identifiers are refused for authentication.
+ /// </summary>
+ /// <value>The default value is <c>false</c>.</value>
+ /// <remarks>
+ /// When set to <c>true</c>, login attempts that start at the RP or arrive via unsolicited
+ /// assertions will be rejected if discovery on the identifier shows that OpenID delegation
+ /// is used for the identifier. This is useful for an RP that should only accept identifiers
+ /// directly issued by the Provider that is sending the assertion.
+ /// </remarks>
+ public bool RejectDelegatingIdentifiers { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether unsigned extensions in authentication responses should be ignored.
+ /// </summary>
+ /// <value>The default value is <c>false</c>.</value>
+ /// <remarks>
+ /// When set to true, the <see cref="IAuthenticationResponse.GetUntrustedExtension"/> methods
+ /// will not return any extension that was not signed by the Provider.
+ /// </remarks>
+ public bool IgnoreUnsignedExtensions { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether authentication requests will only be
+ /// sent to Providers with whom we can create a shared association.
+ /// </summary>
+ /// <value>
+ /// <c>true</c> to immediately fail authentication if an association with the Provider cannot be established; otherwise, <c>false</c>.
+ /// The default value is <c>false</c>.
+ /// </value>
+ public bool RequireAssociation { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether identifiers that are both OP Identifiers and Claimed Identifiers
+ /// should ever be recognized as claimed identifiers.
+ /// </summary>
+ /// <value>
+ /// The default value is <c>false</c>, per the OpenID 2.0 spec.
+ /// </value>
+ /// <remarks>
+ /// OpenID 2.0 sections 7.3.2.2 and 11.2 specify that OP Identifiers never be recognized as Claimed Identifiers.
+ /// However, for some scenarios it may be desirable for an RP to override this behavior and allow this.
+ /// The security ramifications of setting this property to <c>true</c> have not been fully explored and
+ /// therefore this setting should only be changed with caution.
+ /// </remarks>
+ public bool AllowDualPurposeIdentifiers { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether certain Claimed Identifiers that exploit
+ /// features that .NET does not have the ability to send exact HTTP requests for will
+ /// still be allowed by using an approximate HTTP request.
+ /// </summary>
+ /// <value>
+ /// The default value is <c>true</c>.
+ /// </value>
+ public bool AllowApproximateIdentifierDiscovery { get; set; }
+
+ /// <summary>
+ /// Gets the set of trusted OpenID Provider Endpoint URIs.
+ /// </summary>
+ public HashSet<Uri> TrustedProviderEndpoints { get; private set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether any login attempt coming from an OpenID Provider Endpoint that is not on this
+ /// whitelist of trusted OP Endpoints will be rejected. If the trusted providers list is empty and this value
+ /// is true, all assertions are rejected.
+ /// </summary>
+ /// <value>Default is <c>false</c>.</value>
+ public bool RejectAssertionsFromUntrustedProviders { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether special measures are taken to
+ /// protect users from replay attacks when those users' identities are hosted
+ /// by OpenID 1.x Providers.
+ /// </summary>
+ /// <value>The default value is <c>true</c>.</value>
+ /// <remarks>
+ /// <para>Nonces for protection against replay attacks were not mandated
+ /// by OpenID 1.x, which leaves users open to replay attacks.</para>
+ /// <para>This feature works by adding a signed nonce to the authentication request.
+ /// This might increase the request size beyond what some OpenID 1.1 Providers
+ /// (such as Blogger) are capable of handling.</para>
+ /// </remarks>
+ internal bool ProtectDownlevelReplayAttacks { get; set; }
+
+ /// <summary>
+ /// Filters out any disallowed endpoints.
+ /// </summary>
+ /// <param name="endpoints">The endpoints discovered on an Identifier.</param>
+ /// <returns>A sequence of endpoints that satisfy all security requirements.</returns>
+ internal IEnumerable<IdentifierDiscoveryResult> FilterEndpoints(IEnumerable<IdentifierDiscoveryResult> endpoints) {
+ return endpoints
+ .Where(se => !this.RejectDelegatingIdentifiers || se.ClaimedIdentifier == se.ProviderLocalIdentifier)
+ .Where(se => !this.RequireDirectedIdentity || se.ClaimedIdentifier == se.Protocol.ClaimedIdentifierForOPIdentifier);
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/SelectorButton.cs b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/SelectorButton.cs
new file mode 100644
index 0000000..0be3a5f
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/SelectorButton.cs
@@ -0,0 +1,46 @@
+//-----------------------------------------------------------------------
+// <copyright file="SelectorButton.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OpenId.RelyingParty {
+ using System;
+ using System.Diagnostics.Contracts;
+ using System.Web.UI;
+
+ /// <summary>
+ /// A button that would appear in the <see cref="OpenIdSelector"/> control via its <see cref="OpenIdSelector.Buttons"/> collection.
+ /// </summary>
+ [ContractClass(typeof(SelectorButtonContract))]
+ public abstract class SelectorButton {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="SelectorButton"/> class.
+ /// </summary>
+ protected SelectorButton() {
+ }
+
+ /// <summary>
+ /// Ensures that this button has been initialized to a valid state.
+ /// </summary>
+ /// <remarks>
+ /// This is "internal" -- NOT "protected internal" deliberately. It makes it impossible
+ /// to derive from this class outside the assembly, which suits our purposes since the
+ /// <see cref="OpenIdSelector"/> control is not designed for an extensible set of button types.
+ /// </remarks>
+ internal abstract void EnsureValid();
+
+ /// <summary>
+ /// Renders the leading attributes for the LI tag.
+ /// </summary>
+ /// <param name="writer">The writer.</param>
+ protected internal abstract void RenderLeadingAttributes(HtmlTextWriter writer);
+
+ /// <summary>
+ /// Renders the content of the button.
+ /// </summary>
+ /// <param name="writer">The writer.</param>
+ /// <param name="selector">The containing selector control.</param>
+ protected internal abstract void RenderButtonContent(HtmlTextWriter writer, OpenIdSelector selector);
+ }
+}
diff --git a/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/SelectorButtonContract.cs b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/SelectorButtonContract.cs
new file mode 100644
index 0000000..c70218a
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/SelectorButtonContract.cs
@@ -0,0 +1,46 @@
+//-----------------------------------------------------------------------
+// <copyright file="SelectorButtonContract.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OpenId.RelyingParty {
+ using System;
+ using System.Diagnostics.Contracts;
+ using System.Web.UI;
+
+ /// <summary>
+ /// The contract class for the <see cref="SelectorButton"/> class.
+ /// </summary>
+ [ContractClassFor(typeof(SelectorButton))]
+ internal abstract class SelectorButtonContract : SelectorButton {
+ /// <summary>
+ /// Ensures that this button has been initialized to a valid state.
+ /// </summary>
+ /// <remarks>
+ /// This is "internal" -- NOT "protected internal" deliberately. It makes it impossible
+ /// to derive from this class outside the assembly, which suits our purposes since the
+ /// <see cref="OpenIdSelector"/> control is not designed for an extensible set of button types.
+ /// </remarks>
+ internal override void EnsureValid() {
+ }
+
+ /// <summary>
+ /// Renders the leading attributes for the LI tag.
+ /// </summary>
+ /// <param name="writer">The writer.</param>
+ protected internal override void RenderLeadingAttributes(HtmlTextWriter writer) {
+ Contract.Requires<ArgumentNullException>(writer != null);
+ }
+
+ /// <summary>
+ /// Renders the content of the button.
+ /// </summary>
+ /// <param name="writer">The writer.</param>
+ /// <param name="selector">The containing selector control.</param>
+ protected internal override void RenderButtonContent(HtmlTextWriter writer, OpenIdSelector selector) {
+ Contract.Requires<ArgumentNullException>(writer != null);
+ Contract.Requires<ArgumentNullException>(selector != null);
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/SelectorInfoCardButton.cs b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/SelectorInfoCardButton.cs
new file mode 100644
index 0000000..c5dda1c
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/SelectorInfoCardButton.cs
@@ -0,0 +1,102 @@
+//-----------------------------------------------------------------------
+// <copyright file="SelectorInfoCardButton.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OpenId.RelyingParty {
+ using System;
+ using System.Collections.ObjectModel;
+ using System.ComponentModel;
+ using System.Diagnostics.Contracts;
+ using System.Web.UI;
+ using DotNetOpenAuth.InfoCard;
+
+ /// <summary>
+ /// A button that appears in the <see cref="OpenIdSelector"/> control that
+ /// activates the Information Card selector on the browser, if one is available.
+ /// </summary>
+ public class SelectorInfoCardButton : SelectorButton, IDisposable {
+ /// <summary>
+ /// The backing field for the <see cref="InfoCardSelector"/> property.
+ /// </summary>
+ private InfoCardSelector infoCardSelector;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="SelectorInfoCardButton"/> class.
+ /// </summary>
+ public SelectorInfoCardButton() {
+ Reporting.RecordFeatureUse(this);
+ }
+
+ /// <summary>
+ /// Gets or sets the InfoCard selector which may be displayed alongside the OP buttons.
+ /// </summary>
+ [PersistenceMode(PersistenceMode.InnerProperty)]
+ public InfoCardSelector InfoCardSelector {
+ get {
+ if (this.infoCardSelector == null) {
+ this.infoCardSelector = new InfoCardSelector();
+ }
+
+ return this.infoCardSelector;
+ }
+
+ set {
+ Contract.Requires<ArgumentNullException>(value != null);
+ if (this.infoCardSelector != null) {
+ Logger.Library.WarnFormat("{0}.InfoCardSelector property is being set multiple times.", GetType().Name);
+ }
+
+ this.infoCardSelector = value;
+ }
+ }
+
+ #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
+
+ /// <summary>
+ /// Ensures that this button has been initialized to a valid state.
+ /// </summary>
+ internal override void EnsureValid() {
+ }
+
+ /// <summary>
+ /// Renders the leading attributes for the LI tag.
+ /// </summary>
+ /// <param name="writer">The writer.</param>
+ protected internal override void RenderLeadingAttributes(HtmlTextWriter writer) {
+ writer.AddAttribute(HtmlTextWriterAttribute.Class, "infocard");
+ }
+
+ /// <summary>
+ /// Renders the content of the button.
+ /// </summary>
+ /// <param name="writer">The writer.</param>
+ /// <param name="selector">The containing selector control.</param>
+ protected internal override void RenderButtonContent(HtmlTextWriter writer, OpenIdSelector selector) {
+ this.InfoCardSelector.RenderControl(writer);
+ }
+
+ /// <summary>
+ /// Releases unmanaged and - optionally - managed resources
+ /// </summary>
+ /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
+ protected virtual void Dispose(bool disposing) {
+ if (disposing) {
+ if (this.infoCardSelector != null) {
+ this.infoCardSelector.Dispose();
+ }
+ }
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/SelectorOpenIdButton.cs b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/SelectorOpenIdButton.cs
new file mode 100644
index 0000000..ac4dcbf
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/SelectorOpenIdButton.cs
@@ -0,0 +1,82 @@
+//-----------------------------------------------------------------------
+// <copyright file="SelectorOpenIdButton.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OpenId.RelyingParty {
+ using System;
+ using System.ComponentModel;
+ using System.Diagnostics.Contracts;
+ using System.Drawing.Design;
+ using System.Web.UI;
+ using DotNetOpenAuth.Messaging;
+
+ /// <summary>
+ /// A button that appears in the <see cref="OpenIdSelector"/> control that
+ /// allows the user to type in a user-supplied identifier.
+ /// </summary>
+ public class SelectorOpenIdButton : SelectorButton {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="SelectorOpenIdButton"/> class.
+ /// </summary>
+ public SelectorOpenIdButton() {
+ Reporting.RecordFeatureUse(this);
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="SelectorOpenIdButton"/> class.
+ /// </summary>
+ /// <param name="imageUrl">The image to display on the button.</param>
+ public SelectorOpenIdButton(string imageUrl)
+ : this() {
+ Contract.Requires<ArgumentException>(!string.IsNullOrEmpty(imageUrl));
+
+ this.Image = imageUrl;
+ }
+
+ /// <summary>
+ /// Gets or sets the path to the image to display on the button's surface.
+ /// </summary>
+ /// <value>The virtual path to the image.</value>
+ [Editor("System.Web.UI.Design.ImageUrlEditor, System.Design, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a", typeof(UITypeEditor))]
+ [UrlProperty]
+ public string Image { get; set; }
+
+ /// <summary>
+ /// Ensures that this button has been initialized to a valid state.
+ /// </summary>
+ internal override void EnsureValid() {
+ Contract.Ensures(!string.IsNullOrEmpty(this.Image));
+
+ // Every button must have an image.
+ ErrorUtilities.VerifyOperation(!string.IsNullOrEmpty(this.Image), OpenIdStrings.PropertyNotSet, "SelectorButton.Image");
+ }
+
+ /// <summary>
+ /// Renders the leading attributes for the LI tag.
+ /// </summary>
+ /// <param name="writer">The writer.</param>
+ protected internal override void RenderLeadingAttributes(HtmlTextWriter writer) {
+ writer.AddAttribute(HtmlTextWriterAttribute.Id, "OpenIDButton");
+ writer.AddAttribute(HtmlTextWriterAttribute.Class, "OpenIDButton");
+ }
+
+ /// <summary>
+ /// Renders the content of the button.
+ /// </summary>
+ /// <param name="writer">The writer.</param>
+ /// <param name="selector">The containing selector control.</param>
+ protected internal override void RenderButtonContent(HtmlTextWriter writer, OpenIdSelector selector) {
+ writer.AddAttribute(HtmlTextWriterAttribute.Src, selector.Page.ResolveUrl(this.Image));
+ writer.RenderBeginTag(HtmlTextWriterTag.Img);
+ writer.RenderEndTag();
+
+ writer.AddAttribute(HtmlTextWriterAttribute.Src, selector.Page.ClientScript.GetWebResourceUrl(typeof(OpenIdAjaxTextBox), OpenIdAjaxTextBox.EmbeddedLoginSuccessResourceName));
+ writer.AddAttribute(HtmlTextWriterAttribute.Class, "loginSuccess");
+ writer.AddAttribute(HtmlTextWriterAttribute.Title, selector.AuthenticatedAsToolTip);
+ writer.RenderBeginTag(HtmlTextWriterTag.Img);
+ writer.RenderEndTag();
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/SelectorProviderButton.cs b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/SelectorProviderButton.cs
new file mode 100644
index 0000000..2195e73
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/SelectorProviderButton.cs
@@ -0,0 +1,113 @@
+//-----------------------------------------------------------------------
+// <copyright file="SelectorProviderButton.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OpenId.RelyingParty {
+ using System;
+ using System.ComponentModel;
+ using System.Diagnostics.Contracts;
+ using System.Drawing.Design;
+ using System.Web.UI;
+ using DotNetOpenAuth.ComponentModel;
+ using DotNetOpenAuth.Messaging;
+
+ /// <summary>
+ /// A button that appears in the <see cref="OpenIdSelector"/> control that
+ /// provides one-click access to a popular OpenID Provider.
+ /// </summary>
+ public class SelectorProviderButton : SelectorButton {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="SelectorProviderButton"/> class.
+ /// </summary>
+ public SelectorProviderButton() {
+ Reporting.RecordFeatureUse(this);
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="SelectorProviderButton"/> class.
+ /// </summary>
+ /// <param name="providerIdentifier">The OP Identifier.</param>
+ /// <param name="imageUrl">The image to display on the button.</param>
+ public SelectorProviderButton(Identifier providerIdentifier, string imageUrl)
+ : this() {
+ Contract.Requires<ArgumentNullException>(providerIdentifier != null);
+ Contract.Requires<ArgumentException>(!string.IsNullOrEmpty(imageUrl));
+
+ this.OPIdentifier = providerIdentifier;
+ this.Image = imageUrl;
+ }
+
+ /// <summary>
+ /// Gets or sets the path to the image to display on the button's surface.
+ /// </summary>
+ /// <value>The virtual path to the image.</value>
+ [Editor("System.Web.UI.Design.ImageUrlEditor, System.Design, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a", typeof(UITypeEditor))]
+ [UrlProperty]
+ public string Image { get; set; }
+
+ /// <summary>
+ /// Gets or sets the OP Identifier represented by the button.
+ /// </summary>
+ /// <value>
+ /// The OP identifier, which may be provided in the easiest "user-supplied identifier" form,
+ /// but for security should be provided with a leading https:// if possible.
+ /// For example: "yahoo.com" or "https://me.yahoo.com/".
+ /// </value>
+ [TypeConverter(typeof(IdentifierConverter))]
+ public Identifier OPIdentifier { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this Provider doesn't handle
+ /// checkid_immediate messages correctly and background authentication
+ /// should not be attempted.
+ /// </summary>
+ public bool SkipBackgroundAuthentication { get; set; }
+
+ /// <summary>
+ /// Ensures that this button has been initialized to a valid state.
+ /// </summary>
+ internal override void EnsureValid() {
+ Contract.Ensures(!string.IsNullOrEmpty(this.Image));
+ Contract.Ensures(this.OPIdentifier != null);
+
+ // Every button must have an image.
+ ErrorUtilities.VerifyOperation(!string.IsNullOrEmpty(this.Image), OpenIdStrings.PropertyNotSet, "SelectorButton.Image");
+
+ // Every button must have exactly one purpose.
+ ErrorUtilities.VerifyOperation(this.OPIdentifier != null, OpenIdStrings.PropertyNotSet, "SelectorButton.OPIdentifier");
+ }
+
+ /// <summary>
+ /// Renders the leading attributes for the LI tag.
+ /// </summary>
+ /// <param name="writer">The writer.</param>
+ protected internal override void RenderLeadingAttributes(HtmlTextWriter writer) {
+ writer.AddAttribute(HtmlTextWriterAttribute.Id, this.OPIdentifier);
+
+ string style = "OPButton";
+ if (this.SkipBackgroundAuthentication) {
+ style += " NoAsyncAuth";
+ }
+ writer.AddAttribute(HtmlTextWriterAttribute.Class, style);
+ }
+
+ /// <summary>
+ /// Renders the content of the button.
+ /// </summary>
+ /// <param name="writer">The writer.</param>
+ /// <param name="selector">The containing selector control.</param>
+ protected internal override void RenderButtonContent(HtmlTextWriter writer, OpenIdSelector selector) {
+ writer.AddAttribute(HtmlTextWriterAttribute.Src, selector.Page.ResolveUrl(this.Image));
+ writer.RenderBeginTag(HtmlTextWriterTag.Img);
+ writer.RenderEndTag();
+
+ writer.AddAttribute(HtmlTextWriterAttribute.Src, selector.Page.ClientScript.GetWebResourceUrl(typeof(OpenIdAjaxTextBox), OpenIdAjaxTextBox.EmbeddedLoginSuccessResourceName));
+ writer.AddAttribute(HtmlTextWriterAttribute.Class, "loginSuccess");
+ writer.AddAttribute(HtmlTextWriterAttribute.Title, selector.AuthenticatedAsToolTip);
+ writer.RenderBeginTag(HtmlTextWriterTag.Img);
+ writer.RenderEndTag();
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/SimpleXrdsProviderEndpoint.cs b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/SimpleXrdsProviderEndpoint.cs
new file mode 100644
index 0000000..678f69a
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/SimpleXrdsProviderEndpoint.cs
@@ -0,0 +1,78 @@
+//-----------------------------------------------------------------------
+// <copyright file="SimpleXrdsProviderEndpoint.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OpenId.RelyingParty {
+ using System;
+ using System.Collections.ObjectModel;
+ using DotNetOpenAuth.Messaging;
+ using DotNetOpenAuth.OpenId.Messages;
+
+ /// <summary>
+ /// A very simple IXrdsProviderEndpoint implementation for verifying that all positive
+ /// assertions (particularly unsolicited ones) are received from OP endpoints that
+ /// are deemed permissible by the host RP.
+ /// </summary>
+ internal class SimpleXrdsProviderEndpoint : IProviderEndpoint {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="SimpleXrdsProviderEndpoint"/> class.
+ /// </summary>
+ /// <param name="positiveAssertion">The positive assertion.</param>
+ internal SimpleXrdsProviderEndpoint(PositiveAssertionResponse positiveAssertion) {
+ this.Uri = positiveAssertion.ProviderEndpoint;
+ this.Version = positiveAssertion.Version;
+ }
+
+ #region IProviderEndpoint Members
+
+ /// <summary>
+ /// Gets the detected version of OpenID implemented by the Provider.
+ /// </summary>
+ public Version Version { get; private set; }
+
+ /// <summary>
+ /// Gets the URL that the OpenID Provider receives authentication requests at.
+ /// </summary>
+ public Uri Uri { get; private set; }
+
+ /// <summary>
+ /// Checks whether the OpenId Identifier claims support for a given extension.
+ /// </summary>
+ /// <typeparam name="T">The extension whose support is being queried.</typeparam>
+ /// <returns>
+ /// True if support for the extension is advertised. False otherwise.
+ /// </returns>
+ /// <remarks>
+ /// Note that a true or false return value is no guarantee of a Provider's
+ /// support for or lack of support for an extension. The return value is
+ /// determined by how the authenticating user filled out his/her XRDS document only.
+ /// The only way to be sure of support for a given extension is to include
+ /// the extension in the request and see if a response comes back for that extension.
+ /// </remarks>
+ bool IProviderEndpoint.IsExtensionSupported<T>() {
+ throw new NotImplementedException();
+ }
+
+ /// <summary>
+ /// Checks whether the OpenId Identifier claims support for a given extension.
+ /// </summary>
+ /// <param name="extensionType">The extension whose support is being queried.</param>
+ /// <returns>
+ /// True if support for the extension is advertised. False otherwise.
+ /// </returns>
+ /// <remarks>
+ /// Note that a true or false return value is no guarantee of a Provider's
+ /// support for or lack of support for an extension. The return value is
+ /// determined by how the authenticating user filled out his/her XRDS document only.
+ /// The only way to be sure of support for a given extension is to include
+ /// the extension in the request and see if a response comes back for that extension.
+ /// </remarks>
+ bool IProviderEndpoint.IsExtensionSupported(Type extensionType) {
+ throw new NotImplementedException();
+ }
+
+ #endregion
+ }
+}
diff --git a/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/StandardRelyingPartyApplicationStore.cs b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/StandardRelyingPartyApplicationStore.cs
new file mode 100644
index 0000000..a14b55d
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/StandardRelyingPartyApplicationStore.cs
@@ -0,0 +1,110 @@
+//-----------------------------------------------------------------------
+// <copyright file="StandardRelyingPartyApplicationStore.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OpenId.RelyingParty {
+ using System;
+ using System.Collections.Generic;
+ using DotNetOpenAuth.Configuration;
+ using DotNetOpenAuth.Messaging.Bindings;
+ using DotNetOpenAuth.OpenId.ChannelElements;
+
+ /// <summary>
+ /// An in-memory store for Relying Parties, suitable for single server, single process
+ /// ASP.NET web sites.
+ /// </summary>
+ public class StandardRelyingPartyApplicationStore : IOpenIdApplicationStore {
+ /// <summary>
+ /// The nonce store to use.
+ /// </summary>
+ private readonly INonceStore nonceStore;
+
+ /// <summary>
+ /// The association store to use.
+ /// </summary>
+ private readonly ICryptoKeyStore keyStore;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="StandardRelyingPartyApplicationStore"/> class.
+ /// </summary>
+ public StandardRelyingPartyApplicationStore() {
+ this.nonceStore = new NonceMemoryStore(OpenIdElement.Configuration.MaxAuthenticationTime);
+ this.keyStore = new MemoryCryptoKeyStore();
+ }
+
+ #region ICryptoKeyStore Members
+
+ /// <summary>
+ /// Gets the key in a given bucket and handle.
+ /// </summary>
+ /// <param name="bucket">The bucket name. Case sensitive.</param>
+ /// <param name="handle">The key handle. Case sensitive.</param>
+ /// <returns>
+ /// The cryptographic key, or <c>null</c> if no matching key was found.
+ /// </returns>
+ public CryptoKey GetKey(string bucket, string handle) {
+ return this.keyStore.GetKey(bucket, handle);
+ }
+
+ /// <summary>
+ /// Gets a sequence of existing keys within a given bucket.
+ /// </summary>
+ /// <param name="bucket">The bucket name. Case sensitive.</param>
+ /// <returns>
+ /// A sequence of handles and keys, ordered by descending <see cref="CryptoKey.ExpiresUtc"/>.
+ /// </returns>
+ public IEnumerable<KeyValuePair<string, CryptoKey>> GetKeys(string bucket) {
+ return this.keyStore.GetKeys(bucket);
+ }
+
+ /// <summary>
+ /// Stores a cryptographic key.
+ /// </summary>
+ /// <param name="bucket">The name of the bucket to store the key in. Case sensitive.</param>
+ /// <param name="handle">The handle to the key, unique within the bucket. Case sensitive.</param>
+ /// <param name="key">The key to store.</param>
+ /// <exception cref="CryptoKeyCollisionException">Thrown in the event of a conflict with an existing key in the same bucket and with the same handle.</exception>
+ public void StoreKey(string bucket, string handle, CryptoKey key) {
+ this.keyStore.StoreKey(bucket, handle, key);
+ }
+
+ /// <summary>
+ /// Removes the key.
+ /// </summary>
+ /// <param name="bucket">The bucket name. Case sensitive.</param>
+ /// <param name="handle">The key handle. Case sensitive.</param>
+ public void RemoveKey(string bucket, string handle) {
+ this.keyStore.RemoveKey(bucket, handle);
+ }
+
+ #endregion
+
+ #region INonceStore Members
+
+ /// <summary>
+ /// Stores a given nonce and timestamp.
+ /// </summary>
+ /// <param name="context">The context, or namespace, within which the <paramref name="nonce"/> must be unique.</param>
+ /// <param name="nonce">A series of random characters.</param>
+ /// <param name="timestampUtc">The timestamp that together with the nonce string make it unique.
+ /// The timestamp may also be used by the data store to clear out old nonces.</param>
+ /// <returns>
+ /// True if the nonce+timestamp (combination) was not previously in the database.
+ /// False if the nonce was stored previously with the same timestamp.
+ /// </returns>
+ /// <remarks>
+ /// The nonce must be stored for no less than the maximum time window a message may
+ /// be processed within before being discarded as an expired message.
+ /// If the binding element is applicable to your channel, this expiration window
+ /// is retrieved or set using the
+ /// <see cref="StandardExpirationBindingElement.MaximumMessageAge"/> property.
+ /// </remarks>
+ public bool StoreNonce(string context, string nonce, DateTime timestampUtc) {
+ return this.nonceStore.StoreNonce(context, nonce, timestampUtc);
+ }
+
+ #endregion
+ }
+}
diff --git a/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/WellKnownProviders.cs b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/WellKnownProviders.cs
new file mode 100644
index 0000000..ad1a11a
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/WellKnownProviders.cs
@@ -0,0 +1,52 @@
+//-----------------------------------------------------------------------
+// <copyright file="WellKnownProviders.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OpenId.RelyingParty {
+ using System.Diagnostics.CodeAnalysis;
+
+ /// <summary>
+ /// Common OpenID Provider Identifiers.
+ /// </summary>
+ public sealed class WellKnownProviders {
+ /// <summary>
+ /// The Yahoo OP Identifier.
+ /// </summary>
+ [SuppressMessage("Microsoft.Security", "CA2104:DoNotDeclareReadOnlyMutableReferenceTypes", Justification = "Immutable type")]
+ public static readonly Identifier Yahoo = "https://me.yahoo.com/";
+
+ /// <summary>
+ /// The Google OP Identifier.
+ /// </summary>
+ [SuppressMessage("Microsoft.Security", "CA2104:DoNotDeclareReadOnlyMutableReferenceTypes", Justification = "Immutable type")]
+ public static readonly Identifier Google = "https://www.google.com/accounts/o8/id";
+
+ /// <summary>
+ /// The MyOpenID OP Identifier.
+ /// </summary>
+ [SuppressMessage("Microsoft.Security", "CA2104:DoNotDeclareReadOnlyMutableReferenceTypes", Justification = "Immutable type")]
+ public static readonly Identifier MyOpenId = "https://www.myopenid.com/";
+
+ /// <summary>
+ /// The Verisign OP Identifier.
+ /// </summary>
+ [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Verisign", Justification = "The spelling is correct.")]
+ [SuppressMessage("Microsoft.Security", "CA2104:DoNotDeclareReadOnlyMutableReferenceTypes", Justification = "Immutable type")]
+ public static readonly Identifier Verisign = "https://pip.verisignlabs.com/";
+
+ /// <summary>
+ /// The MyVidoop OP Identifier.
+ /// </summary>
+ [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Vidoop", Justification = "The spelling is correct.")]
+ [SuppressMessage("Microsoft.Security", "CA2104:DoNotDeclareReadOnlyMutableReferenceTypes", Justification = "Immutable type")]
+ public static readonly Identifier MyVidoop = "https://myvidoop.com/";
+
+ /// <summary>
+ /// Prevents a default instance of the <see cref="WellKnownProviders"/> class from being created.
+ /// </summary>
+ private WellKnownProviders() {
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/login_failure.png b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/login_failure.png
new file mode 100644
index 0000000..8003700
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/login_failure.png
Binary files differ
diff --git a/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/login_success (lock).png b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/login_success (lock).png
new file mode 100644
index 0000000..bc0c0c8
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/login_success (lock).png
Binary files differ
diff --git a/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/login_success.png b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/login_success.png
new file mode 100644
index 0000000..0ae1365
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/login_success.png
Binary files differ
diff --git a/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/openid_login.png b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/openid_login.png
new file mode 100644
index 0000000..caebd58
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/openid_login.png
Binary files differ
diff --git a/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/spinner.gif b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/spinner.gif
new file mode 100644
index 0000000..9cb298e
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/spinner.gif
Binary files differ
diff --git a/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/UriDiscoveryService.cs b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/UriDiscoveryService.cs
new file mode 100644
index 0000000..7d17fd9
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/UriDiscoveryService.cs
@@ -0,0 +1,139 @@
+//-----------------------------------------------------------------------
+// <copyright file="UriDiscoveryService.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OpenId {
+ using System;
+ using System.Collections.Generic;
+ using System.Linq;
+ using System.Text;
+ using System.Text.RegularExpressions;
+ using System.Web.UI.HtmlControls;
+ using System.Xml;
+ using DotNetOpenAuth.Messaging;
+ using DotNetOpenAuth.OpenId.RelyingParty;
+ using DotNetOpenAuth.Xrds;
+ using DotNetOpenAuth.Yadis;
+
+ /// <summary>
+ /// The discovery service for URI identifiers.
+ /// </summary>
+ public class UriDiscoveryService : IIdentifierDiscoveryService {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="UriDiscoveryService"/> class.
+ /// </summary>
+ public UriDiscoveryService() {
+ }
+
+ #region IDiscoveryService Members
+
+ /// <summary>
+ /// Performs discovery on the specified identifier.
+ /// </summary>
+ /// <param name="identifier">The identifier to perform discovery on.</param>
+ /// <param name="requestHandler">The means to place outgoing HTTP requests.</param>
+ /// <param name="abortDiscoveryChain">if set to <c>true</c>, no further discovery services will be called for this identifier.</param>
+ /// <returns>
+ /// A sequence of service endpoints yielded by discovery. Must not be null, but may be empty.
+ /// </returns>
+ public IEnumerable<IdentifierDiscoveryResult> Discover(Identifier identifier, IDirectWebRequestHandler requestHandler, out bool abortDiscoveryChain) {
+ abortDiscoveryChain = false;
+ var uriIdentifier = identifier as UriIdentifier;
+ if (uriIdentifier == null) {
+ return Enumerable.Empty<IdentifierDiscoveryResult>();
+ }
+
+ var endpoints = new List<IdentifierDiscoveryResult>();
+
+ // Attempt YADIS discovery
+ DiscoveryResult yadisResult = Yadis.Discover(requestHandler, uriIdentifier, identifier.IsDiscoverySecureEndToEnd);
+ if (yadisResult != null) {
+ if (yadisResult.IsXrds) {
+ try {
+ XrdsDocument xrds = new XrdsDocument(yadisResult.ResponseText);
+ var xrdsEndpoints = xrds.XrdElements.CreateServiceEndpoints(yadisResult.NormalizedUri, uriIdentifier);
+
+ // Filter out insecure endpoints if high security is required.
+ if (uriIdentifier.IsDiscoverySecureEndToEnd) {
+ xrdsEndpoints = xrdsEndpoints.Where(se => se.ProviderEndpoint.IsTransportSecure());
+ }
+ endpoints.AddRange(xrdsEndpoints);
+ } catch (XmlException ex) {
+ Logger.Yadis.Error("Error while parsing the XRDS document. Falling back to HTML discovery.", ex);
+ }
+ }
+
+ // Failing YADIS discovery of an XRDS document, we try HTML discovery.
+ if (endpoints.Count == 0) {
+ yadisResult.TryRevertToHtmlResponse();
+ var htmlEndpoints = new List<IdentifierDiscoveryResult>(DiscoverFromHtml(yadisResult.NormalizedUri, uriIdentifier, yadisResult.ResponseText));
+ if (htmlEndpoints.Any()) {
+ Logger.Yadis.DebugFormat("Total services discovered in HTML: {0}", htmlEndpoints.Count);
+ Logger.Yadis.Debug(htmlEndpoints.ToStringDeferred(true));
+ endpoints.AddRange(htmlEndpoints.Where(ep => !uriIdentifier.IsDiscoverySecureEndToEnd || ep.ProviderEndpoint.IsTransportSecure()));
+ if (endpoints.Count == 0) {
+ Logger.Yadis.Info("No HTML discovered endpoints met the security requirements.");
+ }
+ } else {
+ Logger.Yadis.Debug("HTML discovery failed to find any endpoints.");
+ }
+ } else {
+ Logger.Yadis.Debug("Skipping HTML discovery because XRDS contained service endpoints.");
+ }
+ }
+ return endpoints;
+ }
+
+ #endregion
+
+ /// <summary>
+ /// Searches HTML for the HEAD META tags that describe OpenID provider services.
+ /// </summary>
+ /// <param name="claimedIdentifier">The final URL that provided this HTML document.
+ /// This may not be the same as (this) userSuppliedIdentifier if the
+ /// userSuppliedIdentifier pointed to a 301 Redirect.</param>
+ /// <param name="userSuppliedIdentifier">The user supplied identifier.</param>
+ /// <param name="html">The HTML that was downloaded and should be searched.</param>
+ /// <returns>
+ /// A sequence of any discovered ServiceEndpoints.
+ /// </returns>
+ private static IEnumerable<IdentifierDiscoveryResult> DiscoverFromHtml(Uri claimedIdentifier, UriIdentifier userSuppliedIdentifier, string html) {
+ var linkTags = new List<HtmlLink>(HtmlParser.HeadTags<HtmlLink>(html));
+ foreach (var protocol in Protocol.AllPracticalVersions) {
+ // rel attributes are supposed to be interpreted with case INsensitivity,
+ // and is a space-delimited list of values. (http://www.htmlhelp.com/reference/html40/values.html#linktypes)
+ var serverLinkTag = linkTags.WithAttribute("rel").FirstOrDefault(tag => Regex.IsMatch(tag.Attributes["rel"], @"\b" + Regex.Escape(protocol.HtmlDiscoveryProviderKey) + @"\b", RegexOptions.IgnoreCase));
+ if (serverLinkTag == null) {
+ continue;
+ }
+
+ Uri providerEndpoint = null;
+ if (Uri.TryCreate(serverLinkTag.Href, UriKind.Absolute, out providerEndpoint)) {
+ // See if a LocalId tag of the discovered version exists
+ Identifier providerLocalIdentifier = null;
+ var delegateLinkTag = linkTags.WithAttribute("rel").FirstOrDefault(tag => Regex.IsMatch(tag.Attributes["rel"], @"\b" + Regex.Escape(protocol.HtmlDiscoveryLocalIdKey) + @"\b", RegexOptions.IgnoreCase));
+ if (delegateLinkTag != null) {
+ if (Identifier.IsValid(delegateLinkTag.Href)) {
+ providerLocalIdentifier = delegateLinkTag.Href;
+ } else {
+ Logger.Yadis.WarnFormat("Skipping endpoint data because local id is badly formed ({0}).", delegateLinkTag.Href);
+ continue; // skip to next version
+ }
+ }
+
+ // Choose the TypeURI to match the OpenID version detected.
+ string[] typeURIs = { protocol.ClaimedIdentifierServiceTypeURI };
+ yield return IdentifierDiscoveryResult.CreateForClaimedIdentifier(
+ claimedIdentifier,
+ userSuppliedIdentifier,
+ providerLocalIdentifier,
+ new ProviderEndpointDescription(providerEndpoint, typeURIs),
+ (int?)null,
+ (int?)null);
+ }
+ }
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/XriDiscoveryProxyService.cs b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/XriDiscoveryProxyService.cs
new file mode 100644
index 0000000..d80c59e
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/XriDiscoveryProxyService.cs
@@ -0,0 +1,108 @@
+//-----------------------------------------------------------------------
+// <copyright file="XriDiscoveryProxyService.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OpenId {
+ using System;
+ using System.Collections.Generic;
+ using System.Diagnostics.CodeAnalysis;
+ using System.Diagnostics.Contracts;
+ using System.Globalization;
+ using System.Linq;
+ using System.Text;
+ using System.Xml;
+ using DotNetOpenAuth.Configuration;
+ using DotNetOpenAuth.Messaging;
+ using DotNetOpenAuth.OpenId.RelyingParty;
+ using DotNetOpenAuth.Xrds;
+ using DotNetOpenAuth.Yadis;
+
+ /// <summary>
+ /// The discovery service for XRI identifiers that uses an XRI proxy resolver for discovery.
+ /// </summary>
+ [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Xri", Justification = "Acronym")]
+ public class XriDiscoveryProxyService : IIdentifierDiscoveryService {
+ /// <summary>
+ /// The magic URL that will provide us an XRDS document for a given XRI identifier.
+ /// </summary>
+ /// <remarks>
+ /// We use application/xrd+xml instead of application/xrds+xml because it gets
+ /// xri.net to automatically give us exactly the right XRD element for community i-names
+ /// automatically, saving us having to choose which one to use out of the result.
+ /// The ssl=true parameter tells the proxy resolver to accept only SSL connections
+ /// when resolving community i-names.
+ /// </remarks>
+ private const string XriResolverProxyTemplate = "https://{1}/{0}?_xrd_r=application/xrd%2Bxml;sep=false";
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="XriDiscoveryProxyService"/> class.
+ /// </summary>
+ public XriDiscoveryProxyService() {
+ }
+
+ #region IDiscoveryService Members
+
+ /// <summary>
+ /// Performs discovery on the specified identifier.
+ /// </summary>
+ /// <param name="identifier">The identifier to perform discovery on.</param>
+ /// <param name="requestHandler">The means to place outgoing HTTP requests.</param>
+ /// <param name="abortDiscoveryChain">if set to <c>true</c>, no further discovery services will be called for this identifier.</param>
+ /// <returns>
+ /// A sequence of service endpoints yielded by discovery. Must not be null, but may be empty.
+ /// </returns>
+ public IEnumerable<IdentifierDiscoveryResult> Discover(Identifier identifier, IDirectWebRequestHandler requestHandler, out bool abortDiscoveryChain) {
+ abortDiscoveryChain = false;
+ var xriIdentifier = identifier as XriIdentifier;
+ if (xriIdentifier == null) {
+ return Enumerable.Empty<IdentifierDiscoveryResult>();
+ }
+
+ return DownloadXrds(xriIdentifier, requestHandler).XrdElements.CreateServiceEndpoints(xriIdentifier);
+ }
+
+ #endregion
+
+ /// <summary>
+ /// Downloads the XRDS document for this XRI.
+ /// </summary>
+ /// <param name="identifier">The identifier.</param>
+ /// <param name="requestHandler">The request handler.</param>
+ /// <returns>The XRDS document.</returns>
+ private static XrdsDocument DownloadXrds(XriIdentifier identifier, IDirectWebRequestHandler requestHandler) {
+ Contract.Requires<ArgumentNullException>(identifier != null);
+ Contract.Requires<ArgumentNullException>(requestHandler != null);
+ Contract.Ensures(Contract.Result<XrdsDocument>() != null);
+ XrdsDocument doc;
+ using (var xrdsResponse = Yadis.Request(requestHandler, GetXrdsUrl(identifier), identifier.IsDiscoverySecureEndToEnd)) {
+ doc = new XrdsDocument(XmlReader.Create(xrdsResponse.ResponseStream));
+ }
+ ErrorUtilities.VerifyProtocol(doc.IsXrdResolutionSuccessful, OpenIdStrings.XriResolutionFailed);
+ return doc;
+ }
+
+ /// <summary>
+ /// Gets the URL from which this XRI's XRDS document may be downloaded.
+ /// </summary>
+ /// <param name="identifier">The identifier.</param>
+ /// <returns>The URI to HTTP GET from to get the services.</returns>
+ private static Uri GetXrdsUrl(XriIdentifier identifier) {
+ ErrorUtilities.VerifyProtocol(OpenIdElement.Configuration.XriResolver.Enabled, OpenIdStrings.XriResolutionDisabled);
+ string xriResolverProxy = XriResolverProxyTemplate;
+ if (identifier.IsDiscoverySecureEndToEnd) {
+ // Indicate to xri.net that we require SSL to be used for delegated resolution
+ // of community i-names.
+ xriResolverProxy += ";https=true";
+ }
+
+ return new Uri(
+ string.Format(
+ CultureInfo.InvariantCulture,
+ xriResolverProxy,
+ identifier,
+ OpenIdElement.Configuration.XriResolver.Proxy.Name));
+ }
+ }
+}