diff options
author | Andrew Arnott <andrewarnott@gmail.com> | 2011-07-20 07:01:58 -0600 |
---|---|---|
committer | Andrew Arnott <andrewarnott@gmail.com> | 2011-07-20 07:01:58 -0600 |
commit | 1328f88a36187d8aa5890a46e35af59c4df04d3f (patch) | |
tree | c42a3aad4aa21d39b91dcc87a912f8cb96c22c11 /src/DotNetOpenAuth.OpenId.RelyingParty/OpenId | |
parent | d15895e626b73b6f96f561786b4b5c941c0a4bb1 (diff) | |
download | DotNetOpenAuth-1328f88a36187d8aa5890a46e35af59c4df04d3f.zip DotNetOpenAuth-1328f88a36187d8aa5890a46e35af59c4df04d3f.tar.gz DotNetOpenAuth-1328f88a36187d8aa5890a46e35af59c4df04d3f.tar.bz2 |
Splitting up the OpenID profile into OpenID RP and OP. The core OpenID DLL compiles, but the RP and OP ones do not.
Diffstat (limited to 'src/DotNetOpenAuth.OpenId.RelyingParty/OpenId')
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, & 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 <script> 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 <script> 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<T>"/> 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<T>"/> 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<T>"/> 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<T>"/> 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<T>"/> 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<T>"/> 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<T>"/> 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<T>"/> 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 <A> tag that will be written by the caller using + /// <see cref="HtmlTextWriter.RenderBeginTag(HtmlTextWriterTag)"/> after this method. + /// </summary> + /// <param name="writer">The HTML writer.</param> + /// <param name="request">The outgoing authentication request.</param> + /// <param name="windowStatus">The text to try to display in the status bar on mouse hover.</param> + protected void RenderOpenIdMessageTransmissionAsAnchorAttributes(HtmlTextWriter writer, IAuthenticationRequest request, string windowStatus) { + Contract.Requires<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<T>"/> 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<T>"/> 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<T>"/> 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<T>"/> 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 Binary files differnew file mode 100644 index 0000000..8003700 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/login_failure.png diff --git a/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/login_success (lock).png b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/login_success (lock).png Binary files differnew file mode 100644 index 0000000..bc0c0c8 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/login_success (lock).png diff --git a/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/login_success.png b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/login_success.png Binary files differnew file mode 100644 index 0000000..0ae1365 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/login_success.png diff --git a/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/openid_login.png b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/openid_login.png Binary files differnew file mode 100644 index 0000000..caebd58 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/openid_login.png diff --git a/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/spinner.gif b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/spinner.gif Binary files differnew file mode 100644 index 0000000..9cb298e --- /dev/null +++ b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/spinner.gif 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)); + } + } +} |